1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 12:42:36 +01:00

Working on the bot

This commit is contained in:
Hillel Coren 2016-08-13 22:19:37 +03:00
parent e4929c1008
commit ce2c71843c
29 changed files with 463 additions and 118 deletions

View File

@ -2,15 +2,101 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth;
use DB;
use Utils; use Utils;
use Cache;
use Input;
use Exception; use Exception;
use App\Libraries\Skype\SkypeResponse; use App\Libraries\Skype\SkypeResponse;
use App\Libraries\CurlUtils; use App\Libraries\CurlUtils;
use App\Models\User;
use App\Models\SecurityCode;
use App\Ninja\Intents\BaseIntent; use App\Ninja\Intents\BaseIntent;
use App\Ninja\Mailers\UserMailer;
class BotController extends Controller class BotController extends Controller
{ {
protected $userMailer;
public function __construct(UserMailer $userMailer)
{
$this->userMailer = $userMailer;
}
public function handleMessage($platform) 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(); $headers = getallheaders();
$token = isset($headers['Authorization']) ? $headers['Authorization'] : false; $token = isset($headers['Authorization']) ? $headers['Authorization'] : false;
@ -18,43 +104,13 @@ class BotController extends Controller
if (Utils::isNinjaDev()) { if (Utils::isNinjaDev()) {
// skip validation for testing // skip validation for testing
} elseif ( ! $this->validateToken($token)) { } elseif ( ! $this->validateToken($token)) {
SkypeResponse::message(trans('texts.not_authorized')); return false;
} }
$to = '29:1C-OsU7OWBEDOYJhQUsDkYHmycOwOq9QOg5FVTwRX9ts'; if ($token = Cache::get('msbot_token')) {
//$message = 'new invoice for john for 2 items due tomorrow'; return $token;
$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 <p>";
$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());
} }
*/
$this->sendResponse($token, $to, $message);
}
private function authenticate()
{
$clientId = env('MSBOT_CLIENT_ID'); $clientId = env('MSBOT_CLIENT_ID');
$clientSecret = env('MSBOT_CLIENT_SECRET'); $clientSecret = env('MSBOT_CLIENT_SECRET');
$scope = 'https://graph.microsoft.com/.default'; $scope = 'https://graph.microsoft.com/.default';
@ -64,6 +120,9 @@ class BotController extends Controller
$response = CurlUtils::post(MSBOT_LOGIN_URL, $data); $response = CurlUtils::post(MSBOT_LOGIN_URL, $data);
$response = json_decode($response); $response = json_decode($response);
$expires = ($response->expires_in / 60) - 5;
Cache::put('msbot_token', $response->access_token, $expires);
return $response->access_token; return $response->access_token;
} }
@ -103,8 +162,11 @@ class BotController extends Controller
'Content-Type: application/json', 'Content-Type: application/json',
]; ];
//echo "STATE<pre>" . htmlentities(json_encode($data), JSON_PRETTY_PRINT) . "</pre>";
$data = '{ eTag: "*", data: "' . addslashes(json_encode($data)) . '" }'; $data = '{ eTag: "*", data: "' . addslashes(json_encode($data)) . '" }';
CurlUtils::post($url, $data, $headers); CurlUtils::post($url, $data, $headers);
} }
@ -116,10 +178,81 @@ class BotController extends Controller
'Authorization: Bearer ' . $token, 'Authorization: Bearer ' . $token,
]; ];
//echo "<pre>" . htmlentities(json_encode(json_decode($message), JSON_PRETTY_PRINT)) . "</pre>";
$response = CurlUtils::post($url, $message, $headers); $response = CurlUtils::post($url, $message, $headers);
echo "<pre>" . htmlentities(json_encode(json_decode($message), JSON_PRETTY_PRINT)) . "</pre>"; //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) private function validateToken($token)

View File

