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:
parent
e4929c1008
commit
ce2c71843c
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
*/
|
||||
|
15
app/Models/SecurityCode.php
Normal file
15
app/Models/SecurityCode.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use Eloquent;
|
||||
|
||||
/**
|
||||
* Class DatetimeFormat
|
||||
*/
|
||||
class SecurityCode extends Eloquent
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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')
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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') {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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;
|
||||
|
24
resources/views/emails/security_code_html.blade.php
Normal file
24
resources/views/emails/security_code_html.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
@extends('emails.master_user')
|
||||
|
||||
@section('body')
|
||||
<div>
|
||||
{{ trans('texts.email_salutation', ['name' => $userName]) }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ trans("texts.security_code_email_line1") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<center><h2>{{ $code }}</h2></center>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ trans("texts.security_code_email_line2") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ trans('texts.email_signature') }} <br/>
|
||||
{{ trans('texts.email_from') }}
|
||||
</div>
|
||||
@stop
|
10
resources/views/emails/security_code_text.blade.php
Normal file
10
resources/views/emails/security_code_text.blade.php
Normal 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') !!}
|
Loading…
Reference in New Issue
Block a user