From 2dfc053cbc83de51c764984ac9ec7088362e28a7 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Oct 2015 10:11:44 +0300 Subject: [PATCH 1/4] Bug fixes --- .env.example | 2 +- app/Console/Commands/SendReminders.php | 4 + app/Http/Controllers/AccountController.php | 6 +- app/Http/Controllers/AppController.php | 2 +- app/Http/Controllers/InvoiceApiController.php | 4 + app/Http/Controllers/InvoiceController.php | 274 +++++++++--------- app/Http/Controllers/PaymentController.php | 14 +- app/Libraries/Utils.php | 35 ++- app/Models/Account.php | 59 +++- app/Models/Invitation.php | 14 +- app/Models/Invoice.php | 57 ++-- app/Models/User.php | 5 - app/Ninja/Mailers/ContactMailer.php | 43 +-- app/Ninja/Mailers/Mailer.php | 12 +- app/Ninja/Repositories/InvoiceRepository.php | 28 +- bootstrap/app.php | 6 + composer.lock | 46 +-- config/queue.php | 8 +- public/css/built.css | 5 + public/css/style.css | 5 + public/js/built.js | 8 +- public/js/pdf.pdfmake.js | 8 +- resources/lang/en/texts.php | 4 +- .../views/accounts/invoice_settings.blade.php | 51 +++- resources/views/invoices/edit.blade.php | 14 +- resources/views/invoices/pdf.blade.php | 9 +- resources/views/invoices/view.blade.php | 2 +- .../partials/fb_pixel_checkout.blade.php | 15 - storage/pdfcache/.gitignore | 2 - tests/_support/AcceptanceTester.php | 2 +- tests/_support/FunctionalTester.php | 3 +- .../_generated/FunctionalTesterActions.php | 159 +++++++++- .../_support/_generated/UnitTesterActions.php | 2 +- tests/acceptance/GoProCest.php | 2 +- tests/acceptance/InvoiceCest.php | 5 +- tests/functional.suite.yml | 4 +- tests/functional/SettingsCest.php | 274 +++++++++--------- 37 files changed, 735 insertions(+), 458 deletions(-) delete mode 100644 resources/views/partials/fb_pixel_checkout.blade.php delete mode 100755 storage/pdfcache/.gitignore diff --git a/.env.example b/.env.example index ce0cd9128663..8ae5c8ae153a 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,6 @@ MAIL_FROM_ADDRESS MAIL_FROM_NAME MAIL_PASSWORD -PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address' +#PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address' LOG=single diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php index 123e086feb30..a2bfdc29da8b 100644 --- a/app/Console/Commands/SendReminders.php +++ b/app/Console/Commands/SendReminders.php @@ -36,6 +36,10 @@ class SendReminders extends Command $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { + if (!$account->isPro()) { + continue; + } + $invoices = $this->invoiceRepo->findNeedingReminding($account); $this->info($account->name . ': ' . count($invoices).' invoices found'); diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 1eb38bcbdc8a..26777a54feba 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -373,8 +373,10 @@ class AccountController extends BaseController $rules = []; $user = Auth::user(); $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); - $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); - if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { + $iframeURL = rtrim($iframeURL, "/"); + + $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); + if ($iframeURL || !$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { $subdomain = null; } if ($subdomain) { diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index 5ced7948658e..7d186e29c37b 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -94,7 +94,7 @@ class AppController extends BaseController "MAIL_USERNAME={$mail['username']}\n". "MAIL_FROM_NAME={$mail['from']['name']}\n". "MAIL_PASSWORD={$mail['password']}\n\n". - "PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'"; + "#PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'"; // Write Config Settings $fp = fopen(base_path()."/.env", 'w'); diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 27e0a1de90fa..2dd1eef286f6 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -181,6 +181,10 @@ class InvoiceApiController extends Controller // initialize the line items if (isset($data['product_key']) || isset($data['cost']) || isset($data['notes']) || isset($data['qty'])) { $data['invoice_items'] = [self::prepareItem($data)]; + + // make sure the tax isn't applied twice (for the invoice and the line item) + unset($data['invoice_items'][0]['tax_name']); + unset($data['invoice_items'][0]['tax_rate']); } else { foreach ($data['invoice_items'] as $index => $item) { $data['invoice_items'][$index] = self::prepareItem($item); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 948a21532f0e..282990cedc0a 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -172,42 +172,22 @@ class InvoiceController extends BaseController public function view($invitationKey) { - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); - - if (!$invitation) { - app()->abort(404, trans('texts.invoice_not_found')); - } - + $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey); $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - app()->abort(404, trans('texts.invoice_not_found')); - } - - $invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country'); $client = $invoice->client; - $account = $client->account; + $account = $invoice->account; - if (!$client || $client->is_deleted) { + if (!$account->checkSubdomain(Request::server('HTTP_HOST'))) { app()->abort(404, trans('texts.invoice_not_found')); } - if ($account->subdomain) { - $server = explode('.', Request::server('HTTP_HOST')); - $subdomain = $server[0]; - - if (!in_array($subdomain, ['app', 'www']) && $subdomain != $account->subdomain) { - return View::make('invoices.deleted'); - } - } - if (!Input::has('phantomjs') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { Activity::viewInvoice($invitation); Event::fire(new InvoiceViewed($invoice)); } - Session::set($invitationKey, true); - Session::set('invitation_key', $invitationKey); + Session::set($invitationKey, true); // track this invitation has been seen + Session::set('invitation_key', $invitationKey); // track current invitation $account->loadLocalizationSettings($client); @@ -226,27 +206,16 @@ class InvoiceController extends BaseController 'first_name', 'last_name', 'email', - 'phone', ]); - - // Determine payment options - $paymentTypes = []; - if ($client->getGatewayToken()) { - $paymentTypes[] = [ - 'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file') - ]; - } - foreach(Gateway::$paymentTypes as $type) { - if ($account->getGatewayByType($type)) { - $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); - $paymentTypes[] = [ - 'url' => URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"), 'label' => trans('texts.'.strtolower($type)) - ]; - } - } + 'phone', + ]); + $paymentTypes = $this->getPaymentTypes($client, $invitation); $paymentURL = ''; if (count($paymentTypes)) { $paymentURL = $paymentTypes[0]['url']; + if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { + $paymentURL = URL::to($paymentURL); + } } $showApprove = $invoice->quote_invoice_id ? false : true; @@ -271,6 +240,34 @@ class InvoiceController extends BaseController return View::make('invoices.view', $data); } + private function getPaymentTypes($client, $invitation) + { + $paymentTypes = []; + $account = $client->account; + + if ($client->getGatewayToken()) { + $paymentTypes[] = [ + 'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file') + ]; + } + foreach(Gateway::$paymentTypes as $type) { + if ($account->getGatewayByType($type)) { + $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); + $url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"); + + // PayPal doesn't allow being run in an iframe so we need to open in new tab + if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) { + $url = 'javascript:window.open("'.$url.'", "_blank")'; + } + $paymentTypes[] = [ + 'url' => $url, 'label' => trans('texts.'.strtolower($type)) + ]; + } + } + + return $paymentTypes; + } + public function edit($publicId, $clone = false) { $invoice = Invoice::scope($publicId)->withTrashed()->with('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items')->firstOrFail(); @@ -468,121 +465,138 @@ class InvoiceController extends BaseController { $action = Input::get('action'); $entityType = Input::get('entityType'); + $input = json_decode(Input::get('data')); if (in_array($action, ['archive', 'delete', 'mark', 'restore'])) { return InvoiceController::bulk($entityType); } - $input = json_decode(Input::get('data')); - $invoice = $input->invoice; - - if ($errors = $this->invoiceRepo->getErrors($invoice)) { + if ($errors = $this->invoiceRepo->getErrors($input->invoice)) { Session::flash('error', trans('texts.invoice_error')); return Redirect::to("{$entityType}s/create") ->withInput()->withErrors($errors); } else { - $this->taxRateRepo->save($input->tax_rates); - - $clientData = (array) $invoice->client; - $client = $this->clientRepo->save($invoice->client->public_id, $clientData); - - $invoiceData = (array) $invoice; - $invoiceData['client_id'] = $client->id; - $invoice = $this->invoiceRepo->save($publicId, $invoiceData, $entityType); - - $account = Auth::user()->account; - if ($account->invoice_taxes != $input->invoice_taxes - || $account->invoice_item_taxes != $input->invoice_item_taxes - || $account->invoice_design_id != $input->invoice->invoice_design_id - || $account->show_item_taxes != $input->show_item_taxes) { - $account->invoice_taxes = $input->invoice_taxes; - $account->invoice_item_taxes = $input->invoice_item_taxes; - $account->invoice_design_id = $input->invoice->invoice_design_id; - $account->show_item_taxes = $input->show_item_taxes; - $account->save(); - } - - $client->load('contacts'); - $sendInvoiceIds = []; - - foreach ($client->contacts as $contact) { - if ($contact->send_invoice || count($client->contacts) == 1) { - $sendInvoiceIds[] = $contact->id; - } - } - - foreach ($client->contacts as $contact) { - $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); - - if (in_array($contact->id, $sendInvoiceIds) && !$invitation) { - $invitation = Invitation::createNew(); - $invitation->invoice_id = $invoice->id; - $invitation->contact_id = $contact->id; - $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); - $invitation->save(); - } elseif (!in_array($contact->id, $sendInvoiceIds) && $invitation) { - $invitation->delete(); - } - } - + $invoice = $this->saveInvoice($publicId, $input, $entityType); + $url = "{$entityType}s/".$invoice->public_id.'/edit'; $message = trans($publicId ? "texts.updated_{$entityType}" : "texts.created_{$entityType}"); + + // check if we created a new client with the invoice if ($input->invoice->client->public_id == '-1') { $message = $message.' '.trans('texts.and_created_client'); - $url = URL::to('clients/'.$client->public_id); Utils::trackViewed($client->getDisplayName(), ENTITY_CLIENT, $url); } - if ($invoice->account->pdf_email_attachment && !$invoice->is_recurring) { - $pdfUpload = Input::get('pdfupload'); - if (!empty($pdfUpload) && strpos($pdfUpload, 'data:application/pdf;base64,') === 0) { - $invoice->updateCachedPDF($pdfUpload); - } - } - if ($action == 'clone') { return $this->cloneInvoice($publicId); } elseif ($action == 'convert') { return $this->convertQuote($publicId); } elseif ($action == 'email') { - if (Auth::user()->confirmed && !Auth::user()->isDemo()) { - if ($invoice->is_recurring) { - if ($invoice->shouldSendToday()) { - $invoice = $this->invoiceRepo->createRecurringInvoice($invoice); - // in case auto-bill is enabled - if ($invoice->isPaid()) { - $response = true; - } else { - $response = $this->mailer->sendInvoice($invoice); - } - } else { - $response = trans('texts.recurring_too_soon'); - } - } else { - $response = $this->mailer->sendInvoice($invoice); - } - if ($response === true) { - $message = trans("texts.emailed_{$entityType}"); - Session::flash('message', $message); - } else { - Session::flash('error', $response); - } - } else { - $errorMessage = trans(Auth::user()->registered ? 'texts.confirmation_required' : 'texts.registration_required'); - Session::flash('error', $errorMessage); - Session::flash('message', $message); - } - } else { - Session::flash('message', $message); + return $this->emailInvoice($invoice, Input::get('pdfupload')); } - - $url = "{$entityType}s/".$invoice->public_id.'/edit'; - + + Session::flash('message', $message); return Redirect::to($url); } } + private function emailInvoice($invoice, $pdfUpload) + { + $entityType = $invoice->getEntityType(); + $pdfUpload = Utils::decodePDF($pdfUpload); + + if (!Auth::user()->confirmed) { + $errorMessage = trans(Auth::user()->registered ? 'texts.confirmation_required' : 'texts.registration_required'); + Session::flash('error', $errorMessage); + Session::flash('message', $message); + return Redirect::to($url); + } + + if ($invoice->is_recurring) { + $response = $this->emailRecurringInvoice($invoice); + } else { + $response = $this->mailer->sendInvoice($invoice, false, $pdfUpload); + } + + if ($response === true) { + $message = trans("texts.emailed_{$entityType}"); + Session::flash('message', $message); + } else { + Session::flash('error', $response); + } + + return Redirect::to("{$entityType}s/{$invoice->public_id}/edit"); + } + + private function emailRecurringInvoice(&$invoice) + { + if (!$invoice->shouldSendToday()) { + return trans('texts.recurring_too_soon'); + } + + // switch from the recurring invoice to the generated invoice + $invoice = $this->invoiceRepo->createRecurringInvoice($invoice); + + // in case auto-bill is enabled then a receipt has been sent + if ($invoice->isPaid()) { + return true; + } else { + return $this->mailer->sendInvoice($invoice); + } + } + + private function saveInvoice($publicId, $input, $entityType) + { + $invoice = $input->invoice; + + $this->taxRateRepo->save($input->tax_rates); + + $clientData = (array) $invoice->client; + $client = $this->clientRepo->save($invoice->client->public_id, $clientData); + + $invoiceData = (array) $invoice; + $invoiceData['client_id'] = $client->id; + $invoice = $this->invoiceRepo->save($publicId, $invoiceData, $entityType); + + $account = Auth::user()->account; + if ($account->invoice_taxes != $input->invoice_taxes + || $account->invoice_item_taxes != $input->invoice_item_taxes + || $account->invoice_design_id != $input->invoice->invoice_design_id + || $account->show_item_taxes != $input->show_item_taxes) { + $account->invoice_taxes = $input->invoice_taxes; + $account->invoice_item_taxes = $input->invoice_item_taxes; + $account->invoice_design_id = $input->invoice->invoice_design_id; + $account->show_item_taxes = $input->show_item_taxes; + $account->save(); + } + + $client->load('contacts'); + $sendInvoiceIds = []; + + foreach ($client->contacts as $contact) { + if ($contact->send_invoice || count($client->contacts) == 1) { + $sendInvoiceIds[] = $contact->id; + } + } + + foreach ($client->contacts as $contact) { + $invitation = Invitation::scope()->whereContactId($contact->id)->whereInvoiceId($invoice->id)->first(); + + if (in_array($contact->id, $sendInvoiceIds) && !$invitation) { + $invitation = Invitation::createNew(); + $invitation->invoice_id = $invoice->id; + $invitation->contact_id = $contact->id; + $invitation->invitation_key = str_random(RANDOM_KEY_LENGTH); + $invitation->save(); + } elseif (!in_array($contact->id, $sendInvoiceIds) && $invitation) { + $invitation->delete(); + } + } + + return $invoice; + } + /** * Display the specified resource. * diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 326f0be4ad82..bac2d55382cf 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -549,14 +549,16 @@ class PaymentController extends BaseController $invitation = Invitation::with('invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('transaction_reference', '=', $token)->firstOrFail(); $invoice = $invitation->invoice; + $client = $invoice->client; + $account = $client->account; - $accountGateway = $invoice->client->account->getGatewayByType(Session::get('payment_type')); + $accountGateway = $account->getGatewayByType(Session::get('payment_type')); $gateway = $this->paymentService->createGateway($accountGateway); // Check for Dwolla payment error if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) { $this->error('Dwolla', Input::get('error_description'), $accountGateway); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } try { @@ -569,20 +571,20 @@ class PaymentController extends BaseController $payment = $this->paymentService->createPayment($invitation, $ref, $payerId); Session::flash('message', trans('texts.applied_payment')); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } else { $this->error('offsite', $response->getMessage(), $accountGateway); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } } else { $payment = $this->paymentService->createPayment($invitation, $token, $payerId); Session::flash('message', trans('texts.applied_payment')); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } } catch (\Exception $e) { $this->error('Offsite-uncaught', false, $accountGateway, $e); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to($invitation->getLink()); } } diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 4c50b1059955..c4c3ec32541c 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -94,11 +94,6 @@ class Utils return isset($_ENV[DEMO_ACCOUNT_ID]) ? $_ENV[DEMO_ACCOUNT_ID] : false; } - public static function isDemo() - { - return Auth::check() && Auth::user()->isDemo(); - } - public static function getNewsFeedResponse($userType = false) { if (!$userType) { @@ -634,6 +629,11 @@ class Utils ]; } + public static function isEmpty($value) + { + return !$value || $value == '0.00' || $value == '0,00'; + } + public static function startsWith($haystack, $needle) { return $needle === "" || strpos($haystack, $needle) === 0; @@ -672,7 +672,8 @@ class Utils fwrite($output, "\n"); } - public static function getFirst($values) { + public static function getFirst($values) + { if (is_array($values)) { return count($values) ? $values[0] : false; } else { @@ -681,7 +682,8 @@ class Utils } // nouns in German and French should be uppercase - public static function transFlowText($key) { + public static function transFlowText($key) + { $str = trans("texts.$key"); if (!in_array(App::getLocale(), ['de', 'fr'])) { $str = strtolower($str); @@ -689,7 +691,8 @@ class Utils return $str; } - public static function getSubdomainPlaceholder() { + public static function getSubdomainPlaceholder() + { $parts = parse_url(SITE_URL); $subdomain = ''; if (isset($parts['host'])) { @@ -701,7 +704,8 @@ class Utils return $subdomain; } - public static function getDomainPlaceholder() { + public static function getDomainPlaceholder() + { $parts = parse_url(SITE_URL); $domain = ''; if (isset($parts['host'])) { @@ -719,7 +723,8 @@ class Utils return $domain; } - public static function replaceSubdomain($domain, $subdomain) { + public static function replaceSubdomain($domain, $subdomain) + { $parsedUrl = parse_url($domain); $host = explode('.', $parsedUrl['host']); if (count($host) > 0) { @@ -729,11 +734,17 @@ class Utils return $domain; } - public static function splitName($name) { + public static function splitName($name) + { $name = trim($name); $lastName = (strpos($name, ' ') === false) ? '' : preg_replace('#.*\s([\w-]*)$#', '$1', $name); - $firstName = trim( preg_replace('#'.$lastName.'#', '', $name ) ); + $firstName = trim(preg_replace('#'.$lastName.'#', '', $name)); return array($firstName, $lastName); } + public static function decodePDF($string) + { + $string = str_replace('data:application/pdf;base64,', '', $string); + return base64_decode($string); + } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 5d64c68deefb..cd083e5e4403 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -426,11 +426,13 @@ class Account extends Eloquent public function getEmailSubject($entityType) { - $field = "email_subject_{$entityType}"; - $value = $this->$field; + if ($this->isPro()) { + $field = "email_subject_{$entityType}"; + $value = $this->$field; - if ($value) { - return $value; + if ($value) { + return $value; + } } return $this->getDefaultEmailSubject($entityType); @@ -455,13 +457,15 @@ class Account extends Eloquent public function getEmailTemplate($entityType, $message = false) { - $field = "email_template_{$entityType}"; - $template = $this->$field; + if ($this->isPro()) { + $field = "email_template_{$entityType}"; + $template = $this->$field; - if ($template) { - return $template; + if ($template) { + return $template; + } } - + return $this->getDefaultEmailTemplate($entityType, $message); } @@ -503,6 +507,43 @@ class Account extends Eloquent return $url; } + + public function checkSubdomain($host) + { + if (!$this->subdomain) { + return true; + } + + $server = explode('.', $host); + $subdomain = $server[0]; + + if (!in_array($subdomain, ['app', 'www']) && $subdomain != $this->subdomain) { + return false; + } + + return true; + } + + public function showCustomField($field, $entity) + { + if ($this->isPro()) { + return $this->$field ? true : false; + } + + if (!$entity) { + return false; + } + + // convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1' + $field = str_replace(['invoice_', 'label'], ['', 'value'], $field); + + return Utils::isEmpty($entity->$field) ? false : true; + } + + public function attatchPDF() + { + return $this->isPro() && $this->pdf_email_attachment; + } } Account::updated(function ($account) { diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index b42bcfbaa80b..2d3c58f439e2 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -36,13 +36,15 @@ class Invitation extends EntityModel $url = SITE_URL; $iframe_url = $this->account->iframe_url; - - if ($iframe_url) { - return "{$iframe_url}/?{$this->invitation_key}"; - } elseif ($this->account->subdomain) { - $url = Utils::replaceSubdomain($url, $this->account->subdomain); + + if ($this->account->isPro()) { + if ($iframe_url) { + return "{$iframe_url}/?{$this->invitation_key}"; + } elseif ($this->account->subdomain) { + $url = Utils::replaceSubdomain($url, $this->account->subdomain); + } } - + return "{$url}/view/{$this->invitation_key}"; } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 9233d3da2188..f407cc3a972f 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -285,37 +285,36 @@ class Invoice extends EntityModel return false; } - public function updateCachedPDF($encodedString = false) + public function getPDFString() { - if (!$encodedString && env('PHANTOMJS_CLOUD_KEY')) { - $invitation = $this->invitations[0]; - $link = $invitation->getLink(); - - $curl = curl_init(); - $jsonEncodedData = json_encode([ - 'targetUrl' => "{$link}?phantomjs=true", - 'requestType' => 'raw', - 'delayTime' => 3000, - ]); - - $opts = [ - CURLOPT_URL => PHANTOMJS_CLOUD . env('PHANTOMJS_CLOUD_KEY'), - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => $jsonEncodedData, - CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Content-Length: '.strlen($jsonEncodedData)], - ]; - - curl_setopt_array($curl, $opts); - $encodedString = strip_tags(curl_exec($curl)); - curl_close($curl); - } - - $encodedString = str_replace('data:application/pdf;base64,', '', $encodedString); - if ($encodedString = base64_decode($encodedString)) { - file_put_contents($this->getPDFPath(), $encodedString); + if (!env('PHANTOMJS_CLOUD_KEY')) { + return false; } + + $invitation = $this->invitations[0]; + $link = $invitation->getLink(); + + $curl = curl_init(); + $jsonEncodedData = json_encode([ + 'targetUrl' => "{$link}?phantomjs=true", + 'requestType' => 'raw', + 'delayTime' => 1000, + ]); + + $opts = [ + CURLOPT_URL => PHANTOMJS_CLOUD . env('PHANTOMJS_CLOUD_KEY'), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $jsonEncodedData, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Content-Length: '.strlen($jsonEncodedData)], + ]; + + curl_setopt_array($curl, $opts); + $encodedString = strip_tags(curl_exec($curl)); + curl_close($curl); + + return Utils::decodePDF($encodedString); } } diff --git a/app/Models/User.php b/app/Models/User.php index de750d4e71dc..454a1c3f6900 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -96,11 +96,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isPro(); } - public function isDemo() - { - return $this->account->id == Utils::getDemoAccountId(); - } - public function maxInvoiceDesignId() { return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index e7200609c529..7cb322e01dcd 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -13,7 +13,7 @@ use App\Events\InvoiceSent; class ContactMailer extends Mailer { - public function sendInvoice(Invoice $invoice, $reminder = false) + public function sendInvoice(Invoice $invoice, $reminder = false, $pdfString = false) { $invoice->load('invitations', 'client.language', 'account'); $entityType = $invoice->getEntityType(); @@ -26,18 +26,17 @@ class ContactMailer extends Mailer } $account->loadLocalizationSettings($client); - - if ($account->pdf_email_attachment) { - $invoice->updateCachedPDF(); - } - $emailTemplate = $account->getEmailTemplate($reminder ?: $entityType); $emailSubject = $account->getEmailSubject($reminder ?: $entityType); $sent = false; + if ($account->attatchPDF() && !$pdfString) { + $pdfString = $invoice->getPDFString(); + } + foreach ($invoice->invitations as $invitation) { - if ($this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject)) { + if ($this->sendInvitation($invitation, $invoice, $emailTemplate, $emailSubject, $pdfString)) { $sent = true; } } @@ -51,7 +50,7 @@ class ContactMailer extends Mailer return $sent ?: trans('texts.email_error'); } - private function sendInvitation($invitation, $invoice, $body, $subject) + private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString) { $client = $invoice->client; $account = $invoice->account; @@ -80,11 +79,18 @@ class ContactMailer extends Mailer 'amount' => $invoice->getRequestedAmount() ]; - $data['body'] = $this->processVariables($body, $variables); - $data['link'] = $invitation->getLink(); - $data['entityType'] = $invoice->getEntityType(); - $data['invoiceId'] = $invoice->id; - $data['invitation'] = $invitation; + $data = [ + 'body' => $this->processVariables($body, $variables), + 'link' => $invitation->getLink(), + 'entityType' => $invoice->getEntityType(), + 'invoiceId' => $invoice->id, + 'invitation' => $invitation, + ]; + + if ($account->attatchPDF()) { + $data['pdfString'] = $pdfString; + $data['pdfFileName'] = $invoice->getFileName(); + } $subject = $this->processVariables($subject, $variables); $fromEmail = $user->email; @@ -131,13 +137,16 @@ class ContactMailer extends Mailer $data = [ 'body' => $this->processVariables($emailTemplate, $variables) ]; - $subject = $this->processVariables($emailSubject, $variables); - $data['invoice_id'] = $payment->invoice->id; - if ($invoice->account->pdf_email_attachment) { - $invoice->updateCachedPDF(); + if ($account->attatchPDF()) { + $data['pdfString'] = $invoice->getPDFString(); + $data['pdfFileName'] = $invoice->getFileName(); } + $subject = $this->processVariables($emailSubject, $variables); + $data['invoice_id'] = $payment->invoice->id; + $invoice->updateCachedPDF(); + if ($user->email && $contact->email) { $this->sendTo($contact->email, $user->email, $accountName, $subject, $view, $data); } diff --git a/app/Ninja/Mailers/Mailer.php b/app/Ninja/Mailers/Mailer.php index dfe44015d24a..c258216b33ea 100644 --- a/app/Ninja/Mailers/Mailer.php +++ b/app/Ninja/Mailers/Mailer.php @@ -31,14 +31,8 @@ class Mailer ->subject($subject); // Attach the PDF to the email - if (isset($data['invoiceId'])) { - $invoice = Invoice::with('account')->where('id', '=', $data['invoiceId'])->first(); - if ($invoice->account->pdf_email_attachment && file_exists($invoice->getPDFPath())) { - $message->attach( - $invoice->getPDFPath(), - array('as' => $invoice->getFileName(), 'mime' => 'application/pdf') - ); - } + if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) { + $message->attachData($data['pdfString'], $data['pdfFileName']); } }); @@ -54,7 +48,7 @@ class Mailer $invitation = $data['invitation']; // Track the Postmark message id - if (isset($_ENV['POSTMARK_API_TOKEN'])) { + if (isset($_ENV['POSTMARK_API_TOKEN']) && $response) { $json = $response->json(); $invitation->message_id = $json['MessageID']; } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index ea5d2251240e..69a9b137d929 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -285,7 +285,7 @@ class InvoiceRepository if (!$publicId) { $invoice->client_id = $data['client_id']; - $invoice->is_recurring = $data['is_recurring'] && !Utils::isDemo() ? true : false; + $invoice->is_recurring = $data['is_recurring'] ? true : false; } if ($invoice->is_recurring) { @@ -576,6 +576,28 @@ class InvoiceRepository return count($invoices); } + public function findInvoiceByInvitation($invitationKey) + { + $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); + if (!$invitation) { + app()->abort(404, trans('texts.invoice_not_found')); + } + + $invoice = $invitation->invoice; + if (!$invoice || $invoice->is_deleted) { + app()->abort(404, trans('texts.invoice_not_found')); + } + + $invoice->load('user', 'invoice_items', 'invoice_design', 'account.country', 'client.contacts', 'client.country'); + $client = $invoice->client; + + if (!$client || $client->is_deleted) { + app()->abort(404, trans('texts.invoice_not_found')); + } + + return $invitation; + } + public function findOpenInvoices($clientId) { return Invoice::scope() @@ -666,10 +688,6 @@ class InvoiceRepository } } - if ($recurInvoice->account->pdf_email_attachment) { - $invoice->updateCachedPDF(); - } - return $invoice; } diff --git a/bootstrap/app.php b/bootstrap/app.php index f50a3f720632..354e5dd90538 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -52,4 +52,10 @@ $app->singleton( | */ +/* +if (strstr($_SERVER['HTTP_USER_AGENT'], 'PhantomJS') && Utils::isNinjaDev()) { + $app->loadEnvironmentFrom('.env.testing'); +} +*/ + return $app; diff --git a/composer.lock b/composer.lock index fa625eb6b2cd..7c1534c5c2ea 100644 --- a/composer.lock +++ b/composer.lock @@ -1646,12 +1646,12 @@ "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "44c9a6bb292e50cf8a1e4b5030c7954c2709c089" + "reference": "e6c9cd03d6b2a870e74da03332feeb97d477fc87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/44c9a6bb292e50cf8a1e4b5030c7954c2709c089", - "reference": "44c9a6bb292e50cf8a1e4b5030c7954c2709c089", + "url": "https://api.github.com/repos/Intervention/image/zipball/e6c9cd03d6b2a870e74da03332feeb97d477fc87", + "reference": "e6c9cd03d6b2a870e74da03332feeb97d477fc87", "shasum": "" }, "require": { @@ -1700,7 +1700,7 @@ "thumbnail", "watermark" ], - "time": "2015-08-30 15:37:50" + "time": "2015-10-12 08:42:50" }, { "name": "ircmaxell/password-compat", @@ -2312,12 +2312,12 @@ "source": { "type": "git", "url": "https://github.com/lokielse/omnipay-alipay.git", - "reference": "87622e8549b50773a8db83c93c3ad9a22e618991" + "reference": "cbfbee089e0a84a58c73e9d3794894b81a6a82d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lokielse/omnipay-alipay/zipball/87622e8549b50773a8db83c93c3ad9a22e618991", - "reference": "87622e8549b50773a8db83c93c3ad9a22e618991", + "url": "https://api.github.com/repos/lokielse/omnipay-alipay/zipball/cbfbee089e0a84a58c73e9d3794894b81a6a82d6", + "reference": "cbfbee089e0a84a58c73e9d3794894b81a6a82d6", "shasum": "" }, "require": { @@ -2353,7 +2353,7 @@ "payment", "purchase" ], - "time": "2015-09-15 16:43:43" + "time": "2015-10-07 09:33:48" }, { "name": "maximebf/debugbar", @@ -6199,16 +6199,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "2.2.3", + "version": "2.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef1ca6835468857944d5c3b48fa503d5554cff2f" + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef1ca6835468857944d5c3b48fa503d5554cff2f", - "reference": "ef1ca6835468857944d5c3b48fa503d5554cff2f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", "shasum": "" }, "require": { @@ -6257,7 +6257,7 @@ "testing", "xunit" ], - "time": "2015-09-14 06:51:16" + "time": "2015-10-06 15:47:00" }, { "name": "phpunit/php-file-iterator", @@ -6439,16 +6439,16 @@ }, { "name": "phpunit/phpunit", - "version": "4.8.10", + "version": "4.8.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "463163747474815c5ccd4ae12b5b355ec12158e8" + "reference": "00194eb95989190a73198390ceca081ad3441a7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/463163747474815c5ccd4ae12b5b355ec12158e8", - "reference": "463163747474815c5ccd4ae12b5b355ec12158e8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/00194eb95989190a73198390ceca081ad3441a7f", + "reference": "00194eb95989190a73198390ceca081ad3441a7f", "shasum": "" }, "require": { @@ -6507,7 +6507,7 @@ "testing", "xunit" ], - "time": "2015-10-01 09:14:30" + "time": "2015-10-12 03:36:47" }, { "name": "phpunit/phpunit-mock-objects", @@ -6799,16 +6799,16 @@ }, { "name": "sebastian/global-state", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", - "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", "shasum": "" }, "require": { @@ -6846,7 +6846,7 @@ "keywords": [ "global state" ], - "time": "2014-10-06 09:23:50" + "time": "2015-10-12 03:26:01" }, { "name": "sebastian/recursion-context", diff --git a/config/queue.php b/config/queue.php index 9c39a13644a1..30e8e8b9d10b 100644 --- a/config/queue.php +++ b/config/queue.php @@ -59,10 +59,10 @@ return [ 'iron' => [ 'driver' => 'iron', - 'host' => 'mq-aws-us-east-1.iron.io', - 'token' => 'your-token', - 'project' => 'your-project-id', - 'queue' => 'your-queue-name', + 'host' => env('QUEUE_HOST', 'mq-aws-us-east-1.iron.io'), + 'token' => env('QUEUE_TOKEN'), + 'project' => env('QUEUE_PROJECT'), + 'queue' => env('QUEUE_NAME'), 'encrypt' => true, ], diff --git a/public/css/built.css b/public/css/built.css index 22367aad6388..ca50e4b7f2a6 100644 --- a/public/css/built.css +++ b/public/css/built.css @@ -2464,6 +2464,11 @@ table.dataTable tbody th, table.dataTable tbody td { padding: 10px; } +table.data-table tr { + border-bottom: 1px solid #d0d0d0; + border-top: 1px solid #d0d0d0; +} + .datepicker { padding: 4px !important; margin-top: 1px; diff --git a/public/css/style.css b/public/css/style.css index ddabc40c4928..b9714e2da294 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -114,6 +114,11 @@ table.dataTable tbody th, table.dataTable tbody td { padding: 10px; } +table.data-table tr { + border-bottom: 1px solid #d0d0d0; + border-top: 1px solid #d0d0d0; +} + .datepicker { padding: 4px !important; margin-top: 1px; diff --git a/public/js/built.js b/public/js/built.js index 3b1be8b9d74f..df28ee641dd5 100644 --- a/public/js/built.js +++ b/public/js/built.js @@ -31988,9 +31988,13 @@ NINJA.accountAddress = function(invoice) { {text: account.address2}, {text: cityStatePostal}, {text: account.country ? account.country.name : ''}, - {text: invoice.account.custom_value1 ? invoice.account.custom_label1 + ' ' + invoice.account.custom_value1 : false}, - {text: invoice.account.custom_value2 ? invoice.account.custom_label2 + ' ' + invoice.account.custom_value2 : false} ]; + + if (invoice.is_pro) { + data.push({text: invoice.account.custom_value1 ? invoice.account.custom_label1 + ' ' + invoice.account.custom_value1 : false}); + data.push({text: invoice.account.custom_value2 ? invoice.account.custom_label2 + ' ' + invoice.account.custom_value2 : false}); + } + return NINJA.prepareDataList(data, 'accountAddress'); } diff --git a/public/js/pdf.pdfmake.js b/public/js/pdf.pdfmake.js index fb0588113fc5..2980f26fa97e 100644 --- a/public/js/pdf.pdfmake.js +++ b/public/js/pdf.pdfmake.js @@ -415,9 +415,13 @@ NINJA.accountAddress = function(invoice) { {text: account.address2}, {text: cityStatePostal}, {text: account.country ? account.country.name : ''}, - {text: invoice.account.custom_value1 ? invoice.account.custom_label1 + ' ' + invoice.account.custom_value1 : false}, - {text: invoice.account.custom_value2 ? invoice.account.custom_label2 + ' ' + invoice.account.custom_value2 : false} ]; + + if (invoice.is_pro) { + data.push({text: invoice.account.custom_value1 ? invoice.account.custom_label1 + ' ' + invoice.account.custom_value1 : false}); + data.push({text: invoice.account.custom_value2 ? invoice.account.custom_label2 + ' ' + invoice.account.custom_value2 : false}); + } + return NINJA.prepareDataList(data, 'accountAddress'); } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 84e9e16fc785..30121a92d16a 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -769,7 +769,7 @@ return array( 'iframe_url' => 'Website', 'iframe_url_help1' => 'Copy the following code to a page on your site.', - 'iframe_url_help2' => 'Currently only supported with on-site gateways (ie, Stripe and Authorize.net). You can test the feature by clicking \'View as recipient\' for an invoice.', + 'iframe_url_help2' => 'You can test the feature by clicking \'View as recipient\' for an invoice.', 'auto_bill' => 'Auto Bill', 'military_time' => '24 Hour Time', @@ -814,5 +814,7 @@ return array( 'notification_quote_bounced' => 'We were unable to deliver Quote :invoice to :contact.', 'notification_quote_bounced_subject' => 'Unable to deliver Quote :invoice', + 'custom_invoice_link' => 'Custom Invoice Link', + ); diff --git a/resources/views/accounts/invoice_settings.blade.php b/resources/views/accounts/invoice_settings.blade.php index ea878b361bfc..590373bcef60 100644 --- a/resources/views/accounts/invoice_settings.blade.php +++ b/resources/views/accounts/invoice_settings.blade.php @@ -4,6 +4,9 @@ @parent + +@stop + +@section('content') + +
+ +
+ +
+ @if ($account->address1) + {{ $account->address1 }}
+ @endif + @if ($account->address2) + {{ $account->address2 }}
+ @endif + @if ($account->getCityState()) + {{ $account->getCityState() }}
+ @endif +
+
+ @if ($account->work_phone) + {{ $account->work_phone }}
+ @endif + @if ($account->work_email) + {!! HTML::mailto($account->work_email, $account->work_email) !!}
+ @endif +
+
+ +
+
+
+
+
+ {{ trans('texts.total_invoiced') }} +
+
+ {{ Utils::formatMoney($client->paid_to_date + $client->balance, $client->currency_id ?: $account->currency_id) }} +
+
+
+
+
+
+
+ {{ trans('texts.paid_to_date') }} +
+
+ {{ Utils::formatMoney($client->paid_to_date, $client->currency_id ?: $account->currency_id) }} +
+
+
+
+
+
+
+ {{ trans('texts.open_balance') }} +
+
+ {{ Utils::formatMoney($client->balance, $client->currency_id ?: $account->currency_id) }} +
+
+
+
+ + {!! Datatable::table() + ->addColumn( + trans('texts.date'), + trans('texts.message'), + trans('texts.balance'), + trans('texts.adjustment')) + ->setUrl(route('api.client.activity')) + ->setOptions('bFilter', false) + ->setOptions('aaSorting', [['0', 'desc']]) + ->setOptions('sPaginationType', 'bootstrap') + ->render('datatable') !!} + +
+ +@stop \ No newline at end of file diff --git a/resources/views/public/header.blade.php b/resources/views/public/header.blade.php index 36752c2fd4d5..2375f27301c8 100644 --- a/resources/views/public/header.blade.php +++ b/resources/views/public/header.blade.php @@ -1,123 +1,7 @@ @extends('master') @section('head') - - - - + @stop @section('body') @@ -184,6 +68,9 @@ table.table thead .sorting_desc_disabled:after { content: '' !important }