@ -130,8 +130,9 @@ class OnlinePaymentController extends BaseController
} }
try { try {
$paymentDriver->completeOffsitePurchase(Input::all()); if ($paymentDriver->completeOffsitePurchase(Input::all())) {
Session::flash('message', trans('texts.applied_payment')); Session::flash('message', trans('texts.applied_payment'));
}
return redirect()->to('view/' . $invitation->invitation_key); return redirect()->to('view/' . $invitation->invitation_key);
} catch (Exception $exception) { } catch (Exception $exception) {
return $this->error($paymentDriver, $exception); return $this->error($paymentDriver, $exception);

View File

@ -25,6 +25,10 @@ class CreateInvoiceAPIRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items', 'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id, 'invoice_number' => 'unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
]; ];
return $rules; return $rules;

View File

@ -24,6 +24,10 @@ class CreateInvoiceRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items', 'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id, 'invoice_number' => 'required|unique:invoices,invoice_number,,id,account_id,' . $this->user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
]; ];
/* There's a problem parsing the dates /* There's a problem parsing the dates

View File

@ -29,6 +29,10 @@ class UpdateInvoiceAPIRequest extends InvoiceRequest
'invoice_items' => 'valid_invoice_items', 'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'invoice_number' => 'unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
]; ];
return $rules; return $rules;

View File

@ -20,12 +20,16 @@ class UpdateInvoiceRequest extends InvoiceRequest
public function rules() public function rules()
{ {
$invoiceId = $this->entity()->id; $invoiceId = $this->entity()->id;
$rules = [ $rules = [
'client.contacts' => 'valid_contacts', 'client.contacts' => 'valid_contacts',
'invoice_items' => 'valid_invoice_items', 'invoice_items' => 'valid_invoice_items',
'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id, 'invoice_number' => 'required|unique:invoices,invoice_number,' . $invoiceId . ',id,account_id,' . $this->user()->account_id,
'discount' => 'positive', 'discount' => 'positive',
'invoice_date' => 'date',
'due_date' => 'date',
'start_date' => 'date',
'end_date' => 'date',
]; ];
/* There's a problem parsing the dates /* There's a problem parsing the dates

View File

@ -85,7 +85,7 @@ Route::match(['GET', 'POST'], '/buy_now/{gateway_type?}', 'OnlinePaymentControll
Route::post('/hook/email_bounced', 'AppController@emailBounced'); Route::post('/hook/email_bounced', 'AppController@emailBounced');
Route::post('/hook/email_opened', 'AppController@emailOpened'); 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'); Route::post('/payment_hook/{accountKey}/{gatewayId}', 'OnlinePaymentController@handlePaymentWebhook');
// Laravel auth routes // Laravel auth routes
@ -801,6 +801,21 @@ if (!defined('CONTACT_EMAIL')) {
define('SKYPE_CARD_CAROUSEL', 'message/card.carousel'); define('SKYPE_CARD_CAROUSEL', 'message/card.carousel');
define('SKYPE_CARD_HERO', ''); 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 = [ $creditCards = [
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],
2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'],

View File

@ -2,10 +2,11 @@
class ButtonCard class ButtonCard
{ {
public function __construct($type, $title, $value) public function __construct($type, $title, $value, $url = false)
{ {
$this->type = $type; $this->type = $type;
$this->title = $title; $this->title = $title;
$this->value = $value; $this->value = $value;
$this->image = $url;
} }
} }

View File

@ -26,8 +26,8 @@ class HeroCard
$this->content->text = $text; $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);
} }
} }

View File

@ -40,8 +40,12 @@ class InvoiceCard
$this->setTotal($invoice->present()->requestedAmount); $this->setTotal($invoice->present()->requestedAmount);
$this->addButton('imBack', trans('texts.send_email'), 'send_email'); if (floatval($invoice->amount)) {
$this->addButton('imBack', trans('texts.download_pdf'), 'download_pdf'); $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) public function setTitle($title)
@ -68,8 +72,8 @@ class InvoiceCard
$this->content->items[] = new InvoiceItemCard($item, $account); $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);
} }
} }

View File

@ -4,7 +4,7 @@ class InvoiceItemCard
{ {
public function __construct($invoiceItem, $account) public function __construct($invoiceItem, $account)
{ {
$this->title = $invoiceItem->product_key; $this->title = intval($invoiceItem->qty) . ' ' . $invoiceItem->product_key;
$this->subtitle = $invoiceItem->notes; $this->subtitle = $invoiceItem->notes;
$this->quantity = $invoiceItem->qty; $this->quantity = $invoiceItem->qty;
$this->price = $account->formatMoney($invoiceItem->cost); $this->price = $account->formatMoney($invoiceItem->cost);

View File

@ -516,6 +516,15 @@ class Invoice extends EntityModel implements BalanceAffecting
return self::calcLink($this); 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 * @return mixed
*/ */

