1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-17 16:42:48 +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;
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 <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());
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<pre>" . htmlentities(json_encode($data), JSON_PRETTY_PRINT) . "</pre>";
$data = '{ eTag: "*", data: "' . addslashes(json_encode($data)) . '" }';
CurlUtils::post($url, $data, $headers);
}
@ -116,10 +178,81 @@ class BotController extends Controller
'Authorization: Bearer ' . $token,
];
//echo "<pre>" . htmlentities(json_encode(json_decode($message), JSON_PRETTY_PRINT)) . "</pre>";
$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)

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

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_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'],

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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
*/

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 $parameters;
protected $fieldMap = [];
public function __construct($state, $data)
{
//if (true) {
if ( ! $state) {
if ( ! $state || is_string($state)) {
$state = new stdClass;
foreach (['current', 'previous'] as $reference) {
$state->$reference = new stdClass;
@ -54,7 +55,7 @@ class BaseIntent
$intent = str_replace('Entity', $entityType, $intent);
$className = "App\\Ninja\\Intents\\{$intent}Intent";
echo "Intent: $intent<p>";
//echo "Intent: $intent<p>";
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;
}

View File

@ -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);
}

View File

@ -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 .= '<br/>' . trans('texts.bot_emailed_notify_viewed');
} elseif (Auth::user()->notify_paid) {
$message .= '<br/>' . trans('texts.bot_emailed_notify_paid');
}
return SkypeResponse::message($message);
}
}

View File

@ -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;
}
}

View File

@ -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;
});

View File

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

View File

@ -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);
}
}

View File

@ -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') {

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

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-dump-dir": "^0.1.2",
"gulp": "^3.8.8",
"laravel-elixir": "*"
"laravel-elixir": "^6.0.0"
},
"dependencies": {
"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.',
'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)<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;

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