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')
+