View File

@ -0,0 +1,15 @@
<?php namespace App\Models;
use Eloquent;
/**
* Class DatetimeFormat
*/
class SecurityCode extends Eloquent
{
/**
* @var bool
*/
public $timestamps = false;
}

View File

@ -9,11 +9,12 @@ class BaseIntent
{ {
protected $state; protected $state;
protected $parameters; protected $parameters;
protected $fieldMap = [];
public function __construct($state, $data) public function __construct($state, $data)
{ {
//if (true) { //if (true) {
if ( ! $state) { if ( ! $state || is_string($state)) {
$state = new stdClass; $state = new stdClass;
foreach (['current', 'previous'] as $reference) { foreach (['current', 'previous'] as $reference) {
$state->$reference = new stdClass; $state->$reference = new stdClass;
@ -54,7 +55,7 @@ class BaseIntent
$intent = str_replace('Entity', $entityType, $intent); $intent = str_replace('Entity', $entityType, $intent);
$className = "App\\Ninja\\Intents\\{$intent}Intent"; $className = "App\\Ninja\\Intents\\{$intent}Intent";
echo "Intent: $intent<p>"; //echo "Intent: $intent<p>";
if ( ! class_exists($className)) { if ( ! class_exists($className)) {
throw new Exception(trans('texts.intent_not_supported')); throw new Exception(trans('texts.intent_not_supported'));
@ -69,7 +70,7 @@ class BaseIntent
// do nothing by default // do nothing by default
} }
public function setEntities($entityType, $entities) public function setStateEntities($entityType, $entities)
{ {
if ( ! is_array($entities)) { if ( ! is_array($entities)) {
$entities = [$entities]; $entities = [$entities];
@ -81,7 +82,7 @@ class BaseIntent
$state->current->$entityType = $entities; $state->current->$entityType = $entities;
} }
public function setEntityType($entityType) public function setStateEntityType($entityType)
{ {
$state = $this->state; $state = $this->state;
@ -93,24 +94,24 @@ class BaseIntent
$state->current->entityType = $entityType; $state->current->entityType = $entityType;
} }
public function entities($entityType) public function stateEntities($entityType)
{ {
return $this->state->current->$entityType; return $this->state->current->$entityType;
} }
public function entity($entityType) public function stateEntity($entityType)
{ {
$entities = $this->state->current->$entityType; $entities = $this->state->current->$entityType;
return count($entities) ? $entities[0] : false; return count($entities) ? $entities[0] : false;
} }
public function previousEntities($entityType) public function previousStateEntities($entityType)
{ {
return $this->state->previous->$entityType; return $this->state->previous->$entityType;
} }
public function entityType() public function stateEntityType()
{ {
return $this->state->current->entityType; return $this->state->current->entityType;
} }
@ -121,7 +122,7 @@ class BaseIntent
return $this->state; return $this->state;
} }
protected function parseClient() protected function requestClient()
{ {
$clientRepo = app('App\Ninja\Repositories\ClientRepository'); $clientRepo = app('App\Ninja\Repositories\ClientRepository');
$client = false; $client = false;
@ -135,7 +136,7 @@ class BaseIntent
return $client; return $client;
} }
protected function parseFields() protected function requestFields()
{ {
$data = []; $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; return $data;
} }

View File

@ -7,19 +7,19 @@ class CreateInvoiceIntent extends InvoiceIntent
{ {
public function process() public function process()
{ {
$client = $this->parseClient(); $client = $this->requestClient();
$invoiceItems = $this->parseInvoiceItems(); $invoiceItems = $this->requestInvoiceItems();
if ( ! $client) { if ( ! $client) {
throw new Exception(trans('texts.client_not_found')); throw new Exception(trans('texts.client_not_found'));
} }
$data = array_merge($this->parseFields(), [ $data = array_merge($this->requestFields(), [
'client_id' => $client->id, 'client_id' => $client->id,
'invoice_items' => $invoiceItems, 'invoice_items' => $invoiceItems,
]); ]);
var_dump($data); //var_dump($data);
$valid = EntityModel::validate($data, ENTITY_INVOICE); $valid = EntityModel::validate($data, ENTITY_INVOICE);
@ -27,16 +27,17 @@ class CreateInvoiceIntent extends InvoiceIntent
throw new Exception($valid); throw new Exception($valid);
} }
$invoice = $this->invoiceRepo->save($data); $invoiceService = app('App\Services\InvoiceService');
$invoice = $invoiceService->save($data);
$invoiceItemIds = array_map(function($item) { $invoiceItemIds = array_map(function($item) {
return $item['public_id']; return $item['public_id'];
}, $invoice->invoice_items->toArray()); }, $invoice->invoice_items->toArray());
$this->setEntityType(ENTITY_INVOICE); $this->setStateEntityType(ENTITY_INVOICE);
$this->setEntities(ENTITY_CLIENT, $client->public_id); $this->setStateEntities(ENTITY_CLIENT, $client->public_id);
$this->setEntities(ENTITY_INVOICE, $invoice->public_id); $this->setStateEntities(ENTITY_INVOICE, $invoice->public_id);
$this->setEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds); $this->setStateEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds);
return $this->createResponse(SKYPE_CARD_RECEIPT, $invoice->present()->skypeBot); return $this->createResponse(SKYPE_CARD_RECEIPT, $invoice->present()->skypeBot);
} }

View File

@ -10,7 +10,7 @@ class EmailInvoiceIntent extends InvoiceIntent
{ {
public function process() public function process()
{ {
$invoice = $this->invoice(); $invoice = $this->stateInvoice();
if ( ! Auth::user()->can('edit', $invoice)) { if ( ! Auth::user()->can('edit', $invoice)) {
throw new Exception(trans('texts.not_allowed')); throw new Exception(trans('texts.not_allowed'));
@ -21,6 +21,12 @@ class EmailInvoiceIntent extends InvoiceIntent
$message = trans('texts.bot_emailed_' . $invoice->getEntityType()); $message = trans('texts.bot_emailed_' . $invoice->getEntityType());
if (Auth::user()->notify_viewed) {
$message .= '<br/>' . trans('texts.bot_emailed_notify_viewed');
} elseif (Auth::user()->notify_paid) {
$message .= '<br/>' . trans('texts.bot_emailed_notify_paid');
}
return SkypeResponse::message($message); return SkypeResponse::message($message);
} }
} }

View File

@ -6,8 +6,10 @@ use App\Models\Invoice;
class InvoiceIntent extends BaseIntent class InvoiceIntent extends BaseIntent
{ {
private $_invoice; protected $fieldMap = [
private $_invoiceItem; 'deposit' => 'partial',
'due' => 'due_date',
];
public function __construct($state, $data) public function __construct($state, $data)
{ {
@ -16,13 +18,9 @@ class InvoiceIntent extends BaseIntent
parent::__construct($state, $data); parent::__construct($state, $data);
} }
protected function invoice() protected function stateInvoice()
{ {
if ($this->_invoice) { $invoiceId = $this->stateEntity(ENTITY_INVOICE);
return $this->_invoice;
}
$invoiceId = $this->entity(ENTITY_INVOICE);
if ( ! $invoiceId) { if ( ! $invoiceId) {
throw new Exception(trans('texts.intent_not_supported')); throw new Exception(trans('texts.intent_not_supported'));
@ -41,20 +39,7 @@ class InvoiceIntent extends BaseIntent
return $invoice; return $invoice;
} }
protected function invoiceItem() protected function requestInvoiceItems()
{
if ($this->_invoiceItem) {
return $this->_invoiceItem;
}
$invoiceItemId = $this->entity(ENTITY_INVOICE_ITEM);
if ( ! $invoiceItemId) {
$invoice = $this->invoice();
}
}
protected function parseInvoiceItems()
{ {
$productRepo = app('App\Ninja\Repositories\ProductRepository'); $productRepo = app('App\Ninja\Repositories\ProductRepository');
@ -76,20 +61,33 @@ class InvoiceIntent extends BaseIntent
} }
} }
$item = $product->toArray(); if ($product) {
$item['qty'] = $qty; $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; return $invoiceItems;
} }
protected function parseFields()
{
$data = parent::parseFields();
return $data;
}
} }

View File

@ -16,8 +16,8 @@ class ListProductsIntent extends ProductIntent
->get() ->get()
->transform(function($item, $key) use ($account) { ->transform(function($item, $key) use ($account) {
$card = $item->present()->skypeBot($account); $card = $item->present()->skypeBot($account);
if ($this->entity(ENTITY_INVOICE)) { if ($this->stateEntity(ENTITY_INVOICE)) {
$card->addButton('imBack', trans('texts.add_to_invoice'), trans('texts.add_to_invoice_command', ['product' => $item->product_key])); $card->addButton('imBack', trans('texts.add_to_invoice'), trans('texts.add_product_to_invoice', ['product' => $item->product_key]));
} }
return $card; return $card;
}); });

View File

@ -8,10 +8,10 @@ class UpdateInvoiceIntent extends InvoiceIntent
{ {
public function process() public function process()
{ {
$invoice = $this->invoice(); $invoice = $this->stateInvoice();
$invoiceItems = $this->parseInvoiceItems(); $invoiceItems = $this->requestInvoiceItems();
$data = array_merge($this->parseFields(), [ $data = array_merge($this->requestFields(), [
'public_id' => $invoice->public_id, 'public_id' => $invoice->public_id,
'invoice_items' => array_merge($invoice->invoice_items->toArray(), $invoiceItems), '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); $valid = EntityModel::validate($data, ENTITY_INVOICE, $invoice);
@ -42,7 +42,7 @@ class UpdateInvoiceIntent extends InvoiceIntent
return $item['public_id']; return $item['public_id'];
}, $invoiceItems); }, $invoiceItems);
$this->setEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds); $this->setStateEntities(ENTITY_INVOICE_ITEM, $invoiceItemIds);
$response = $invoice $response = $invoice
->load('invoice_items') ->load('invoice_items')

View File

@ -109,4 +109,20 @@ class UserMailer extends Mailer
$this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); $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);
}
} }

View File

@ -362,15 +362,15 @@ class StripePaymentDriver extends BasePaymentDriver
$eventDetails = $this->makeStripeCall('GET', 'events/'.$eventId); $eventDetails = $this->makeStripeCall('GET', 'events/'.$eventId);
if (is_string($eventDetails) || !$eventDetails) { if (is_string($eventDetails) || !$eventDetails) {
throw new Exception('Could not get event details'); return false;
} }
if ($eventType != $eventDetails['type']) { if ($eventType != $eventDetails['type']) {
throw new Exception('Event type mismatch'); return false;
} }
if (!$eventDetails['pending_webhooks']) { 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') { 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(); $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first();
if (!$payment) { if (!$payment) {
throw new Exception('Unknown payment'); return false;
} }
if ($eventType == 'charge.failed') { if ($eventType == 'charge.failed') {

View File

@ -138,7 +138,7 @@ class ClientRepository extends BaseRepository
$clientNameMeta = metaphone($clientName); $clientNameMeta = metaphone($clientName);
$map = []; $map = [];
$max = 0; $max = SIMILAR_MIN_THRESHOLD;
$clientId = 0; $clientId = 0;
$clients = Client::scope()->get(['id', 'name', 'public_id']); $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']); $contacts = Contact::scope()->get(['client_id', 'first_name', 'last_name', 'public_id']);
foreach ($contacts as $contact) { foreach ($contacts as $contact) {
if ( ! $contact->getFullName()) { if ( ! $contact->getFullName() || ! isset($map[$contact->client_id])) {
continue; continue;
} }
@ -172,7 +172,7 @@ class ClientRepository extends BaseRepository
} }
} }
return isset($map[$clientId]) ? $map[$clientId] : null; return ($clientId && isset($map[$clientId])) ? $map[$clientId] : null;
} }
} }

View File

@ -271,6 +271,7 @@ class InvoiceRepository extends BaseRepository
$entityType = ENTITY_QUOTE; $entityType = ENTITY_QUOTE;
} }
$invoice = $account->createInvoice($entityType, $data['client_id']); $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)) { if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_tasks = true; $invoice->has_tasks = true;
} }

