From b165f477638083cb81d01bdf2db2c9c3ecf4698f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 27 Feb 2015 10:10:23 +0200 Subject: [PATCH] Enabled creating invoices through the API --- app/controllers/ClientApiController.php | 8 - app/controllers/InvoiceApiController.php | 174 +++++++++++++++++-- app/controllers/PaymentApiController.php | 4 - app/controllers/QuoteApiController.php | 4 - app/filters.php | 41 +++++ app/handlers/InvoiceEventHandler.php | 2 +- app/libraries/utils.php | 23 +-- app/ninja/mailers/UserMailer.php | 4 +- app/ninja/repositories/InvoiceRepository.php | 51 +++--- app/routes.php | 3 +- 10 files changed, 246 insertions(+), 68 deletions(-) diff --git a/app/controllers/ClientApiController.php b/app/controllers/ClientApiController.php index 2d76dfab108b..1ce750931252 100644 --- a/app/controllers/ClientApiController.php +++ b/app/controllers/ClientApiController.php @@ -20,10 +20,6 @@ class ClientApiController extends Controller public function index() { - if (!Utils::isPro()) { - return Redirect::to('/'); - } - $clients = Client::scope()->with('contacts')->orderBy('created_at', 'desc')->get(); $clients = Utils::remapPublicIds($clients->toArray()); @@ -35,10 +31,6 @@ class ClientApiController extends Controller public function store() { - if (!Utils::isPro()) { - return Redirect::to('/'); - } - $data = Input::all(); $error = $this->clientRepo->getErrors($data); diff --git a/app/controllers/InvoiceApiController.php b/app/controllers/InvoiceApiController.php index bd488fd1ed7e..6e8696bfe87d 100644 --- a/app/controllers/InvoiceApiController.php +++ b/app/controllers/InvoiceApiController.php @@ -1,22 +1,20 @@ invoiceRepo = $invoiceRepo; + $this->mailer = $mailer; } public function index() { - if (!Utils::isPro()) { - return Redirect::to('/'); - } - $invoices = Invoice::scope()->where('invoices.is_quote', '=', false)->orderBy('created_at', 'desc')->get(); $invoices = Utils::remapPublicIds($invoices->toArray()); @@ -26,20 +24,168 @@ class InvoiceApiController extends Controller return Response::make($response, 200, $headers); } - /* public function store() { - if (!Utils::isPro()) { - return Redirect::to('/'); + $data = Input::all(); + $error = null; + + // check if the invoice number is set and unique + if (!isset($data['invoice_number'])) { + $data['invoice_number'] = Auth::user()->account->getNextInvoiceNumber(); + } else { + $invoice = Invoice::scope()->where('invoice_number', '=', $data['invoice_number'])->first(); + if ($invoice) { + $error = trans('validation.unique', ['attribute' => 'texts.invoice_number']); + } } - $data = Input::all(); - $invoice = $this->invoiceRepo->save(false, $data, false); + // check the client id is set and exists + if (!isset($data['client_id'])) { + $error = trans('validation.required', ['attribute' => 'client_id']); + } else { + $client = Client::scope($data['client_id'])->first(); + if (!$client) { + $error = trans('validation.not_in', ['attribute' => 'client_id']); + } + } + + if ($error) { + $response = json_encode($error, JSON_PRETTY_PRINT); + } else { + $data = self::prepareData($data); + $invoice = $this->invoiceRepo->save(false, $data, false); + + $invitation = Invitation::createNew(); + $invitation->invoice_id = $invoice->id; + $invitation->contact_id = $client->contacts[0]->id; + $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); + $invitation->save(); + + // prepare the return data + $invoice->load('invoice_items'); + $invoice = $invoice->toArray(); + $invoice['link'] = $invitation->getLink(); + unset($invoice['account']); + unset($invoice['client']); + $invoice = Utils::remapPublicIds($invoice); + $invoice['client_id'] = $client->public_id; + + $response = json_encode($invoice, JSON_PRETTY_PRINT); + } - $response = json_encode($invoice, JSON_PRETTY_PRINT); $headers = Utils::getApiHeaders(); - - return Response::make($response, 200, $headers); + + return Response::make($response, $error ? 400 : 200, $headers); + } + + private function prepareData($data) + { + $account = Auth::user()->account; + $account->loadLocalizationSettings(); + + // set defaults for optional fields + $fields = [ + 'discount' => 0, + 'is_amount_discount' => false, + 'terms' => $account->invoice_terms, + 'public_notes' => '', + 'po_number' => '', + 'invoice_design_id' => $account->invoice_design_id, + 'invoice_items' => [], + 'custom_value1' => 0, + 'custom_value2' => 0, + 'custom_taxes1' => false, + 'custom_taxes2' => false, + ]; + + if (!isset($data['invoice_date'])) { + $fields['invoice_date_sql'] = date_create()->format('Y-m-d'); + } + if (!isset($data['due_date'])) { + $fields['due_date_sql'] = false; + } + + foreach ($fields as $key => $val) { + if (!isset($data[$key])) { + $data[$key] = $val; + } + } + + // hardcode some fields + $fields = [ + 'is_recurring' => false + ]; + + foreach ($fields as $key => $val) { + $data[$key] = $val; + } + + // initialize the line items + if (isset($data['product_key']) || isset($data['cost']) || isset($data['notes']) || isset($data['qty'])) { + $data['invoice_items'] = [self::prepareItem($data)]; + } else { + foreach ($data['invoice_items'] as $index => $item) { + $data['invoice_items'][$index] = self::prepareItem($item); + } + } + + return $data; + } + + private function prepareItem($item) + { + $fields = [ + 'cost' => 0, + 'product_key' => '', + 'notes' => '', + 'qty' => 1 + ]; + + foreach ($fields as $key => $val) { + if (!isset($item[$key])) { + $item[$key] = $val; + } + } + + // if only the product key is set we'll load the cost and notes + if ($item['product_key'] && (!$item['cost'] || !$item['notes'])) { + $product = Product::findProductByKey($item['product_key']); + if ($product) { + if (!$item['cost']) { + $item['cost'] = $product->cost; + } + if (!$item['notes']) { + $item['notes'] = $product->notes; + } + } + } + + return $item; + } + + public function emailInvoice() + { + $data = Input::all(); + $error = null; + + if (!isset($data['id'])) { + $error = trans('validation.required', ['attribute' => 'id']); + } else { + $invoice = Invoice::scope($data['id'])->first(); + if (!$invoice) { + $error = trans('validation.not_in', ['attribute' => 'id']); + } else { + $this->mailer->sendInvoice($invoice); + } + } + + if ($error) { + $response = json_encode($error, JSON_PRETTY_PRINT); + } else { + $response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT); + } + + $headers = Utils::getApiHeaders(); + return Response::make($response, $error ? 400 : 200, $headers); } - */ } diff --git a/app/controllers/PaymentApiController.php b/app/controllers/PaymentApiController.php index 410069f09c66..46db0d353d2b 100644 --- a/app/controllers/PaymentApiController.php +++ b/app/controllers/PaymentApiController.php @@ -13,10 +13,6 @@ class PaymentApiController extends Controller public function index() { - if (!Utils::isPro()) { - return Redirect::to('/'); - } - $payments = Payment::scope()->orderBy('created_at', 'desc')->get(); $payments = Utils::remapPublicIds($payments->toArray()); diff --git a/app/controllers/QuoteApiController.php b/app/controllers/QuoteApiController.php index 5b442516d088..713f92997cb6 100644 --- a/app/controllers/QuoteApiController.php +++ b/app/controllers/QuoteApiController.php @@ -13,10 +13,6 @@ class QuoteApiController extends Controller public function index() { - if (!Utils::isPro()) { - return Redirect::to('/'); - } - $invoices = Invoice::scope()->where('invoices.is_quote', '=', true)->orderBy('created_at', 'desc')->get(); $invoices = Utils::remapPublicIds($invoices->toArray()); diff --git a/app/filters.php b/app/filters.php index 74ee30ac001c..3792f38a9d11 100755 --- a/app/filters.php +++ b/app/filters.php @@ -173,6 +173,47 @@ Route::filter('auth.basic', function() return Auth::basic(); }); +Route::filter('api.access', function() +{ + $headers = Utils::getApiHeaders(); + + if (!Utils::isPro()) { + return Response::make('API requires pro plan', 403, $headers); + } else { + $accountId = Auth::user()->account->id; + + // http://stackoverflow.com/questions/1375501/how-do-i-throttle-my-sites-api-users + $hour = 60 * 60; + $hour_limit = 100; # users are limited to 100 requests/hour + $hour_throttle = Cache::get("hour_throttle:{$accountId}", null); + $last_api_request = Cache::get("last_api_request:{$accountId}", 0); + $last_api_diff = time() - $last_api_request; + + if (is_null($hour_throttle)) { + $new_hour_throttle = 0; + } else { + $new_hour_throttle = $hour_throttle - $last_api_diff; + $new_hour_throttle = $new_hour_throttle < 0 ? 0 : $new_hour_throttle; + $new_hour_throttle += $hour / $hour_limit; + $hour_hits_remaining = floor(( $hour - $new_hour_throttle ) * $hour_limit / $hour); + $hour_hits_remaining = $hour_hits_remaining >= 0 ? $hour_hits_remaining : 0; + } + + if ($new_hour_throttle > $hour) { + $wait = ceil($new_hour_throttle - $hour); + sleep(1); + return Response::make("Please wait {$wait} second(s)", 403, $headers); + } + + Cache::put("hour_throttle:{$accountId}", $new_hour_throttle, 10); + Cache::put("last_api_request:{$accountId}", time(), 10); + } + + return null; +}); + + + /* |-------------------------------------------------------------------------- | Guest Filter diff --git a/app/handlers/InvoiceEventHandler.php b/app/handlers/InvoiceEventHandler.php index 243d096dcd0b..7b6f4e9cc52b 100755 --- a/app/handlers/InvoiceEventHandler.php +++ b/app/handlers/InvoiceEventHandler.php @@ -44,7 +44,7 @@ class InvoiceEventHandler { if ($user->{'notify_' . $type}) { - $this->userMailer->sendNotification($user, $invoice, $type, $payment); + $this->userMailer->sendNotification($user, $invoice, $type, $payment); } } } diff --git a/app/libraries/utils.php b/app/libraries/utils.php index 89f0427e921f..03d892cd2579 100755 --- a/app/libraries/utils.php +++ b/app/libraries/utils.php @@ -539,23 +539,26 @@ class utils } } - public static function remapPublicIds($data) + + public static function remapPublicIds(array $data) { - foreach ($data as $index => $record) { - if (!isset($data[$index]['public_id'])) { + $return = []; + + foreach ($data as $key => $val) { + if ($key === 'public_id') { + $key = 'id'; + } elseif (strpos($key, '_id')) { continue; } - $data[$index]['id'] = $data[$index]['public_id']; - unset($data[$index]['public_id']); - foreach ($record as $key => $val) { - if (is_array($val)) { - $data[$index][$key] = Utils::remapPublicIds($val); - } + if (is_array($val)) { + $val = Utils::remapPublicIds($val); } + + $return[$key] = $val; } - return $data; + return $return; } public static function getApiHeaders($count = 0) diff --git a/app/ninja/mailers/UserMailer.php b/app/ninja/mailers/UserMailer.php index c8bb82cf8c86..12e7bedd60eb 100755 --- a/app/ninja/mailers/UserMailer.php +++ b/app/ninja/mailers/UserMailer.php @@ -37,7 +37,7 @@ class UserMailer extends Mailer if (!$user->email) { return; } - + $view = 'invoice_'.$notificationType; $entityType = $invoice->getEntityType(); @@ -56,7 +56,7 @@ class UserMailer extends Mailer } $subject = trans("texts.notification_{$entityType}_{$notificationType}_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->getDisplayName()]); - + $this->sendTo($user->email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } } diff --git a/app/ninja/repositories/InvoiceRepository.php b/app/ninja/repositories/InvoiceRepository.php index c964d558a907..cef04eb2dbeb 100755 --- a/app/ninja/repositories/InvoiceRepository.php +++ b/app/ninja/repositories/InvoiceRepository.php @@ -220,13 +220,13 @@ class InvoiceRepository $invoice->is_quote = true; } } - + $invoice->client_id = $data['client_id']; $invoice->discount = round(Utils::parseFloat($data['discount']), 2); $invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; $invoice->invoice_number = trim($data['invoice_number']); $invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false; - $invoice->invoice_date = Utils::toSqlDate($data['invoice_date']); + $invoice->invoice_date = isset($data['invoice_date_sql']) ? $data['invoice_date_sql'] : Utils::toSqlDate($data['invoice_date']); if ($invoice->is_recurring) { $invoice->frequency_id = $data['frequency_id'] ? $data['frequency_id'] : 0; @@ -234,7 +234,7 @@ class InvoiceRepository $invoice->end_date = Utils::toSqlDate($data['end_date']); $invoice->due_date = null; } else { - $invoice->due_date = Utils::toSqlDate($data['due_date']); + $invoice->due_date = isset($data['due_date_sql']) ? $data['due_date_sql'] : Utils::toSqlDate($data['due_date']); $invoice->frequency_id = 0; $invoice->start_date = null; $invoice->end_date = null; @@ -256,16 +256,17 @@ class InvoiceRepository $total = 0; foreach ($data['invoice_items'] as $item) { - if (!$item->cost && !$item->product_key && !$item->notes) { + $item = (array) $item; + if (!$item['cost'] && !$item['product_key'] && !$item['notes']) { continue; } - $invoiceItemCost = Utils::parseFloat($item->cost); - $invoiceItemQty = Utils::parseFloat($item->qty); + $invoiceItemCost = Utils::parseFloat($item['cost']); + $invoiceItemQty = Utils::parseFloat($item['qty']); $invoiceItemTaxRate = 0; - if (isset($item->tax_rate) && Utils::parseFloat($item->tax_rate) > 0) { - $invoiceItemTaxRate = Utils::parseFloat($item->tax_rate); + if (isset($item['tax_rate']) && Utils::parseFloat($item['tax_rate']) > 0) { + $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate']); } $lineTotal = $invoiceItemCost * $invoiceItemQty; @@ -314,25 +315,27 @@ class InvoiceRepository $invoice->amount = $total; $invoice->save(); - $invoice->invoice_items()->forceDelete(); + if ($publicId) { + $invoice->invoice_items()->forceDelete(); + } foreach ($data['invoice_items'] as $item) { - if (!$item->cost && !$item->product_key && !$item->notes) { + $item = (array) $item; + if (!$item['cost'] && !$item['product_key'] && !$item['notes']) { continue; } - if ($item->product_key) { - $product = Product::findProductByKey(trim($item->product_key)); + if ($item['product_key']) { + $product = Product::findProductByKey(trim($item['product_key'])); if (!$product) { $product = Product::createNew(); - $product->product_key = trim($item->product_key); + $product->product_key = trim($item['product_key']); } if (\Auth::user()->account->update_products) { - $product->notes = $item->notes; - $product->cost = $item->cost; - //$product->qty = $item->qty; + $product->notes = $item['notes']; + $product->cost = $item['cost']; } $product->save(); @@ -340,21 +343,21 @@ class InvoiceRepository $invoiceItem = InvoiceItem::createNew(); $invoiceItem->product_id = isset($product) ? $product->id : null; - $invoiceItem->product_key = trim($invoice->is_recurring ? $item->product_key : Utils::processVariables($item->product_key)); - $invoiceItem->notes = trim($invoice->is_recurring ? $item->notes : Utils::processVariables($item->notes)); - $invoiceItem->cost = Utils::parseFloat($item->cost); - $invoiceItem->qty = Utils::parseFloat($item->qty); + $invoiceItem->product_key = trim($invoice->is_recurring ? $item->product_key : Utils::processVariables($item['product_key'])); + $invoiceItem->notes = trim($invoice->is_recurring ? $item['notes'] : Utils::processVariables($item['notes'])); + $invoiceItem->cost = Utils::parseFloat($item['cost']); + $invoiceItem->qty = Utils::parseFloat($item['qty']); $invoiceItem->tax_rate = 0; - if (isset($item->tax_rate) && isset($item->tax_name) && $item->tax_name) { - $invoiceItem->tax_rate = Utils::parseFloat($item->tax_rate); - $invoiceItem->tax_name = trim($item->tax_name); + if (isset($item['tax_rate']) && isset($item['tax_name']) && $item['tax_name']) { + $invoiceItem['tax_rate'] = Utils::parseFloat($item['tax_rate']); + $invoiceItem['tax_name'] = trim($item['tax_name']); } $invoice->invoice_items()->save($invoiceItem); } - if ($data['set_default_terms']) { + if (isset($data['set_default_terms']) && $data['set_default_terms']) { $account = \Auth::user()->account; $account->invoice_terms = $invoice->terms; $account->save(); diff --git a/app/routes.php b/app/routes.php index 1d0cedaa7308..d142ba616b39 100755 --- a/app/routes.php +++ b/app/routes.php @@ -142,7 +142,7 @@ Route::group(array('before' => 'auth'), function() { }); // Route group for API -Route::group(array('prefix' => 'api/v1', 'before' => 'auth.basic'), function() +Route::group(array('prefix' => 'api/v1', 'before' => ['auth.basic', 'api.access']), function() { Route::resource('ping', 'ClientApiController@ping'); Route::resource('clients', 'ClientApiController'); @@ -150,6 +150,7 @@ Route::group(array('prefix' => 'api/v1', 'before' => 'auth.basic'), function() Route::resource('quotes', 'QuoteApiController'); Route::resource('payments', 'PaymentApiController'); Route::post('api/hooks', 'IntegrationController@subscribe'); + Route::post('email_invoice', 'InvoiceApiController@emailInvoice'); }); define('CONTACT_EMAIL', Config::get('mail.from.address'));