diff --git a/app/Http/Controllers/BotController.php b/app/Http/Controllers/BotController.php index 24802506a2..846ace8ee1 100644 --- a/app/Http/Controllers/BotController.php +++ b/app/Http/Controllers/BotController.php @@ -2,15 +2,101 @@ namespace App\Http\Controllers; +use Auth; +use DB; use Utils; +use Cache; +use Input; use Exception; use App\Libraries\Skype\SkypeResponse; use App\Libraries\CurlUtils; +use App\Models\User; +use App\Models\SecurityCode; use App\Ninja\Intents\BaseIntent; +use App\Ninja\Mailers\UserMailer; class BotController extends Controller { + protected $userMailer; + + public function __construct(UserMailer $userMailer) + { + $this->userMailer = $userMailer; + } + public function handleMessage($platform) + { + $input = Input::all(); + $botUserId = $input['from']['id']; + + if ( ! $token = $this->authenticate($input)) { + return SkypeResponse::message(trans('texts.not_authorized')); + } + + try { + if ($input['type'] === 'contactRelationUpdate') { + // brand new user, ask for their email + if ($input['action'] === 'add') { + $response = SkypeResponse::message(trans('texts.bot_get_email')); + $state = BOT_STATE_GET_EMAIL; + } elseif ($input['action'] === 'remove') { + $this->removeBot($botUserId); + $this->saveState($token, false); + return RESULT_SUCCESS; + } + } else { + $state = $this->loadState($token); + $text = strip_tags($input['text']); + + // user gaves us their email + if ($state === BOT_STATE_GET_EMAIL) { + if ($this->validateEmail($text, $botUserId)) { + $response = SkypeResponse::message(trans('texts.bot_get_code')); + $state = BOT_STATE_GET_CODE; + } else { + $response = SkypeResponse::message(trans('texts.email_not_found', ['email' => $text])); + } + // user sent the scurity code + } elseif ($state === BOT_STATE_GET_CODE) { + if ($this->validateCode($text, $botUserId)) { + $response = SkypeResponse::message(trans('texts.bot_welcome') . trans('texts.bot_help_message')); + $state = BOT_STATE_READY; + } else { + $response = SkypeResponse::message(trans('texts.invalid_code')); + } + // regular chat message + } else { + if ($message === 'help') { + $response = SkypeResponse::message(trans('texts.bot_help_message')); + } elseif ($message == 'status') { + $response = SkypeResponse::message(trans('texts.intent_not_supported')); + } else { + if ( ! $user = User::whereBotUserId($botUserId)->with('account')->first()) { + return SkypeResponse::message(trans('texts.not_authorized')); + } + + Auth::onceUsingId($user->id); + $user->account->loadLocalizationSettings(); + + $data = $this->parseMessage($text); + $intent = BaseIntent::createIntent($state, $data); + $response = $intent->process(); + $state = $intent->getState(); + } + } + } + + $this->saveState($token, $state); + } catch (Exception $exception) { + $response = SkypeResponse::message($exception->getMessage()); + } + + $this->sendResponse($token, $botUserId, $response); + + return RESULT_SUCCESS; + } + + private function authenticate($input) { $headers = getallheaders(); $token = isset($headers['Authorization']) ? $headers['Authorization'] : false; @@ -18,43 +104,13 @@ class BotController extends Controller if (Utils::isNinjaDev()) { // skip validation for testing } elseif ( ! $this->validateToken($token)) { - SkypeResponse::message(trans('texts.not_authorized')); + return false; } - $to = '29:1C-OsU7OWBEDOYJhQUsDkYHmycOwOq9QOg5FVTwRX9ts'; - //$message = 'new invoice for john for 2 items due tomorrow'; - $message = 'invoice acme client for 3 months support, set due date to next thursday and the discount to 10 percent'; - //$message = 'create a new invoice for john smith with a due date of September 7th'; - //$message = 'create a new invoice for john'; - //$message = 'add 2 tickets and set the due date to yesterday'; - //$message = 'set the po number to 0004'; - //$message = 'set the quantity to 20'; - //$message = 'send the invoice'; - //$message = 'show me my products'; - - echo "Message: $message

"; - $token = $this->authenticate(); - - //try { - $state = $this->loadState($token); - $data = $this->parseMessage($message); - - $intent = BaseIntent::createIntent($state, $data); - $message = $intent->process(); - $state = $intent->getState(); - - $this->saveState($token, $state); - /* - } catch (Exception $exception) { - SkypeResponse::message($exception->getMessage()); + if ($token = Cache::get('msbot_token')) { + return $token; } - */ - $this->sendResponse($token, $to, $message); - } - - private function authenticate() - { $clientId = env('MSBOT_CLIENT_ID'); $clientSecret = env('MSBOT_CLIENT_SECRET'); $scope = 'https://graph.microsoft.com/.default'; @@ -64,6 +120,9 @@ class BotController extends Controller $response = CurlUtils::post(MSBOT_LOGIN_URL, $data); $response = json_decode($response); + $expires = ($response->expires_in / 60) - 5; + Cache::put('msbot_token', $response->access_token, $expires); + return $response->access_token; } @@ -103,8 +162,11 @@ class BotController extends Controller 'Content-Type: application/json', ]; + //echo "STATE

" . htmlentities(json_encode($data), JSON_PRETTY_PRINT) . "
"; + $data = '{ eTag: "*", data: "' . addslashes(json_encode($data)) . '" }'; + CurlUtils::post($url, $data, $headers); } @@ -116,10 +178,81 @@ class BotController extends Controller 'Authorization: Bearer ' . $token, ]; + //echo "
" . htmlentities(json_encode(json_decode($message), JSON_PRETTY_PRINT)) . "
"; + $response = CurlUtils::post($url, $message, $headers); - echo "
" . htmlentities(json_encode(json_decode($message), JSON_PRETTY_PRINT)) . "
"; - var_dump($response); + //var_dump($response); + } + + private function validateEmail($email, $botUserId) + { + if ( ! $email || ! $botUserId) { + return false; + } + + // delete any expired codes + SecurityCode::whereBotUserId($botUserId) + ->where('created_at', '<', DB::raw('now() - INTERVAL 10 MINUTE')) + ->delete(); + + if (SecurityCode::whereBotUserId($botUserId)->first()) { + return false; + } + + $user = User::whereEmail($email) + ->whereNull('bot_user_id') + ->first(); + + if ( ! $user) { + return false; + } + + $code = new SecurityCode(); + $code->user_id = $user->id; + $code->account_id = $user->account_id; + $code->code = mt_rand(100000, 999999); + $code->bot_user_id = $botUserId; + $code->save(); + + $this->userMailer->sendSecurityCode($user, $code->code); + + return $code->code; + } + + private function validateCode($input, $botUserId) + { + if ( ! $input || ! $botUserId) { + return false; + } + + $code = SecurityCode::whereBotUserId($botUserId) + ->where('created_at', '>', DB::raw('now() - INTERVAL 10 MINUTE')) + ->where('attempts', '<', 5) + ->first(); + + if ( ! $code) { + return false; + } + + if ( ! hash_equals($code->code, $input)) { + $code->attempts += 1; + $code->save(); + return false; + } + + $user = User::find($code->user_id); + $user->bot_user_id = $code->bot_user_id; + $user->save(); + + return true; + } + + private function removeBot($botUserId) + { + $user = User::whereBotUserId($botUserId)->first(); + $user->bot_user_id = null; + $user->save(); } private function validateToken($token) diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index 4e1f5a1fc6..fb0dc8eca0 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -130,8 +130,9 @@ class OnlinePaymentController extends BaseController } try { - $paymentDriver->completeOffsitePurchase(Input::all()); - Session::flash('message', trans('texts.applied_payment')); + if ($paymentDriver->completeOffsitePurchase(Input::all())) { + Session::flash('message', trans('texts.applied_payment')); + } return redirect()->to('view/' . $invitation->invitation_key); } catch (Exception $exception) { return $this->error($paymentDriver, $exception); diff --git a/app/Http/Requests/CreateInvoiceAPIRequest.php b/app/Http/Requests/CreateInvoiceAPIRequest.php index 141d8788ab..8a3c356642 100644 --- a/app/Http/Requests/CreateInvoiceAPIRequest.php +++ b/app/Http/Requests/CreateInvoiceAPIRequest.php @@ -25,6 +25,10 @@ class CreateInvoiceAPIRequest extends InvoiceRequest 'invoice_items' => 'valid_invoice_items', 'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id, 'discount' => 'positive', + 'invoice_date' => 'date', + 'due_date' => 'date', + 'start_date' => 'date', + 'end_date' => 'date', ]; return $rules; diff --git a/app/Http/Requests/CreateInvoiceRequest.php b/app/Http/Requests/CreateInvoiceRequest.php index a3f556d408..cf309bfd5b 100644 --- a/app/Http/Requests/CreateInvoiceRequest.php +++ b/app/Http/Requests/CreateInvoiceRequest.php @@ -24,6 +24,10 @@ class CreateInvoiceRequest extends InvoiceRequest 'invoice_items' => 'valid_invoice_items', 'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id, 'discount' => 'positive', + 'invoice_date' => 'date', + 'due_date' => 'date', + 'start_date' => 'date', + 'end_date' => 'date', ]; /* There's a problem parsing the dates diff --git a/app/Http/Requests/UpdateInvoiceAPIRequest.php b/app/Http/Requests/UpdateInvoiceAPIRequest.php index 6fa3c4e92e..93b135effa 100644 --- a/app/Http/Requests/UpdateInvoiceAPIRequest.php +++ b/app/Http/Requests/UpdateInvoiceAPIRequest.php @@ -29,6 +29,10 @@ class UpdateInvoiceAPIRequest extends InvoiceRequest 'invoice_items' => 'valid_invoice_items', 'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'discount' => 'positive', + 'invoice_date' => 'date', + 'due_date' => 'date', + 'start_date' => 'date', + 'end_date' => 'date', ]; return $rules; diff --git a/app/Http/Requests/UpdateInvoiceRequest.php b/app/Http/Requests/UpdateInvoiceRequest.php index 96d112b157..de10a49b32 100644 --- a/app/Http/Requests/UpdateInvoiceRequest.php +++ b/app/Http/Requests/UpdateInvoiceRequest.php @@ -20,12 +20,16 @@ class UpdateInvoiceRequest extends InvoiceRequest public function rules() { $invoiceId = $this->entity()->id; - + $rules = [ 'client.contacts' => 'valid_contacts', 'invoice_items' => 'valid_invoice_items', 'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'discount' => 'positive', + 'invoice_date' => 'date', + 'due_date' => 'date', + 'start_date' => 'date', + 'end_date' => 'date', ]; /* There's a problem parsing the dates diff --git a/app/Http/routes.php b/app/Http/routes.php index 5e4a7752de..8377399a6d 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -85,7 +85,7 @@ Route::match(['GET', 'POST'], '/buy_now/{gateway_type?}', 'OnlinePaymentControll Route::post('/hook/email_bounced', 'AppController@emailBounced'); Route::post('/hook/email_opened', 'AppController@emailOpened'); -Route::any('/hook/bot/{platform?}', 'BotController@handleMessage'); +Route::post('/hook/bot/{platform?}', 'BotController@handleMessage'); Route::post('/payment_hook/{accountKey}/{gatewayId}', 'OnlinePaymentController@handlePaymentWebhook'); // Laravel auth routes @@ -801,6 +801,21 @@ if (!defined('CONTACT_EMAIL')) { define('SKYPE_CARD_CAROUSEL', 'message/card.carousel'); define('SKYPE_CARD_HERO', ''); + define('BOT_STATE_GET_EMAIL', 'get_email'); + define('BOT_STATE_GET_CODE', 'get_code'); + define('BOT_STATE_READY', 'ready'); + define('SIMILAR_MIN_THRESHOLD', 50); + + // https://docs.botframework.com/en-us/csharp/builder/sdkreference/attachments.html + define('SKYPE_BUTTON_OPEN_URL', 'openUrl'); + define('SKYPE_BUTTON_IM_BACK', 'imBack'); + define('SKYPE_BUTTON_POST_BACK', 'postBack'); + define('SKYPE_BUTTON_CALL', 'call'); // "tel:123123123123" + define('SKYPE_BUTTON_PLAY_AUDIO', 'playAudio'); + define('SKYPE_BUTTON_PLAY_VIDEO', 'playVideo'); + define('SKYPE_BUTTON_SHOW_IMAGE', 'showImage'); + define('SKYPE_BUTTON_DOWNLOAD_FILE', 'downloadFile'); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], diff --git a/app/Libraries/Skype/ButtonCard.php b/app/Libraries/Skype/ButtonCard.php index c81060ccdf..f2e1c52ae6 100644 --- a/app/Libraries/Skype/ButtonCard.php +++ b/app/Libraries/Skype/ButtonCard.php @@ -2,10 +2,11 @@ class ButtonCard { - public function __construct($type, $title, $value) + public function __construct($type, $title, $value, $url = false) { $this->type = $type; $this->title = $title; $this->value = $value; + $this->image = $url; } } diff --git a/app/Libraries/Skype/HeroCard.php b/app/Libraries/Skype/HeroCard.php index 468d17d10d..2218390100 100644 --- a/app/Libraries/Skype/HeroCard.php +++ b/app/Libraries/Skype/HeroCard.php @@ -26,8 +26,8 @@ class HeroCard $this->content->text = $text; } - public function addButton($type, $title, $value) + public function addButton($type, $title, $value, $url = false) { - $this->content->buttons[] = new ButtonCard($type, $title, $value); + $this->content->buttons[] = new ButtonCard($type, $title, $value, $url); } } diff --git a/app/Libraries/Skype/InvoiceCard.php b/app/Libraries/Skype/InvoiceCard.php index 24c928d32a..2a6d11df1c 100644 --- a/app/Libraries/Skype/InvoiceCard.php +++ b/app/Libraries/Skype/InvoiceCard.php @@ -40,8 +40,12 @@ class InvoiceCard $this->setTotal($invoice->present()->requestedAmount); - $this->addButton('imBack', trans('texts.send_email'), 'send_email'); - $this->addButton('imBack', trans('texts.download_pdf'), 'download_pdf'); + if (floatval($invoice->amount)) { + $this->addButton(SKYPE_BUTTON_OPEN_URL, trans('texts.download_pdf'), $invoice->getInvitationLink('view', true)); + $this->addButton(SKYPE_BUTTON_IM_BACK, trans('texts.email_invoice'), trans('texts.email_invoice')); + } else { + $this->addButton(SKYPE_BUTTON_IM_BACK, trans('texts.list_products'), trans('texts.list_products')); + } } public function setTitle($title) @@ -68,8 +72,8 @@ class InvoiceCard $this->content->items[] = new InvoiceItemCard($item, $account); } - public function addButton($type, $title, $value) + public function addButton($type, $title, $value, $url = false) { - $this->content->buttons[] = new ButtonCard($type, $title, $value); + $this->content->buttons[] = new ButtonCard($type, $title, $value, $url); } } diff --git a/app/Libraries/Skype/InvoiceItemCard.php b/app/Libraries/Skype/InvoiceItemCard.php index 17ac91a912..7922d70530 100644 --- a/app/Libraries/Skype/InvoiceItemCard.php +++ b/app/Libraries/Skype/InvoiceItemCard.php @@ -4,7 +4,7 @@ class InvoiceItemCard { public function __construct($invoiceItem, $account) { - $this->title = $invoiceItem->product_key; + $this->title = intval($invoiceItem->qty) . ' ' . $invoiceItem->product_key; $this->subtitle = $invoiceItem->notes; $this->quantity = $invoiceItem->qty; $this->price = $account->formatMoney($invoiceItem->cost); diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 432e811bff..d639c5038e 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -516,6 +516,15 @@ class Invoice extends EntityModel implements BalanceAffecting return self::calcLink($this); } + public function getInvitationLink($type = 'view', $forceOnsite = false) + { + if ( ! $this->relationLoaded('invitations')) { + $this->load('invitations'); + } + + return $this->invitations[0]->getLink($type, $forceOnsite); + } + /** * @return mixed */ diff --git a/app/Models/SecurityCode.php b/app/Models/SecurityCode.php new file mode 100644 index 0000000000..1725e65889 --- /dev/null +++ b/app/Models/SecurityCode.php @@ -0,0 +1,15 @@ +$reference = new stdClass; @@ -54,7 +55,7 @@ class BaseIntent $intent = str_replace('Entity', $entityType, $intent); $className = "App\\Ninja\\Intents\\{$intent}Intent"; - echo "Intent: $intent

"; + //echo "Intent: $intent

"; if ( ! class_exists($className)) { throw new Exception(trans('texts.intent_not_supported')); @@ -69,7 +70,7 @@ class BaseIntent // do nothing by default } - public function setEntities($entityType, $entities) + public function setStateEntities($entityType, $entities) { if ( ! is_array($entities)) { $entities = [$entities]; @@ -81,7 +82,7 @@ class BaseIntent $state->current->$entityType = $entities; } - public function setEntityType($entityType) + public function setStateEntityType($entityType) { $state = $this->state; @@ -93,24 +94,24 @@ class BaseIntent $state->current->entityType = $entityType; } - public function entities($entityType) + public function stateEntities($entityType) { return $this->state->current->$entityType; } - public function entity($entityType) + public function stateEntity($entityType) { $entities = $this->state->current->$entityType; return count($entities) ? $entities[0] : false; } - public function previousEntities($entityType) + public function previousStateEntities($entityType) { return $this->state->previous->$entityType; } - public function entityType() + public function stateEntityType() { return $this->state->current->entityType; } @@ -121,7 +122,7 @@ class BaseIntent return $this->state; } - protected function parseClient() + protected function requestClient() { $clientRepo = app('App\Ninja\Repositories\ClientRepository'); $client = false; @@ -135,7 +136,7 @@ class BaseIntent return $client; } - protected function parseFields() + protected function requestFields() { $data = []; @@ -167,6 +168,13 @@ class BaseIntent } } + foreach ($this->fieldMap as $key => $value) { + if (isset($data[$key])) { + $data[$value] = $data[$key]; + unset($data[$key]); + } + } + return $data; } diff --git a/app/Ninja/Intents/CreateInvoiceIntent.php b/app/Ninja/Intents/CreateInvoiceIntent.php index 3cb0abb8e1..96a1b9d64d 100644 --- a/app/Ninja/Intents/CreateInvoiceIntent.php +++ b/app/Ninja/Intents/CreateInvoiceIntent.php @@ -7,19 +7,19 @@ class CreateInvoiceIntent extends InvoiceIntent { public function process() { - $client = $this->parseClient(); - $invoiceItems = $this->parseInvoiceItems(); + $client = $this->requestClient(); + $invoiceItems = $this->requestInvoiceItems(); if ( ! $client) { throw new Exception(trans('texts.client_not_found')); } - $data = array_merge($this->parseFields(), [ + $data = array_merge($this->requestFields(), [ 'client_id' => $client->id, 'invoice_items' => $invoiceItems, ]); - var_dump($data); + //var_dump($data); $valid = EntityModel::validate($data, ENTITY_INVOICE); @@ -27,16 +27,17 @@ class CreateInvoiceIntent extends InvoiceIntent throw new Exception($valid); } - $invoice = $this->invoiceRepo->save($data); + $invoiceService = app('App\Services\InvoiceService'); + $invoice = $invoiceService->save($data); $invoiceItemIds = array_map(function($item) { return $item['public_id']; }, $invoice->invoice_items->toArray()); - $this->setEntityType(ENTITY_INVOICE); - $this->setEntities(ENTITY_CLIENT, $client->public_id); - $this->setEntities(ENTITY_INVOICE, $invoice->public_id); - $this->setEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds); + $this->setStateEntityType(ENTITY_INVOICE); + $this->setStateEntities(ENTITY_CLIENT, $client->public_id); + $this->setStateEntities(ENTITY_INVOICE, $invoice->public_id); + $this->setStateEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds); return $this->createResponse(SKYPE_CARD_RECEIPT, $invoice->present()->skypeBot); } diff --git a/app/Ninja/Intents/EmailInvoiceIntent.php b/app/Ninja/Intents/EmailInvoiceIntent.php index 46e07ac0ad..2d857c9280 100644 --- a/app/Ninja/Intents/EmailInvoiceIntent.php +++ b/app/Ninja/Intents/EmailInvoiceIntent.php @@ -10,7 +10,7 @@ class EmailInvoiceIntent extends InvoiceIntent { public function process() { - $invoice = $this->invoice(); + $invoice = $this->stateInvoice(); if ( ! Auth::user()->can('edit', $invoice)) { throw new Exception(trans('texts.not_allowed')); @@ -21,6 +21,12 @@ class EmailInvoiceIntent extends InvoiceIntent $message = trans('texts.bot_emailed_' . $invoice->getEntityType()); + if (Auth::user()->notify_viewed) { + $message .= '
' . trans('texts.bot_emailed_notify_viewed'); + } elseif (Auth::user()->notify_paid) { + $message .= '
' . trans('texts.bot_emailed_notify_paid'); + } + return SkypeResponse::message($message); } } diff --git a/app/Ninja/Intents/InvoiceIntent.php b/app/Ninja/Intents/InvoiceIntent.php index f8e69af638..389082cca0 100644 --- a/app/Ninja/Intents/InvoiceIntent.php +++ b/app/Ninja/Intents/InvoiceIntent.php @@ -6,8 +6,10 @@ use App\Models\Invoice; class InvoiceIntent extends BaseIntent { - private $_invoice; - private $_invoiceItem; + protected $fieldMap = [ + 'deposit' => 'partial', + 'due' => 'due_date', + ]; public function __construct($state, $data) { @@ -16,13 +18,9 @@ class InvoiceIntent extends BaseIntent parent::__construct($state, $data); } - protected function invoice() + protected function stateInvoice() { - if ($this->_invoice) { - return $this->_invoice; - } - - $invoiceId = $this->entity(ENTITY_INVOICE); + $invoiceId = $this->stateEntity(ENTITY_INVOICE); if ( ! $invoiceId) { throw new Exception(trans('texts.intent_not_supported')); @@ -41,20 +39,7 @@ class InvoiceIntent extends BaseIntent return $invoice; } - protected function invoiceItem() - { - if ($this->_invoiceItem) { - return $this->_invoiceItem; - } - - $invoiceItemId = $this->entity(ENTITY_INVOICE_ITEM); - - if ( ! $invoiceItemId) { - $invoice = $this->invoice(); - } - } - - protected function parseInvoiceItems() + protected function requestInvoiceItems() { $productRepo = app('App\Ninja\Repositories\ProductRepository'); @@ -76,20 +61,33 @@ class InvoiceIntent extends BaseIntent } } - $item = $product->toArray(); - $item['qty'] = $qty; + if ($product) { + $item['qty'] = $qty; + $item['product_key'] = $product->product_key; + $item['cost'] = $product->cost; + $item['notes'] = $product->notes; - $invoiceItems[] = $item; + if ($taxRate = $product->default_tax_rate) { + $item['tax_name1'] = $taxRate->name; + $item['tax_rate1'] = $taxRate->rate; + } + + $invoiceItems[] = $item; + } } } + /* + if ( ! count($invoiceItems)) { + foreach ($this->data->entities as $param) { + if ($param->type == 'Product') { + $product = $productRepo->findPhonetically($param->entity); + } + } + } + */ + return $invoiceItems; } - protected function parseFields() - { - $data = parent::parseFields(); - - return $data; - } } diff --git a/app/Ninja/Intents/ListProductsIntent.php b/app/Ninja/Intents/ListProductsIntent.php index deb510b41f..fd0700d2fa 100644 --- a/app/Ninja/Intents/ListProductsIntent.php +++ b/app/Ninja/Intents/ListProductsIntent.php @@ -16,8 +16,8 @@ class ListProductsIntent extends ProductIntent ->get() ->transform(function($item, $key) use ($account) { $card = $item->present()->skypeBot($account); - if ($this->entity(ENTITY_INVOICE)) { - $card->addButton('imBack', trans('texts.add_to_invoice'), trans('texts.add_to_invoice_command', ['product' => $item->product_key])); + if ($this->stateEntity(ENTITY_INVOICE)) { + $card->addButton('imBack', trans('texts.add_to_invoice'), trans('texts.add_product_to_invoice', ['product' => $item->product_key])); } return $card; }); diff --git a/app/Ninja/Intents/UpdateInvoiceIntent.php b/app/Ninja/Intents/UpdateInvoiceIntent.php index b271331a3b..bcba89456d 100644 --- a/app/Ninja/Intents/UpdateInvoiceIntent.php +++ b/app/Ninja/Intents/UpdateInvoiceIntent.php @@ -8,10 +8,10 @@ class UpdateInvoiceIntent extends InvoiceIntent { public function process() { - $invoice = $this->invoice(); - $invoiceItems = $this->parseInvoiceItems(); + $invoice = $this->stateInvoice(); + $invoiceItems = $this->requestInvoiceItems(); - $data = array_merge($this->parseFields(), [ + $data = array_merge($this->requestFields(), [ 'public_id' => $invoice->public_id, 'invoice_items' => array_merge($invoice->invoice_items->toArray(), $invoiceItems), ]); @@ -27,7 +27,7 @@ class UpdateInvoiceIntent extends InvoiceIntent } } - var_dump($data); + //var_dump($data); $valid = EntityModel::validate($data, ENTITY_INVOICE, $invoice); @@ -42,7 +42,7 @@ class UpdateInvoiceIntent extends InvoiceIntent return $item['public_id']; }, $invoiceItems); - $this->setEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds); + $this->setStateEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds); $response = $invoice ->load('invoice_items') diff --git a/app/Ninja/Mailers/UserMailer.php b/app/Ninja/Mailers/UserMailer.php index 492a551a9e..2508a7f93f 100644 --- a/app/Ninja/Mailers/UserMailer.php +++ b/app/Ninja/Mailers/UserMailer.php @@ -109,4 +109,20 @@ class UserMailer extends Mailer $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } + + public function sendSecurityCode($user, $code) + { + if (!$user->email) { + return; + } + + $subject = trans('texts.security_code_email_subject'); + $view = 'security_code'; + $data = [ + 'userName' => $user->getDisplayName(), + 'code' => $code, + ]; + + $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); + } } diff --git a/app/Ninja/PaymentDrivers/StripePaymentDriver.php b/app/Ninja/PaymentDrivers/StripePaymentDriver.php index 088f09c192..90b9d0ab49 100644 --- a/app/Ninja/PaymentDrivers/StripePaymentDriver.php +++ b/app/Ninja/PaymentDrivers/StripePaymentDriver.php @@ -362,15 +362,15 @@ class StripePaymentDriver extends BasePaymentDriver $eventDetails = $this->makeStripeCall('GET', 'events/'.$eventId); if (is_string($eventDetails) || !$eventDetails) { - throw new Exception('Could not get event details'); + return false; } if ($eventType != $eventDetails['type']) { - throw new Exception('Event type mismatch'); + return false; } if (!$eventDetails['pending_webhooks']) { - throw new Exception('This is not a pending event'); + return false; } if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded' || $eventType == 'charge.refunded') { @@ -380,7 +380,7 @@ class StripePaymentDriver extends BasePaymentDriver $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first(); if (!$payment) { - throw new Exception('Unknown payment'); + return false; } if ($eventType == 'charge.failed') { diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index 4b81a7f1ba..753bb0e6f4 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -138,7 +138,7 @@ class ClientRepository extends BaseRepository $clientNameMeta = metaphone($clientName); $map = []; - $max = 0; + $max = SIMILAR_MIN_THRESHOLD; $clientId = 0; $clients = Client::scope()->get(['id', 'name', 'public_id']); @@ -160,7 +160,7 @@ class ClientRepository extends BaseRepository $contacts = Contact::scope()->get(['client_id', 'first_name', 'last_name', 'public_id']); foreach ($contacts as $contact) { - if ( ! $contact->getFullName()) { + if ( ! $contact->getFullName() || ! isset($map[$contact->client_id])) { continue; } @@ -172,7 +172,7 @@ class ClientRepository extends BaseRepository } } - return isset($map[$clientId]) ? $map[$clientId] : null; + return ($clientId && isset($map[$clientId])) ? $map[$clientId] : null; } } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index fd78698563..b310ac1318 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -271,6 +271,7 @@ class InvoiceRepository extends BaseRepository $entityType = ENTITY_QUOTE; } $invoice = $account->createInvoice($entityType, $data['client_id']); + $invoice->invoice_date = date_create()->format('Y-m-d'); if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) { $invoice->has_tasks = true; } diff --git a/app/Ninja/Repositories/ProductRepository.php b/app/Ninja/Repositories/ProductRepository.php index 0f8b77c46a..bf20e03171 100644 --- a/app/Ninja/Repositories/ProductRepository.php +++ b/app/Ninja/Repositories/ProductRepository.php @@ -61,10 +61,12 @@ class ProductRepository extends BaseRepository $productNameMeta = metaphone($productName); $map = []; - $max = 0; + $max = SIMILAR_MIN_THRESHOLD; $productId = 0; - $products = Product::scope()->get(['product_key', 'notes', 'cost']); + $products = Product::scope() + ->with('default_tax_rate') + ->get(); foreach ($products as $product) { if ( ! $product->product_key) { @@ -80,7 +82,7 @@ class ProductRepository extends BaseRepository } } - return $map[$productId]; + return ($productId && isset($map[$productId])) ? $map[$productId] : null; } diff --git a/database/migrations/2016_08_10_184027_add_support_for_bots.php b/database/migrations/2016_08_10_184027_add_support_for_bots.php new file mode 100644 index 0000000000..40704b4564 --- /dev/null +++ b/database/migrations/2016_08_10_184027_add_support_for_bots.php @@ -0,0 +1,72 @@ +increments('id'); + $table->unsignedInteger('account_id')->index(); + $table->unsignedInteger('user_id')->nullable(); + $table->unsignedInteger('contact_id')->nullable(); + $table->smallInteger('attempts'); + $table->string('code')->nullable(); + $table->string('bot_user_id')->unique(); + $table->timestamp('created_at')->useCurrent(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::table('users', function($table) + { + $table->string('bot_user_id')->nullable(); + }); + + Schema::table('contacts', function($table) + { + $table->string('bot_user_id')->nullable(); + }); + + Schema::table('accounts', function($table) + { + $table->boolean('include_item_taxes_inline')->default(false); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('security_codes'); + + Schema::table('users', function($table) + { + $table->dropColumn('bot_user_id'); + }); + + Schema::table('contacts', function($table) + { + $table->dropColumn('bot_user_id'); + }); + + Schema::table('accounts', function($table) + { + $table->dropColumn('include_item_taxes_inline'); + }); + } +} diff --git a/package.json b/package.json index 67209c7798..0f743c4d17 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "grunt-contrib-uglify": "~0.2.2", "grunt-dump-dir": "^0.1.2", "gulp": "^3.8.8", - "laravel-elixir": "*" + "laravel-elixir": "^6.0.0" }, "dependencies": { "grunt-dump-dir": "^0.1.2" diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index b88569670d..a93fbfe0a7 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -2056,10 +2056,23 @@ $LANG = array( 'intent_not_supported' => 'Sorry, I\'m not able to do that.', 'client_not_found' => 'We weren\'t able to find the client', 'not_allowed' => 'Sorry, you don\'t have the needed permissions', - 'bot_emailed_invoice' => 'Your invoice has been emailed', + 'bot_emailed_invoice' => 'Your invoice has been sent.', + 'bot_emailed_notify_viewed' => 'I\'ll email you when it\'s viewed.', + 'bot_emailed_notify_paid' => 'I\'ll email you when it\'s paid.', 'add_to_invoice' => 'Add to invoice', - 'add_to_invoice_command' => 'Add 1 :product', + 'add_product_to_invoice' => 'Add 1 :product', 'not_authorized' => 'Your are not authorized', + 'bot_get_email' => 'Hi! (wave)
Thanks for trying the Invoice Ninja Bot.
Send me your account email to get started.', + 'bot_get_code' => 'Thanks! I\'ve sent a you an email with your security code.', + 'bot_welcome' => 'That\'s it, your account is verified.
', + 'email_not_found' => 'I wasn\'t able to find an available account for :email', + 'invalid_code' => 'The code is not correct', + 'security_code_email_subject' => 'Security code for Invoice Ninja Bot', + 'security_code_email_line1' => 'This is your Invoice Ninja Bot security code.', + 'security_code_email_line2' => 'Note: it will expire in 10 minutes.', + 'bot_help_message' => 'We currently support:
• Create\update\email an invoice
• List products
For example:
invoice bob for 2 tickets, set the due date to next thursday and the discount to 10 percent', + 'list_products' => 'List Products', + ); return $LANG; diff --git a/resources/views/emails/security_code_html.blade.php b/resources/views/emails/security_code_html.blade.php new file mode 100644 index 0000000000..a6e8751f2d --- /dev/null +++ b/resources/views/emails/security_code_html.blade.php @@ -0,0 +1,24 @@ +@extends('emails.master_user') + +@section('body') +

+ {{ trans('texts.email_salutation', ['name' => $userName]) }} +
+   +
+ {{ trans("texts.security_code_email_line1") }} +
+   +
+

{{ $code }}

+
+   +
+ {{ trans("texts.security_code_email_line2") }} +
+   +
+ {{ trans('texts.email_signature') }}
+ {{ trans('texts.email_from') }} +
+@stop diff --git a/resources/views/emails/security_code_text.blade.php b/resources/views/emails/security_code_text.blade.php new file mode 100644 index 0000000000..0c19f076b2 --- /dev/null +++ b/resources/views/emails/security_code_text.blade.php @@ -0,0 +1,10 @@ +{!! trans('texts.email_salutation', ['name' => $userName]) !!} + +{!! trans("texts.security_code_email_line1") !!} + +{!! $code !!} + +{!! trans("texts.security_code_email_line2") !!} + +{!! trans('texts.email_signature') !!} +{!! trans('texts.email_from') !!}