View File

@ -61,10 +61,12 @@ class ProductRepository extends BaseRepository
$productNameMeta = metaphone($productName); $productNameMeta = metaphone($productName);
$map = []; $map = [];
$max = 0; $max = SIMILAR_MIN_THRESHOLD;
$productId = 0; $productId = 0;
$products = Product::scope()->get(['product_key', 'notes', 'cost']); $products = Product::scope()
->with('default_tax_rate')
->get();
foreach ($products as $product) { foreach ($products as $product) {
if ( ! $product->product_key) { if ( ! $product->product_key) {
@ -80,7 +82,7 @@ class ProductRepository extends BaseRepository
} }
} }
return $map[$productId]; return ($productId && isset($map[$productId])) ? $map[$productId] : null;
} }

View File

@ -0,0 +1,72 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSupportForBots extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('security_codes', function($table)
{
$table->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');
});
}
}

View File

@ -8,7 +8,7 @@
"grunt-contrib-uglify": "~0.2.2", "grunt-contrib-uglify": "~0.2.2",
"grunt-dump-dir": "^0.1.2", "grunt-dump-dir": "^0.1.2",
"gulp": "^3.8.8", "gulp": "^3.8.8",
"laravel-elixir": "*" "laravel-elixir": "^6.0.0"
}, },
"dependencies": { "dependencies": {
"grunt-dump-dir": "^0.1.2" "grunt-dump-dir": "^0.1.2"

View File

@ -2056,10 +2056,23 @@ $LANG = array(
'intent_not_supported' => 'Sorry, I\'m not able to do that.', 'intent_not_supported' => 'Sorry, I\'m not able to do that.',
'client_not_found' => 'We weren\'t able to find the client', 'client_not_found' => 'We weren\'t able to find the client',
'not_allowed' => 'Sorry, you don\'t have the needed permissions', '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' => 'Add to invoice',
'add_to_invoice_command' => 'Add 1 :product', 'add_product_to_invoice' => 'Add 1 :product',
'not_authorized' => 'Your are not authorized', 'not_authorized' => 'Your are not authorized',
'bot_get_email' => 'Hi! (wave)<br/>Thanks for trying the Invoice Ninja Bot.<br/>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.<br/>',
'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:<br/>• Create\update\email an invoice<br/>• List products<br/>For example:<br/><i>invoice bob for 2 tickets, set the due date to next thursday and the discount to 10 percent</i>',
'list_products' => 'List Products',
); );
return $LANG; return $LANG;

View File

@ -0,0 +1,24 @@
@extends('emails.master_user')
@section('body')
<div>
{{ trans('texts.email_salutation', ['name' => $userName]) }}
</div>
&nbsp;
<div>
{{ trans("texts.security_code_email_line1") }}
</div>
&nbsp;
<div>
<center><h2>{{ $code }}</h2></center>
</div>
&nbsp;
<div>
{{ trans("texts.security_code_email_line2") }}
</div>
&nbsp;
<div>
{{ trans('texts.email_signature') }} <br/>
{{ trans('texts.email_from') }}
</div>
@stop

View File

@ -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') !!}