From 8d345cc1b86c9fb500e5c45ff2d5481551334beb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 Oct 2023 11:44:58 +1100 Subject: [PATCH 01/38] Adjustment for missing props --- app/Http/Requests/ClientPortal/RegisterRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/ClientPortal/RegisterRequest.php b/app/Http/Requests/ClientPortal/RegisterRequest.php index c2c5e8ecfcea..860628340369 100644 --- a/app/Http/Requests/ClientPortal/RegisterRequest.php +++ b/app/Http/Requests/ClientPortal/RegisterRequest.php @@ -40,7 +40,7 @@ class RegisterRequest extends FormRequest $rules = []; foreach ($this->company()->client_registration_fields as $field) { - if ($field['visible']) { + if ($field['visible'] ?? true) { $rules[$field['key']] = $field['required'] ? ['bail','required'] : ['sometimes']; } } From 17d11710a7147db67b3623386365398752b4bbcf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Oct 2023 05:49:14 +1100 Subject: [PATCH 02/38] Minor fixes --- .../Invoices/ProcessInvoicesInBulkRequest.php | 11 ++++++ .../Requests/Payment/StorePaymentRequest.php | 2 +- app/Utils/HtmlEngine.php | 10 ++--- app/Utils/Traits/MakesHash.php | 22 ++++++++++- app/Utils/VendorHtmlEngine.php | 39 +++++++++++-------- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php b/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php index 1ee0490b8fb6..73724da000c3 100644 --- a/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php +++ b/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php @@ -28,4 +28,15 @@ class ProcessInvoicesInBulkRequest extends FormRequest 'invoices' => ['array'], ]; } + + public function prepareForValidation() + { + $input = $this->all(); + + if(isset($input['invoices'])){ + $input['invoices'] = array_unique($input['invoices']); + } + + $this->replace($input); + } } diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index e51eeb50bc57..b110abcf38ec 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -51,7 +51,7 @@ class StorePaymentRequest extends Request $credits_total = 0; if (isset($input['client_id']) && is_string($input['client_id'])) { - $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + $input['client_id'] = $this->decodePrimaryKey($input['client_id'], true); } if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index f96d37be5df8..538b40b1d311 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -1014,17 +1014,17 @@ html { */ protected function generateEntityImagesMarkup() { - // if (!$this->client->getSetting('embed_documents') && !$this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { - // return ''; - // } + if (!$this->client->getSetting('embed_documents') || !$this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { + return ''; + } $dom = new \DOMDocument('1.0', 'UTF-8'); $container = $dom->createElement('div'); $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);justify-items: center;'); - foreach ($this->entity->documents as $document) { - if (!$document->isImage()) { + foreach ($this->entity->documents()->where('is_public',true)->get() as $document) { + if (!$document->isImage()) { continue; } diff --git a/app/Utils/Traits/MakesHash.php b/app/Utils/Traits/MakesHash.php index 63c151b93530..dc8d6d158a40 100644 --- a/app/Utils/Traits/MakesHash.php +++ b/app/Utils/Traits/MakesHash.php @@ -62,8 +62,27 @@ trait MakesHash return $hashids->encode($value); } - public function decodePrimaryKey($value) + public function decodePrimaryKey($value, $return_string_failure = false) { + + try { + $hashids = new Hashids(config('ninja.hash_salt'), 10); + + $decoded_array = $hashids->decode($value); + + if(isset($decoded_array[0]) ?? false) { + return $decoded_array[0]; + } elseif($return_string_failure) { + return "Invalid Primary Key"; + } else { + throw new \Exception('Invalid Primary Key'); + } + + } catch (\Exception $e) { + return response()->json(['error'=>'Invalid primary key'], 400); + } + + /* try { $hashids = new Hashids(config('ninja.hash_salt'), 10); @@ -77,6 +96,7 @@ trait MakesHash } catch (\Exception $e) { return response()->json(['error'=>'Invalid primary key'], 400); } + */ } public function transformKeys($keys) diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index eb1f595584e5..7bd05d75924c 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -12,18 +12,19 @@ namespace App\Utils; -use App\Models\Country; -use App\Models\CreditInvitation; -use App\Models\InvoiceInvitation; -use App\Models\PurchaseOrderInvitation; -use App\Models\QuoteInvitation; -use App\Models\RecurringInvoiceInvitation; -use App\Utils\Traits\AppSetup; -use App\Utils\Traits\DesignCalculator; -use App\Utils\Traits\MakesDates; use Exception; +use App\Models\Account; +use App\Models\Country; +use App\Utils\Traits\AppSetup; +use App\Models\QuoteInvitation; +use App\Models\CreditInvitation; +use App\Utils\Traits\MakesDates; +use App\Models\InvoiceInvitation; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; +use App\Utils\Traits\DesignCalculator; +use App\Models\PurchaseOrderInvitation; +use App\Models\RecurringInvoiceInvitation; /** * Note the premise used here is that any currencies will be formatted back to the company currency and not @@ -775,31 +776,37 @@ html { */ protected function generateEntityImagesMarkup() { - if ($this->company->getSetting('embed_documents') === false) { + + if (!$this->vendor->getSetting('embed_documents') || !$this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { return ''; } $dom = new \DOMDocument('1.0', 'UTF-8'); $container = $dom->createElement('div'); - $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr);'); - - foreach ($this->entity->documents as $document) { + $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);justify-items: center;'); + + foreach ($this->entity->documents()->where('is_public',true)->get() as $document) { if (!$document->isImage()) { continue; } $image = $dom->createElement('img'); - $image->setAttribute('src', $document->generateUrl()); - $image->setAttribute('style', 'max-height: 100px; margin-top: 20px;'); + $image->setAttribute('src', "data:image/png;base64,".base64_encode($document->getFile())); + $image->setAttribute('style', 'max-width: 50%; margin-top: 20px;'); $container->appendChild($image); } $dom->appendChild($container); - return $dom->saveHTML(); + $html = $dom->saveHTML(); + + $dom = null; + + return $html; + } /** From 14144f72e4da70b05392b8f22ef311ad2e55db61 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Oct 2023 16:57:17 +1100 Subject: [PATCH 03/38] Updated translations --- lang/en/texts.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lang/en/texts.php b/lang/en/texts.php index 49eb412a6202..5fe2741906a1 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -3679,9 +3679,9 @@ $LANG = array( 'send_date' => 'Send Date', 'auto_bill_on' => 'Auto Bill On', 'minimum_under_payment_amount' => 'Minimum Under Payment Amount', - 'allow_over_payment' => 'Allow Over Payment', + 'allow_over_payment' => 'Allow Overpayment', 'allow_over_payment_help' => 'Support paying extra to accept tips', - 'allow_under_payment' => 'Allow Under Payment', + 'allow_under_payment' => 'Allow Underpayment', 'allow_under_payment_help' => 'Support paying at minimum the partial/deposit amount', 'test_mode' => 'Test Mode', 'calculated_rate' => 'Calculated Rate', @@ -3978,8 +3978,8 @@ $LANG = array( 'account_balance' => 'Account Balance', 'thanks' => 'Thanks', 'minimum_required_payment' => 'Minimum required payment is :amount', - 'under_payments_disabled' => 'Company doesn\'t support under payments.', - 'over_payments_disabled' => 'Company doesn\'t support over payments.', + 'under_payments_disabled' => 'Company doesn\'t support underpayments.', + 'over_payments_disabled' => 'Company doesn\'t support overpayments.', 'saved_at' => 'Saved at :time', 'credit_payment' => 'Credit applied to Invoice :invoice_number', 'credit_subject' => 'New credit :number from :account', @@ -4654,8 +4654,8 @@ $LANG = array( 'search_purchase_order' => 'Search Purchase Order', 'search_purchase_orders' => 'Search Purchase Orders', 'login_url' => 'Login URL', - 'enable_applying_payments' => 'Enable Applying Payments', - 'enable_applying_payments_help' => 'Support separately creating and applying payments', + 'enable_applying_payments' => 'Manual Overpayments', + 'enable_applying_payments_help' => 'Support adding an overpayment amount manually on a payment', 'stock_quantity' => 'Stock Quantity', 'notification_threshold' => 'Notification Threshold', 'track_inventory' => 'Track Inventory', From 9913d4c0b260d4d5a33ea92e7502181620ba1ffa Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Oct 2023 17:15:38 +1100 Subject: [PATCH 04/38] Minor fixes for tax rules --- app/DataMapper/Tax/AU/Rule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/DataMapper/Tax/AU/Rule.php b/app/DataMapper/Tax/AU/Rule.php index d6536e1a7177..c1d9e7ed218e 100644 --- a/app/DataMapper/Tax/AU/Rule.php +++ b/app/DataMapper/Tax/AU/Rule.php @@ -62,7 +62,7 @@ class Rule extends BaseRule implements RuleInterface public function taxByType($item): self { - if ($this->client->is_tax_exempt) { + if ($this->client->is_tax_exempt || !property_exists($item, 'tax_id')) { return $this->taxExempt($item); } From 14033459891de54ec545dee807495d3154944904 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Oct 2023 17:16:27 +1100 Subject: [PATCH 05/38] Fixes for null or missing tax_id props --- app/DataMapper/Tax/BaseRule.php | 2 +- app/DataMapper/Tax/DE/Rule.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 36635429a6d7..ba8169734547 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -297,7 +297,7 @@ class BaseRule implements RuleInterface public function tax($item = null): self { - if ($this->client->is_tax_exempt) { + if ($this->client->is_tax_exempt || !property_exists($item, 'tax_id')) { return $this->taxExempt($item); diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index c82f701496f4..8214188c369f 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -63,7 +63,7 @@ class Rule extends BaseRule implements RuleInterface public function taxByType($item): self { - if ($this->client->is_tax_exempt) { + if ($this->client->is_tax_exempt || !property_exists($item, 'tax_id')) { return $this->taxExempt($item); } From 36f5b2e11ec80562b4d8b68637fbfa7901c0fd98 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Oct 2023 17:21:56 +1100 Subject: [PATCH 06/38] Minor fixes for paytrace --- app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php | 2 +- app/PaymentDrivers/PayTrace/CreditCard.php | 2 +- app/PaymentDrivers/PaytracePaymentDriver.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php b/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php index 19ff1974c64d..ceb1417c7530 100644 --- a/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php +++ b/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php @@ -140,7 +140,7 @@ class AccountTransformer implements AccountTransformerInterface 'id' => $account->id, 'account_type' => $account->CONTAINER, // 'account_name' => $account->accountName, - 'account_name' => property_exists($account, 'accountName') ? $account->accountName : $account->nickname, + 'account_name' => property_exists($account, 'accountName') ? $account->accountName : ($account->nickname ?? 'Unknown Account'), 'account_status' => $account_status, 'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '', 'provider_account_id' => $account->providerAccountId, diff --git a/app/PaymentDrivers/PayTrace/CreditCard.php b/app/PaymentDrivers/PayTrace/CreditCard.php index 720d5f0b02be..350fc6948b87 100644 --- a/app/PaymentDrivers/PayTrace/CreditCard.php +++ b/app/PaymentDrivers/PayTrace/CreditCard.php @@ -183,7 +183,7 @@ class CreditCard $response = $this->paytrace->gatewayRequest('/v1/transactions/sale/by_customer', $data); - if ($response->success) { + if ($response->success ?? false) { $this->paytrace->logSuccessfulGatewayResponse(['response' => $response, 'data' => $this->paytrace->payment_hash], SystemLog::TYPE_PAYTRACE); return $this->processSuccessfulPayment($response); diff --git a/app/PaymentDrivers/PaytracePaymentDriver.php b/app/PaymentDrivers/PaytracePaymentDriver.php index 5f33ea843e3a..b55e8857c169 100644 --- a/app/PaymentDrivers/PaytracePaymentDriver.php +++ b/app/PaymentDrivers/PaytracePaymentDriver.php @@ -198,7 +198,7 @@ class PaytracePaymentDriver extends BaseDriver $auth_data = json_decode($response); - if (! property_exists($auth_data, 'access_token')) { + if (!isset($auth_data) || ! property_exists($auth_data, 'access_token')) { throw new SystemError('Error authenticating with PayTrace'); } From 3ac6f4c2a76ca37041528cfc40d70531fcdb9acf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Oct 2023 17:27:00 +1100 Subject: [PATCH 07/38] Catch missing props --- app/PaymentDrivers/Eway/CreditCard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/Eway/CreditCard.php b/app/PaymentDrivers/Eway/CreditCard.php index 11dbafdcfe2b..d4203d0eab9f 100644 --- a/app/PaymentDrivers/Eway/CreditCard.php +++ b/app/PaymentDrivers/Eway/CreditCard.php @@ -251,7 +251,7 @@ class CreditCard $response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction); - if ($response->TransactionStatus) { + if ($response->TransactionStatus ?? false) { $this->logResponse($response, true); $payment = $this->storePayment($response); } else { From bf935896073f1a5b7b7927c030b653a452929dca Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 16 Oct 2023 06:27:37 +1100 Subject: [PATCH 08/38] Only add shipping address to einvoice if country is set. --- app/Services/Invoice/EInvoice/ZugferdEInvoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php index 6b7dbe884350..539e78f6e270 100644 --- a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php +++ b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php @@ -75,7 +75,7 @@ class ZugferdEInvoice extends AbstractService } else { $this->xrechnung->setDocumentBuyerReference($client->routing_id); } - if (!empty($client->shipping_address1)){ + if (!empty($client->shipping_address1) && $client->shipping_country->exists()){ $this->xrechnung->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state); } From 55f71a27c7df25b10fa8e075d5521acdfd6f90b4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 16 Oct 2023 07:25:47 +1100 Subject: [PATCH 09/38] Fixes for square autobill --- app/PaymentDrivers/Square/SquareWebhook.php | 10 ++++++++-- app/Services/Invoice/AutoBillInvoice.php | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/PaymentDrivers/Square/SquareWebhook.php b/app/PaymentDrivers/Square/SquareWebhook.php index c83b73fd4765..c638e2ad3bb7 100644 --- a/app/PaymentDrivers/Square/SquareWebhook.php +++ b/app/PaymentDrivers/Square/SquareWebhook.php @@ -134,8 +134,14 @@ class SquareWebhook implements ShouldQueue nlog("Searching by payment hash"); - $payment_hash_id = $apiResponse->getPayment()->getReferenceId() ?? false; - $square_payment = $apiResponse->getPayment()->jsonSerialize(); + $body = json_decode($apiResponse->getBody()); + + $payment_hash_id = $body->payment->reference_id ?? false; + $square_payment = $body->payment ?? false; + + if(!$payment_hash_id) + return; + $payment_hash = PaymentHash::query()->where('hash', $payment_hash_id)->firstOrFail(); $payment_hash->data = array_merge((array) $payment_hash->data, (array)$square_payment); diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 0585f888bd66..19a366fdc507 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -120,7 +120,7 @@ class AutoBillInvoice extends AbstractService /* Build payment hash */ $payment_hash = PaymentHash::create([ - 'hash' => Str::random(64), + 'hash' => Str::random(32), 'data' => [ 'amount_with_fee' => $amount + $fee, 'invoices' => [ From 0694378bb5e83ffacab896751c1c8e53fafad6a8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 16 Oct 2023 07:28:17 +1100 Subject: [PATCH 10/38] Fixes for square autobill --- app/PaymentDrivers/Square/SquareWebhook.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/PaymentDrivers/Square/SquareWebhook.php b/app/PaymentDrivers/Square/SquareWebhook.php index c638e2ad3bb7..67d656e8c926 100644 --- a/app/PaymentDrivers/Square/SquareWebhook.php +++ b/app/PaymentDrivers/Square/SquareWebhook.php @@ -134,14 +134,8 @@ class SquareWebhook implements ShouldQueue nlog("Searching by payment hash"); - $body = json_decode($apiResponse->getBody()); - - $payment_hash_id = $body->payment->reference_id ?? false; - $square_payment = $body->payment ?? false; - - if(!$payment_hash_id) - return; - + $payment_hash_id = $apiResponse->getResult()->getPayment()->getReferenceId() ?? false; + $square_payment = $apiResponse->getResult()->getPayment()->jsonSerialize(); $payment_hash = PaymentHash::query()->where('hash', $payment_hash_id)->firstOrFail(); $payment_hash->data = array_merge((array) $payment_hash->data, (array)$square_payment); From fb3c0120ec7ff82c65ee6bf2bcd74098e84725a3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Oct 2023 13:39:36 +1100 Subject: [PATCH 11/38] Fixes for Email histoyr --- .../Controllers/EmailHistoryController.php | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/EmailHistoryController.php b/app/Http/Controllers/EmailHistoryController.php index 4da69ba7b6dc..aea6a5e2008f 100644 --- a/app/Http/Controllers/EmailHistoryController.php +++ b/app/Http/Controllers/EmailHistoryController.php @@ -31,11 +31,11 @@ class EmailHistoryController extends BaseController ->where('category_id', SystemLog::CATEGORY_MAIL) ->orderBy('id', 'DESC') ->cursor() - ->map(function ($system_log) { - if(($system_log->log['history'] && $system_log->log['history']['events'] && count($system_log->log['history']['events']) >=1) ?? false) { - return $system_log->log['history']; - } - }); + ->filter(function ($system_log) { + return ($system_log->log['history'] && isset($system_log->log['history']['events']) && count($system_log->log['history']['events']) >=1) !== false; + })->map(function ($system_log) { + return $system_log->log['history']; + })->values()->all(); return response()->json($data, 200); @@ -51,16 +51,17 @@ class EmailHistoryController extends BaseController /** @var \App\Models\User $user */ $user = auth()->user(); + $data = SystemLog::where('company_id', $user->company()->id) - ->where('category_id', SystemLog::CATEGORY_MAIL) - ->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id)) - ->orderBy('id', 'DESC') - ->cursor() - ->map(function ($system_log) { - if(($system_log->log['history'] && $system_log->log['history']['events'] && count($system_log->log['history']['events']) >=1) ?? false) { - return $system_log->log['history']; - } - }); + ->where('category_id', SystemLog::CATEGORY_MAIL) + ->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id)) + ->orderBy('id', 'DESC') + ->cursor() + ->filter(function ($system_log) { + return ($system_log->log['history'] && isset($system_log->log['history']['events']) && count($system_log->log['history']['events']) >=1) !== false; + })->map(function ($system_log) { + return $system_log->log['history']; + })->values()->all(); return response()->json($data, 200); From e4fc9e2cc897116a699327cb132b5a949b23a03d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Oct 2023 21:28:18 +1100 Subject: [PATCH 12/38] Refactor for live previews --- app/Http/Controllers/PreviewController.php | 180 ++++++++++++++---- .../ProtectedDownloadController.php | 2 +- .../Preview/PreviewInvoiceRequest.php | 126 +++++++++++- app/Jobs/Util/PreviewPdf.php | 13 +- tests/Feature/LiveDesignTest.php | 26 +++ 5 files changed, 297 insertions(+), 50 deletions(-) diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 0f94c8e9ac1a..6777af3f941d 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -11,42 +11,43 @@ namespace App\Http\Controllers; -use App\DataMapper\Analytics\LivePreview; -use App\Factory\CreditFactory; -use App\Factory\InvoiceFactory; -use App\Factory\QuoteFactory; -use App\Factory\RecurringInvoiceFactory; -use App\Http\Requests\Preview\DesignPreviewRequest; -use App\Http\Requests\Preview\PreviewInvoiceRequest; -use App\Jobs\Util\PreviewPdf; -use App\Libraries\MultiDB; +use App\Utils\Ninja; +use App\Models\Quote; use App\Models\Client; -use App\Models\ClientContact; use App\Models\Credit; use App\Models\Invoice; -use App\Models\InvoiceInvitation; -use App\Models\Quote; -use App\Models\RecurringInvoice; -use App\Repositories\CreditRepository; -use App\Repositories\InvoiceRepository; -use App\Repositories\QuoteRepository; -use App\Repositories\RecurringInvoiceRepository; +use App\Utils\HtmlEngine; +use App\Libraries\MultiDB; +use App\Factory\QuoteFactory; +use App\Jobs\Util\PreviewPdf; +use App\Models\ClientContact; use App\Services\Pdf\PdfMock; +use App\Factory\CreditFactory; +use App\Factory\InvoiceFactory; +use App\Utils\Traits\MakesHash; +use App\Models\RecurringInvoice; +use App\Utils\PhantomJS\Phantom; +use App\Models\InvoiceInvitation; use App\Services\PdfMaker\Design; +use App\Utils\HostedPDF\NinjaPdf; +use Illuminate\Support\Facades\DB; +use App\Services\PdfMaker\PdfMaker; +use Illuminate\Support\Facades\App; +use App\Repositories\QuoteRepository; +use Illuminate\Support\Facades\Cache; +use App\Repositories\CreditRepository; +use App\Utils\Traits\MakesInvoiceHtml; +use Turbo124\Beacon\Facades\LightLogs; +use App\Repositories\InvoiceRepository; +use App\Utils\Traits\Pdf\PageNumbering; +use App\Factory\RecurringInvoiceFactory; +use Illuminate\Support\Facades\Response; +use App\DataMapper\Analytics\LivePreview; +use App\Repositories\RecurringInvoiceRepository; +use App\Http\Requests\Preview\DesignPreviewRequest; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; -use App\Services\PdfMaker\PdfMaker; -use App\Utils\HostedPDF\NinjaPdf; -use App\Utils\HtmlEngine; -use App\Utils\Ninja; -use App\Utils\PhantomJS\Phantom; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\MakesInvoiceHtml; -use App\Utils\Traits\Pdf\PageNumbering; -use Illuminate\Support\Facades\App; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Response; -use Turbo124\Beacon\Facades\LightLogs; +use App\Http\Requests\Preview\PreviewInvoiceRequest; class PreviewController extends BaseController { @@ -56,7 +57,108 @@ class PreviewController extends BaseController public function __construct() { - parent::__construct(); + parent::__construct(); + } + + private function purgeCache() + { nlog(auth()->user()->id); + Cache::pull("preview_".auth()->user()->id); + } + + public function newLivePreview(PreviewInvoiceRequest $request) + { + + $time = time(); + nlog($time); + + $invitation = $request->resolveInvitation(); + + App::forgetInstance('translator'); + $t = app('translator'); + App::setLocale($invitation->contact->preferredLocale()); + $t->replace(Ninja::transformTranslations($invitation->{$request->entity}->client->getMergedSettings())); + + $html = new HtmlEngine($invitation); + $variables = $html->generateLabelsAndValues(); + + $entity_obj = $invitation->{$request->entity}; + + // if (! $invitation->{$request->entity}->id ?? true) { + // $invitation->{$request->entity}->service()->fillDefaults(); + // } + + $design = \App\Models\Design::withTrashed()->find($entity_obj->design_id ?? 2); + + /* Catch all in case migration doesn't pass back a valid design */ + if (! $design) { + $design = \App\Models\Design::find(2); + } + + if ($design->is_custom) { + $options = [ + 'custom_partials' => json_decode(json_encode($design->design), true), + ]; + $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); + } else { + $template = new PdfMakerDesign(strtolower($design->name)); + } + + $state = [ + 'template' => $template->elements([ + 'client' => $entity_obj->client, + 'entity' => $entity_obj, + 'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables, + '$product' => $design->design->product, + 'variables' => $variables, + ]), + 'variables' => $variables, + 'options' => [ + 'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'), + 'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'), + ], + 'process_markdown' => $entity_obj->client->company->markdown_enabled, + ]; + + $maker = new PdfMaker($state); + + $maker + ->design($template) + ->build(); + + if (request()->query('html') == 'true') { + return $maker->getCompiledHTML(); + } + + //if phantom js...... inject here.. + if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); + } + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $company = $user->company(); + + if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { + + $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); + + $numbered_pdf = $this->pageNumbering($pdf, $company); + + if ($numbered_pdf) { + $pdf = $numbered_pdf; + } + + return $pdf; + } + + $pdf = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + + nlog("merpy derp {$time}"); + + return response()->streamDownload(function () use ($pdf) { + echo $pdf; + }, 'preview.pdf', ['Content-Type' => 'application/pdf','Cache-Control:' => 'no-cache']); + } /** @@ -173,10 +275,18 @@ class PreviewController extends BaseController public function live(PreviewInvoiceRequest $request) { + + // if(Cache::has("preview_".auth()->user()->id)) + // return response()->json(['message' => 'Please wait a few seconds before trying again, this many requests are not good.'], 400); + + nlog("should not see a lot of these"); + if (Ninja::isHosted() && !in_array($request->getHost(), ['preview.invoicing.co','staging.invoicing.co'])) { return response()->json(['message' => 'This server cannot handle this request.'], 400); } + Cache::put("preview_".auth()->user()->id, 60); + $start = microtime(true); /** @var \App\Models\User $user */ @@ -287,9 +397,13 @@ class PreviewController extends BaseController DB::connection(config('database.default'))->rollBack(); if (request()->query('html') == 'true') { + $this->purgeCache(); return $maker->getCompiledHTML(); } + } catch(\Exception $e) { + + $this->purgeCache(); DB::connection(config('database.default'))->rollBack(); @@ -302,6 +416,7 @@ class PreviewController extends BaseController //if phantom js...... inject here.. if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + $this->purgeCache(); return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); } @@ -319,7 +434,7 @@ class PreviewController extends BaseController if ($numbered_pdf) { $pdf = $numbered_pdf; } - + $this->purgeCache(); return $pdf; } @@ -335,7 +450,8 @@ class PreviewController extends BaseController $response->header('Content-Type', 'application/pdf'); $response->header('Server-Timing', microtime(true)-$start); - + nlog("returning a response"); + $this->purgeCache(); return $response; } diff --git a/app/Http/Controllers/ProtectedDownloadController.php b/app/Http/Controllers/ProtectedDownloadController.php index b8f598a11021..014527bace14 100644 --- a/app/Http/Controllers/ProtectedDownloadController.php +++ b/app/Http/Controllers/ProtectedDownloadController.php @@ -23,7 +23,7 @@ class ProtectedDownloadController extends BaseController public function index(Request $request) { - + /** @var string $hashed_path */ $hashed_path = Cache::pull($request->hash); if (!$hashed_path) { diff --git a/app/Http/Requests/Preview/PreviewInvoiceRequest.php b/app/Http/Requests/Preview/PreviewInvoiceRequest.php index bfa4db546470..50cc0a7fdf73 100644 --- a/app/Http/Requests/Preview/PreviewInvoiceRequest.php +++ b/app/Http/Requests/Preview/PreviewInvoiceRequest.php @@ -11,15 +11,29 @@ namespace App\Http\Requests\Preview; +use App\Models\Quote; +use App\Models\Client; +use App\Models\Credit; +use App\Models\Invoice; +use App\Libraries\MultiDB; use App\Http\Requests\Request; -use App\Utils\Traits\CleanLineItems; +use App\Models\CompanyGateway; +use App\Models\QuoteInvitation; use App\Utils\Traits\MakesHash; +use Illuminate\Validation\Rule; +use App\Models\CreditInvitation; +use App\Models\RecurringInvoice; +use App\Models\InvoiceInvitation; +use App\Utils\Traits\CleanLineItems; +use App\Models\RecurringInvoiceInvitation; class PreviewInvoiceRequest extends Request { use MakesHash; use CleanLineItems; + private string $entity_plural = ''; + /** * Determine if the user is authorized to make this request. * @@ -27,20 +41,32 @@ class PreviewInvoiceRequest extends Request */ public function authorize() : bool { - return auth()->user()->hasIntersectPermissionsOrAdmin(['view_invoice', 'view_quote', 'view_recurring_invoice', 'view_credit', 'create_invoice', 'create_quote', 'create_recurring_invoice', 'create_credit','edit_invoice', 'edit_quote', 'edit_recurring_invoice', 'edit_credit']); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->hasIntersectPermissionsOrAdmin(['view_invoice', 'view_quote', 'view_recurring_invoice', 'view_credit', 'create_invoice', 'create_quote', 'create_recurring_invoice', 'create_credit','edit_invoice', 'edit_quote', 'edit_recurring_invoice', 'edit_credit']); } public function rules() { - $rules = []; + /** @var \App\Models\User $user */ + $user = auth()->user(); - $rules['number'] = ['nullable']; + return [ + 'number' => 'nullable', + 'entity' => 'bail|sometimes|in:invoice,quote,credit,recurring_invoice', + 'entity_id' => ['bail','sometimes','integer',Rule::exists($this->entity_plural, 'id')->where('is_deleted',0)->where('company_id', $user->company()->id)], + 'client_id' => ['required', Rule::exists(Client::class, 'id')->where('is_deleted', 0)->where('company_id', $user->company()->id)], + ]; - return $rules; } public function prepareForValidation() { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $input = $this->all(); $input = $this->decodePrimaryKeys($input); @@ -50,6 +76,96 @@ class PreviewInvoiceRequest extends Request $input['balance'] = 0; $input['number'] = isset($input['number']) ? $input['number'] : ctrans('texts.live_preview').' #'.rand(0, 1000); + if($input['entity_id'] ?? false) + $input['entity_id'] = $this->decodePrimaryKey($input['entity_id'], true); + + $this->convertEntityPlural($input['entity'] ?? 'invoice'); + $this->replace($input); } + + public function resolveInvitation() + { + $invitation = false; + + if(! $this->entity_id ?? false) + return $this->stubInvitation(); + + match($this->entity){ + 'invoice' => $invitation = Invoice::withTrashed()->where('invoice_id', $this->entity_id)->first(), + 'quote' => $invitation = Quote::withTrashed()->where('quote_id', $this->entity_id)->first(), + 'credit' => $invitation = Credit::withTrashed()->where('credit_id', $this->entity_id)->first(), + 'recurring_invoice' => $invitation = RecurringInvoice::withTrashed()->where('recurring_invoice_id', $this->entity_id)->first(), + }; + + if($invitation) + return $invitation; + + $invitation = $this->stubInvitation(); + } + + public function stubInvitation() + { + $client = Client::query()->with('contacts', 'company', 'user')->withTrashed()->find($this->client_id); + $invitation = false; + + match($this->entity) { + 'invoice' => $invitation = InvoiceInvitation::factory()->make(), + 'quote' => $invitation = QuoteInvitation::factory()->make(), + 'credit' => $invitation = CreditInvitation::factory()->make(), + 'recurring_invoice' => $invitation = RecurringInvoiceInvitation::factory()->make(), + default => $invitation = InvoiceInvitation::factory()->make(), + }; + + $entity = $this->stubEntity($client); + + $invitation->make(); + $invitation->setRelation($this->entity, $entity); + $invitation->setRelation('contact', $client->contacts->first()->load('client.company')); + $invitation->setRelation('company', $client->company); + + return $invitation; + } + + private function stubEntity(Client $client) + { + $entity = false; + + match($this->entity){ + 'invoice' => $entity = Invoice::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + 'quote' => $entity = Quote::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + 'credit' => $entity = Credit::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + 'recurring_invoice' => $entity = RecurringInvoice::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + default => $entity = Invoice::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + }; + + $entity->setRelation('client', $client); + $entity->setRelation('company', $client->company); + $entity->setRelation('user', $client->user); + $entity->fill($this->all()); + + return $entity; + } + + private function convertEntityPlural(string $entity) :self + { + switch ($entity) { + case 'invoice': + $this->entity_plural = 'invoices'; + return $this; + case 'quote': + $this->entity_plural = 'quotes'; + return $this; + case 'credit': + $this->entity_plural = 'credits'; + return $this; + case 'recurring_invoice': + $this->entity_plural = 'invoices'; + return $this; + default: + $this->entity_plural = 'invoices'; + return $this; + } + } + } diff --git a/app/Jobs/Util/PreviewPdf.php b/app/Jobs/Util/PreviewPdf.php index 214201f1a660..0838379e9428 100644 --- a/app/Jobs/Util/PreviewPdf.php +++ b/app/Jobs/Util/PreviewPdf.php @@ -24,25 +24,14 @@ class PreviewPdf implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, PdfMaker, PageNumbering; - public $company; - - private $disk; - - public $design_string; - /** * Create a new job instance. * * @param $design_string * @param Company $company */ - public function __construct($design_string, Company $company) + public function __construct(public string $design_string, public Company $company) { - $this->company = $company; - - $this->design_string = $design_string; - - $this->disk = $disk ?? config('filesystems.default'); } public function handle() diff --git a/tests/Feature/LiveDesignTest.php b/tests/Feature/LiveDesignTest.php index 03ce04551e54..7dc750f10902 100644 --- a/tests/Feature/LiveDesignTest.php +++ b/tests/Feature/LiveDesignTest.php @@ -13,7 +13,9 @@ namespace Tests\Feature; use Tests\TestCase; use App\Models\Design; +use App\Utils\HtmlEngine; use Tests\MockAccountData; +use App\Models\InvoiceInvitation; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -41,6 +43,30 @@ class LiveDesignTest extends TestCase } } + public function testSyntheticInvitations() + { + $this->assertGreaterThanOrEqual(1, $this->client->contacts->count()); + + $ii = InvoiceInvitation::factory() + ->for($this->invoice) + ->for($this->client->contacts->first(), 'contact') + ->for($this->company) + ->for($this->user) + ->make(); + + $this->assertInstanceOf(InvoiceInvitation::class, $ii); + + $engine = new HtmlEngine($ii); + + $this->assertNotNull($engine); + + $data = $engine->generateLabelsAndValues(); + + $this->assertIsArray($data); + + nlog($data); + } + public function testDesignRoute200() { $data = [ From c955dfc910141f63c66cd19a82c20631c8634cad Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Oct 2023 21:38:35 +1100 Subject: [PATCH 13/38] Fixes for recurring invoice export --- app/Export/CSV/BaseExport.php | 1 + app/Export/CSV/InvoiceExport.php | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index e0344792895c..dc371aa28013 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -168,6 +168,7 @@ class BaseExport 'tax_rate1' => 'invoice.tax_rate1', 'tax_rate2' => 'invoice.tax_rate2', 'tax_rate3' => 'invoice.tax_rate3', + 'recurring_invoice' => 'invoice.recurring_id', ]; protected array $recurring_invoice_report_keys = [ diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 0a8f3188f2c3..741f5e22a029 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -142,6 +142,11 @@ class InvoiceExport extends BaseExport if (in_array('invoice.status', $this->input['report_keys'])) { $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); } + + if (in_array('invoice.recurring_id', $this->input['report_keys'])) { + $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; + } + return $entity; } From d06f383a98c16a08056c9f65be852cadc5338be6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 10:14:14 +1100 Subject: [PATCH 14/38] Minor fixes --- app/Models/Presenters/ClientPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Presenters/ClientPresenter.php b/app/Models/Presenters/ClientPresenter.php index ac4b184d94e1..aed92c0eb8c9 100644 --- a/app/Models/Presenters/ClientPresenter.php +++ b/app/Models/Presenters/ClientPresenter.php @@ -23,7 +23,7 @@ class ClientPresenter extends EntityPresenter */ public function name() { - if ($this->entity->name) { + if (strlen($this->entity->name) > 1) { return $this->entity->name; } From d62bdbedcbd7e5d61f40755d18933a453459ffad Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 10:30:45 +1100 Subject: [PATCH 15/38] Fixes for showing tasks in client portal --- app/Http/Controllers/RecurringInvoiceController.php | 1 + app/Http/Livewire/TasksTable.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/RecurringInvoiceController.php b/app/Http/Controllers/RecurringInvoiceController.php index d09d31e80220..94c2192d178a 100644 --- a/app/Http/Controllers/RecurringInvoiceController.php +++ b/app/Http/Controllers/RecurringInvoiceController.php @@ -157,6 +157,7 @@ class RecurringInvoiceController extends BaseController $user = auth()->user(); $recurring_invoice = RecurringInvoiceFactory::create($user->company()->id, $user->id); + $recurring_invoice->auto_bill = $user->company()->settings->auto_bill; return $this->itemResponse($recurring_invoice); } diff --git a/app/Http/Livewire/TasksTable.php b/app/Http/Livewire/TasksTable.php index 40106c390bd6..f229db889f46 100644 --- a/app/Http/Livewire/TasksTable.php +++ b/app/Http/Livewire/TasksTable.php @@ -39,11 +39,11 @@ class TasksTable extends Component ->where('is_deleted', false) ->where('client_id', auth()->guard('contact')->user()->client_id); - if ($this->company->getSetting('show_all_tasks_client_portal') === 'invoiced') { + if ( auth()->guard('contact')->user()->client->getSetting('show_all_tasks_client_portal') === 'invoiced') { $query = $query->whereNotNull('invoice_id'); } - if ($this->company->getSetting('show_all_tasks_client_portal') === 'uninvoiced') { + if ( auth()->guard('contact')->user()->client->getSetting('show_all_tasks_client_portal') === 'uninvoiced') { $query = $query->whereNull('invoice_id'); } From 1163c42fd8a53eae74fd82d74d162a8a53ae73c4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 15:22:38 +1100 Subject: [PATCH 16/38] Fixes for report exports --- app/Export/CSV/BaseExport.php | 10 +++- app/Export/CSV/CreditExport.php | 2 + app/Export/CSV/InvoiceExport.php | 2 + app/Export/CSV/InvoiceItemExport.php | 23 ++++++++ app/Export/CSV/PaymentExport.php | 2 + app/Export/CSV/PurchaseOrderExport.php | 9 ++- app/Export/CSV/PurchaseOrderItemExport.php | 2 + app/Export/CSV/QuoteExport.php | 2 + app/Export/CSV/QuoteItemExport.php | 2 + app/Export/CSV/RecurringInvoiceExport.php | 6 +- app/Export/CSV/TaskExport.php | 2 + .../Export/ReportCsvGenerationTest.php | 58 +++++++++++++++++-- 12 files changed, 110 insertions(+), 10 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index dc371aa28013..0db407e3440f 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -231,7 +231,7 @@ class BaseExport 'po_number' => 'purchase_order.po_number', 'private_notes' => 'purchase_order.private_notes', 'public_notes' => 'purchase_order.public_notes', - 'status' => 'purchase_order.status_id', + 'status' => 'purchase_order.status', 'tax_name1' => 'purchase_order.tax_name1', 'tax_name2' => 'purchase_order.tax_name2', 'tax_name3' => 'purchase_order.tax_name3', @@ -430,6 +430,14 @@ class BaseExport 'project' => 'task.project_id', ]; + protected array $forced_client_fields = [ + "name" => "client.name", + ]; + + protected array $forced_vendor_fields = [ + "name" => "vendor.name", + ]; + protected function filterByClients($query) { if (isset($this->input['client_id']) && $this->input['client_id'] != 'all') { diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 29afbfa16100..c5c05f3c23e2 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -93,6 +93,8 @@ class CreditExport extends BaseExport $this->input['report_keys'] = array_values($this->credit_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Credit::query() ->withTrashed() ->with('client') diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 741f5e22a029..4dc4ee3d8b94 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -50,6 +50,8 @@ class InvoiceExport extends BaseExport $this->input['report_keys'] = array_values($this->invoice_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Invoice::query() ->withTrashed() ->with('client') diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 74c1a64c25b0..5ac3c9fb2fbf 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -62,6 +62,8 @@ class InvoiceItemExport extends BaseExport $this->input['report_keys'] = array_values($this->mergeItemsKeys('invoice_report_keys')); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Invoice::query() ->withTrashed() ->with('client') @@ -200,6 +202,27 @@ class InvoiceItemExport extends BaseExport $entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']); } + if (in_array('invoice.country_id', $this->input['report_keys'])) { + $entity['invoice.country_id'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ''; + } + + if (in_array('invoice.currency_id', $this->input['report_keys'])) { + $entity['invoice.currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; + } + + if (in_array('invoice.client_id', $this->input['report_keys'])) { + $entity['invoice.client_id'] = $invoice->client->present()->name(); + } + + if (in_array('invoice.status', $this->input['report_keys'])) { + $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); + } + + if (in_array('invoice.recurring_id', $this->input['report_keys'])) { + $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; + } + + return $entity; } diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index 01c8641b0725..48f996fae794 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -48,6 +48,8 @@ class PaymentExport extends BaseExport $this->input['report_keys'] = array_values($this->payment_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Payment::query() ->withTrashed() ->where('company_id', $this->company->id) diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php index 88145822b10a..45e8a6adb1b7 100644 --- a/app/Export/CSV/PurchaseOrderExport.php +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -54,7 +54,7 @@ class PurchaseOrderExport extends BaseExport 'po_number' => 'purchase_order.po_number', 'private_notes' => 'purchase_order.private_notes', 'public_notes' => 'purchase_order.public_notes', - 'status' => 'purchase_order.status_id', + 'status' => 'purchase_order.status', 'tax_name1' => 'purchase_order.tax_name1', 'tax_name2' => 'purchase_order.tax_name2', 'tax_name3' => 'purchase_order.tax_name3', @@ -95,6 +95,9 @@ class PurchaseOrderExport extends BaseExport if (count($this->input['report_keys']) == 0) { $this->input['report_keys'] = array_values($this->purchase_order_report_keys); } + + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_vendor_fields, $this->input['report_keys'])); + $query = PurchaseOrder::query() ->withTrashed() ->with('vendor') @@ -181,8 +184,8 @@ class PurchaseOrderExport extends BaseExport $entity['vendor'] = $purchase_order->vendor->present()->name(); } - if (in_array('status_id', $this->input['report_keys'])) { - $entity['status'] = $purchase_order->stringStatus($purchase_order->status_id); + if (in_array('purchase_order.status', $this->input['report_keys'])) { + $entity['purchase_order.status'] = $purchase_order->stringStatus($purchase_order->status_id); } return $entity; diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 9ebf2991c95b..fb09ea6fa8e9 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -55,6 +55,8 @@ class PurchaseOrderItemExport extends BaseExport $this->input['report_keys'] = array_values($this->mergeItemsKeys('purchase_order_report_keys')); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_vendor_fields, $this->input['report_keys'])); + $query = PurchaseOrder::query() ->withTrashed() ->with('vendor')->where('company_id', $this->company->id) diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index 8083c0a71e7a..eba6adbcef26 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -56,6 +56,8 @@ class QuoteExport extends BaseExport $this->input['report_keys'] = array_values($this->quote_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Quote::query() ->withTrashed() ->with('client') diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index f85a82dd4c12..48bc25b95e6a 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -57,6 +57,8 @@ class QuoteItemExport extends BaseExport $this->input['report_keys'] = array_values($this->mergeItemsKeys('quote_report_keys')); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Quote::query() ->withTrashed() ->with('client')->where('company_id', $this->company->id) diff --git a/app/Export/CSV/RecurringInvoiceExport.php b/app/Export/CSV/RecurringInvoiceExport.php index d30359510c80..3234b0674337 100644 --- a/app/Export/CSV/RecurringInvoiceExport.php +++ b/app/Export/CSV/RecurringInvoiceExport.php @@ -48,6 +48,8 @@ class RecurringInvoiceExport extends BaseExport $this->input['report_keys'] = array_values($this->recurring_invoice_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = RecurringInvoice::query() ->withTrashed() ->with('client') @@ -135,8 +137,8 @@ class RecurringInvoiceExport extends BaseExport $entity['client'] = $invoice->client->present()->name(); } - if (in_array('status_id', $this->input['report_keys'])) { - $entity['status'] = $invoice->stringStatus($invoice->status_id); + if (in_array('recurring_invoice.status', $this->input['report_keys'])) { + $entity['recurring_invoice.status'] = $invoice->stringStatus($invoice->status_id); } if (in_array('project_id', $this->input['report_keys'])) { diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 87834c6aee4f..b2eb6425c527 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -60,6 +60,8 @@ class TaskExport extends BaseExport $this->input['report_keys'] = array_values($this->task_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Task::query() ->withTrashed() ->where('company_id', $this->company->id) diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index c7f400db7420..87c967c883bf 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -20,7 +20,6 @@ use App\Models\Account; use App\Models\Company; use App\Models\Expense; use App\Models\Invoice; -use Tests\MockAccountData; use App\Models\CompanyToken; use App\Models\ClientContact; use App\Export\CSV\TaskExport; @@ -30,8 +29,6 @@ use App\Export\CSV\ProductExport; use App\DataMapper\CompanySettings; use App\Export\CSV\PaymentExport; use App\Factory\CompanyUserFactory; -use App\Factory\InvoiceItemFactory; -use App\Services\Report\ARDetailReport; use Illuminate\Routing\Middleware\ThrottleRequests; /** @@ -262,6 +259,21 @@ class ReportCsvGenerationTest extends TestCase } + public function testForcedInsertionOfMandatoryColumns() + { + $forced = ['client.name']; + + $report_keys = ['invoice.number','client.name', 'invoice.amount']; + $array = array_merge($report_keys, array_diff($forced, $report_keys)); + + $this->assertEquals('client.name', $array[1]); + + $report_keys = ['invoice.number','invoice.amount']; + $array = array_merge($report_keys, array_diff($forced, $report_keys)); + + $this->assertEquals('client.name', $array[2]); + + } public function testVendorCsvGeneration() { @@ -322,7 +334,7 @@ class ReportCsvGenerationTest extends TestCase $data = $export->returnJson(); $this->assertNotNull($data); -// nlog($data); + // nlog($data); // $this->assertEquals(0, $this->traverseJson($data, 'columns.0.identifier')); $this->assertEquals('Vendor Name', $this->traverseJson($data, 'columns.9.display_value')); $this->assertEquals('vendor', $this->traverseJson($data, '0.0.entity')); @@ -1021,6 +1033,44 @@ class ReportCsvGenerationTest extends TestCase 'X-API-TOKEN' => $this->token, ])->post('/api/v1/reports/recurring_invoices', $data)->assertStatus(200); + } + + + public function testRecurringInvoiceColumnsCsvGeneration() + { + + \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'frequency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Recurring Invoice Invoice Number')); + $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'Recurring Invoice How Often')); + $this->assertEquals('Active', $this->getFirstValueByColumn($csv, 'Recurring Invoice Status')); } From cdcfcf715ce1edad6c7a6caa13539a2529b65942 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 15:38:45 +1100 Subject: [PATCH 17/38] Fixes for report exports --- app/Export/CSV/BaseExport.php | 5 +++-- app/Export/CSV/PaymentExport.php | 4 ++++ app/Services/Email/Email.php | 5 +++++ tests/Feature/InvoiceEmailTest.php | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 0db407e3440f..fd1c1b5a3ef4 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -378,6 +378,7 @@ class BaseExport "custom_value4" => "payment.custom_value4", "user" => "payment.user_id", "assigned_user" => "payment.assigned_user_id", + ]; protected array $expense_report_keys = [ @@ -431,11 +432,11 @@ class BaseExport ]; protected array $forced_client_fields = [ - "name" => "client.name", + "client.name", ]; protected array $forced_vendor_fields = [ - "name" => "vendor.name", + "vendor.name", ]; protected function filterByClients($query) diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index 48f996fae794..ef9a4479bca2 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -67,10 +67,14 @@ class PaymentExport extends BaseExport $headerdisplay = $this->buildHeader(); + nlog($headerdisplay); + $header = collect($this->input['report_keys'])->map(function ($key, $value) use ($headerdisplay) { return ['identifier' => $key, 'display_value' => $headerdisplay[$value]]; })->toArray(); + nlog($header); + $report = $query->cursor() ->map(function ($resource) { $row = $this->buildRow($resource); diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index cebb8b7b6f79..6fada767e07b 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -413,6 +413,11 @@ class Email implements ShouldQueue if ($address_object->address == " ") { return true; } + + if ($address_object->address == "") { + return true; + } + } diff --git a/tests/Feature/InvoiceEmailTest.php b/tests/Feature/InvoiceEmailTest.php index a08b306aa44d..89ef93a1d3b8 100644 --- a/tests/Feature/InvoiceEmailTest.php +++ b/tests/Feature/InvoiceEmailTest.php @@ -32,6 +32,8 @@ class InvoiceEmailTest extends TestCase use DatabaseTransactions; use GeneratesCounter; + public $faker; + protected function setUp() :void { parent::setUp(); From e41228a1151998a0f4f8c826603e59674916e31c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 15:54:55 +1100 Subject: [PATCH 18/38] Fixes for null/not set client contact passwords --- app/Http/Requests/Request.php | 2 +- tests/Feature/ClientTest.php | 28 ++++++++++++++++++++++++++++ tests/Feature/InvoiceEmailTest.php | 10 +++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 2fb9a589e082..8d7eb2acd05b 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -180,7 +180,7 @@ class Request extends FormRequest } //Filter the client contact password - if it is sent with ***** we should ignore it! - if (isset($contact['password'])) { + if (isset($contact['password']) && is_string($contact['password'])) { if (strlen($contact['password']) == 0) { $input['contacts'][$key]['password'] = ''; } else { diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php index 0244a3e8227b..89b49d945c6a 100644 --- a/tests/Feature/ClientTest.php +++ b/tests/Feature/ClientTest.php @@ -65,6 +65,34 @@ class ClientTest extends TestCase $this->makeTestData(); } + public function testStoreClientFixes() + { + $data = [ + "contacts" => [ + [ + "email" => "tenda@gmail.com", + "first_name" => "Tenda", + "is_primary" => True, + "last_name" => "Bavuma", + "password" => null, + "send_email" => True + ], + ], + "country_id" => "356", + "display_name" => "Tenda Bavuma", + "name" => "Tenda Bavuma", + "shipping_country_id" => "356", + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/clients', $data); + + $response->assertStatus(200); + } + public function testClientMergeContactDrop() { diff --git a/tests/Feature/InvoiceEmailTest.php b/tests/Feature/InvoiceEmailTest.php index 89ef93a1d3b8..23ba03236086 100644 --- a/tests/Feature/InvoiceEmailTest.php +++ b/tests/Feature/InvoiceEmailTest.php @@ -33,7 +33,7 @@ class InvoiceEmailTest extends TestCase use GeneratesCounter; public $faker; - + protected function setUp() :void { parent::setUp(); @@ -50,6 +50,14 @@ class InvoiceEmailTest extends TestCase } + public function testInvalidEmailParsing() + { + $email = 'illegal@example.com'; + + $this->assertTrue(strpos($email, '@example.com') !== false); + } + + public function testClientEmailHistory() { $system_log = new SystemLog(); From 25ceba275cfc44534c0236cbccd65b29813ec051 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 17:28:10 +1100 Subject: [PATCH 19/38] Add suppressions for user notifications --- app/Export/CSV/PaymentExport.php | 4 +- app/Jobs/Mail/NinjaMailerObject.php | 4 +- app/Listeners/User/UpdateUserLastLogin.php | 3 +- app/Mail/TemplateEmail.php | 11 +++-- app/Mail/VendorTemplateEmail.php | 20 +++++++-- app/Models/User.php | 4 +- app/Services/Email/Email.php | 5 ++- app/Services/Email/EmailDefaults.php | 4 +- app/Transformers/UserTransformer.php | 1 + ...1415_add_user_notification_suppression.php | 43 +++++++++++++++++++ database/seeders/CurrenciesSeeder.php | 1 + lang/en/texts.php | 5 ++- .../ninja2020/invoices/payment.blade.php | 2 +- 13 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 database/migrations/2023_10_18_061415_add_user_notification_suppression.php diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index ef9a4479bca2..7f77563f9b92 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -67,14 +67,12 @@ class PaymentExport extends BaseExport $headerdisplay = $this->buildHeader(); - nlog($headerdisplay); - $header = collect($this->input['report_keys'])->map(function ($key, $value) use ($headerdisplay) { return ['identifier' => $key, 'display_value' => $headerdisplay[$value]]; })->toArray(); nlog($header); - + $report = $query->cursor() ->map(function ($resource) { $row = $this->buildRow($resource); diff --git a/app/Jobs/Mail/NinjaMailerObject.php b/app/Jobs/Mail/NinjaMailerObject.php index 1d973cb90275..44eea91cc5e1 100644 --- a/app/Jobs/Mail/NinjaMailerObject.php +++ b/app/Jobs/Mail/NinjaMailerObject.php @@ -32,12 +32,12 @@ class NinjaMailerObject /* Variable for cascading notifications */ public $entity_string = false; - /* @var bool | App\Models\InvoiceInvitation | app\Models\QuoteInvitation | app\Models\CreditInvitation | app\Models\RecurringInvoiceInvitation | app\Models\PurchaseOrderInvitation $invitation*/ + /* @var bool | App\Models\InvoiceInvitation | App\Models\QuoteInvitation | App\Models\CreditInvitation | App\Models\RecurringInvoiceInvitation | App\Models\PurchaseOrderInvitation $invitation*/ public $invitation = false; public $template = false; - /* @var bool | App\Models\Invoice | app\Models\Quote | app\Models\Credit | app\Models\RecurringInvoice | app\Models\PurchaseOrder $invitation*/ + /* @var bool | App\Models\Invoice | App\Models\Quote | App\Models\Credit | App\Models\RecurringInvoice | App\Models\PurchaseOrder | App\Models\Payment $entity*/ public $entity = false; public $reminder_template = ''; diff --git a/app/Listeners/User/UpdateUserLastLogin.php b/app/Listeners/User/UpdateUserLastLogin.php index 9fc383db207f..94e20512eefc 100644 --- a/app/Listeners/User/UpdateUserLastLogin.php +++ b/app/Listeners/User/UpdateUserLastLogin.php @@ -55,7 +55,7 @@ class UpdateUserLastLogin implements ShouldQueue $key = "user_logged_in_{$user->id}{$event->company->db}"; - if ($user->ip != $ip && is_null(Cache::get($key))) { + if ($user->ip != $ip && is_null(Cache::get($key)) && $user->user_logged_in_notification) { $nmo = new NinjaMailerObject; $nmo->mailable = new UserLoggedIn($user, $user->account->companies->first(), $ip); $nmo->company = $user->account->companies->first(); @@ -69,6 +69,7 @@ class UpdateUserLastLogin implements ShouldQueue Cache::put($key, true, 60 * 24); $arr = json_encode(['ip' => $ip]); + $arr = ctrans('texts.new_login_detected'). " {$ip}"; SystemLogger::dispatch( $arr, diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 984029cefe26..63794936e5c8 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -108,12 +108,15 @@ class TemplateEmail extends Mailable if (strlen($settings->bcc_email) > 1) { if (Ninja::isHosted()) { - $bccs = explode(',', str_replace(' ', '', $settings->bcc_email)); - $this->bcc(array_slice($bccs, 0, 2)); - //$this->bcc(reset($bccs)); //remove whitespace if any has been inserted. + + if($company->account->isPaid()) { + $bccs = explode(',', str_replace(' ', '', $settings->bcc_email)); + $this->bcc(array_slice($bccs, 0, 5)); + } + } else { $this->bcc(explode(',', str_replace(' ', '', $settings->bcc_email))); - }//remove whitespace if any has been inserted. + } } $this->subject(str_replace("
", "", $this->build_email->getSubject())) diff --git a/app/Mail/VendorTemplateEmail.php b/app/Mail/VendorTemplateEmail.php index 2ac0987b8045..bdfb818b0091 100644 --- a/app/Mail/VendorTemplateEmail.php +++ b/app/Mail/VendorTemplateEmail.php @@ -11,10 +11,11 @@ namespace App\Mail; +use App\Utils\Ninja; use App\Models\VendorContact; -use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; -use App\Utils\VendorHtmlEngine; use Illuminate\Mail\Mailable; +use App\Utils\VendorHtmlEngine; +use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; class VendorTemplateEmail extends Mailable { @@ -102,8 +103,19 @@ class VendorTemplateEmail extends Mailable $this->from(config('mail.from.address'), $email_from_name); if (strlen($settings->bcc_email) > 1) { - $this->bcc(explode(',', str_replace(' ', '', $settings->bcc_email))); - }//remove whitespace if any has been inserted. + + if (Ninja::isHosted()) { + + if($this->company->account->isPaid()) { + $bccs = explode(',', str_replace(' ', '', $settings->bcc_email)); + $this->bcc(array_slice($bccs, 0, 5)); + } + + } else { + $this->bcc(explode(',', str_replace(' ', '', $settings->bcc_email))); + } + + } $this->subject($this->build_email->getSubject()) ->text('email.template.text', [ diff --git a/app/Models/User.php b/app/Models/User.php index 22e5130d0df1..aec9b0e787d5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -73,7 +73,8 @@ use Illuminate\Foundation\Auth\User as Authenticatable; * @property int|null $deleted_at * @property string|null $oauth_user_refresh_token * @property string|null $last_confirmed_email_address - * @property int $has_password + * @property bool $has_password + * @property bool $user_logged_in_notification * @property Carbon|null $oauth_user_token_expiry * @property string|null $sms_verification_code * @property bool $verified_phone_number @@ -140,6 +141,7 @@ class User extends Authenticatable implements MustVerifyEmail * */ protected $fillable = [ + 'user_logged_in_notification', 'first_name', 'last_name', 'email', diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index 6fada767e07b..c539d075d445 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -140,7 +140,7 @@ class Email implements ShouldQueue $this->email_object->client_id ? $this->email_object->settings = $this->email_object->client->getMergedSettings() : $this->email_object->settings = $this->company->settings; - $this->email_object->client_id ? nlog("client settings") : nlog("company settings "); + // $this->email_object->client_id ? nlog("client settings") : nlog("company settings "); $this->email_object->whitelabel = $this->company->account->isPaid() ? true : false; @@ -418,6 +418,9 @@ class Email implements ShouldQueue return true; } + if($address_object->name == " " || $address_object->name == "") { + return true; + } } diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 401060b6c7b4..1f78facaa1e5 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -255,8 +255,8 @@ class EmailDefaults if (strlen($this->email->email_object->settings->bcc_email) > 1) { if (Ninja::isHosted() && $this->email->company->account->isPaid()) { - $bccs = array_slice(explode(',', str_replace(' ', '', $this->email->email_object->settings->bcc_email)), 0, 2); - } elseif (Ninja::isSelfHost()) { + $bccs = array_slice(explode(',', str_replace(' ', '', $this->email->email_object->settings->bcc_email)), 0, 5); + } else { $bccs = (explode(',', str_replace(' ', '', $this->email->email_object->settings->bcc_email))); } } diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php index ffe029fbb0fe..0971c7dc2b02 100644 --- a/app/Transformers/UserTransformer.php +++ b/app/Transformers/UserTransformer.php @@ -64,6 +64,7 @@ class UserTransformer extends EntityTransformer 'oauth_user_token' => empty($user->oauth_user_token) ? '' : '***', 'verified_phone_number' => (bool) $user->verified_phone_number, 'language_id' => (string) $user->language_id ?? '', + 'user_logged_in_notification' => (bool) $user->user_logged_in_notification, ]; } diff --git a/database/migrations/2023_10_18_061415_add_user_notification_suppression.php b/database/migrations/2023_10_18_061415_add_user_notification_suppression.php new file mode 100644 index 000000000000..4c01624a02ac --- /dev/null +++ b/database/migrations/2023_10_18_061415_add_user_notification_suppression.php @@ -0,0 +1,43 @@ +boolean('user_logged_in_notification')->default(true); + }); + + + $cur = Currency::find(120); + + if(!$cur) { + $cur = new \App\Models\Currency(); + $cur->id = 120; + $cur->code = 'TOP'; + $cur->name = "Tongan Pa'anga"; + $cur->symbol = 'T$'; + $cur->thousand_separator = ','; + $cur->decimal_separator = '.'; + $cur->precision = 2; + $cur->save(); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/seeders/CurrenciesSeeder.php b/database/seeders/CurrenciesSeeder.php index b2c8b35af751..e6976622ee86 100644 --- a/database/seeders/CurrenciesSeeder.php +++ b/database/seeders/CurrenciesSeeder.php @@ -142,6 +142,7 @@ class CurrenciesSeeder extends Seeder ['id' => 117, 'name' => 'Gold Troy Ounce', 'code' => 'XAU', 'symbol' => 'XAU', 'precision' => '3', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 118, 'name' => 'Nicaraguan Córdoba', 'code' => 'NIO', 'symbol' => 'C$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 119, 'name' => 'Malagasy ariary', 'code' => 'MGA', 'symbol' => 'Ar', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['id' => 120, 'name' => "Tongan Pa anga", 'code' => 'TOP', 'symbol' => 'T$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ]; foreach ($currencies as $currency) { diff --git a/lang/en/texts.php b/lang/en/texts.php index 5fe2741906a1..c7bdffb4bd50 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -2402,7 +2402,10 @@ $LANG = array( 'currency_libyan_dinar' => 'Libyan Dinar', 'currency_silver_troy_ounce' => 'Silver Troy Ounce', 'currency_gold_troy_ounce' => 'Gold Troy Ounce', - + 'currency_nicaraguan_córdoba' => 'Nicaraguan Córdoba', + 'currency_malagasy_ariary' => 'Malagasy ariary', + "currency_tongan_pa_anga" => "Tongan Pa'anga", + 'review_app_help' => 'We hope you\'re enjoying using the app.
If you\'d consider :link we\'d greatly appreciate it!', 'writing_a_review' => 'writing a review', diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php index 8d5df833d326..a0d786ff4610 100644 --- a/resources/views/portal/ninja2020/invoices/payment.blade.php +++ b/resources/views/portal/ninja2020/invoices/payment.blade.php @@ -64,7 +64,7 @@ {{ ctrans('texts.public_notes') }}
- {{ $invoice->public_notes }} + {!! html_entity_decode($invoice->public_notes) !!}
@else
From d49909e799e03d72a204d5a3359f2688a22bead3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 17:43:26 +1100 Subject: [PATCH 20/38] Add user login notification suppression --- app/Factory/UserFactory.php | 3 ++- lang/en/texts.php | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Factory/UserFactory.php b/app/Factory/UserFactory.php index d31a0019b2c9..9b15037f625c 100644 --- a/app/Factory/UserFactory.php +++ b/app/Factory/UserFactory.php @@ -27,7 +27,8 @@ class UserFactory $user->last_login = now(); $user->failed_logins = 0; $user->signature = ''; - $user->theme_id = 0; + $user->theme_id = 0; + $user->user_logged_in_notification = true; return $user; } diff --git a/lang/en/texts.php b/lang/en/texts.php index c7bdffb4bd50..48cfe30beb3a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -2405,7 +2405,7 @@ $LANG = array( 'currency_nicaraguan_córdoba' => 'Nicaraguan Córdoba', 'currency_malagasy_ariary' => 'Malagasy ariary', "currency_tongan_pa_anga" => "Tongan Pa'anga", - + 'review_app_help' => 'We hope you\'re enjoying using the app.
If you\'d consider :link we\'d greatly appreciate it!', 'writing_a_review' => 'writing a review', @@ -5183,6 +5183,8 @@ $LANG = array( 'upcoming' => 'Upcoming', 'client_contact' => 'Client Contact', 'uncategorized' => 'Uncategorized', + 'login_notification' => 'Login Notification', + 'login_notification_help' => 'Sends an email notifying that a login has taken place.' ); return $LANG; From bde9f60aeece6f7b0e7527e783735c5f6d216c85 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 18:42:16 +1100 Subject: [PATCH 21/38] Add group settings to client transformer --- app/Jobs/Mail/NinjaMailerObject.php | 2 +- app/Transformers/ClientTransformer.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Mail/NinjaMailerObject.php b/app/Jobs/Mail/NinjaMailerObject.php index 44eea91cc5e1..df33c35c2a11 100644 --- a/app/Jobs/Mail/NinjaMailerObject.php +++ b/app/Jobs/Mail/NinjaMailerObject.php @@ -37,7 +37,7 @@ class NinjaMailerObject public $template = false; - /* @var bool | App\Models\Invoice | App\Models\Quote | App\Models\Credit | App\Models\RecurringInvoice | App\Models\PurchaseOrder | App\Models\Payment $entity*/ + /* @var bool | App\Models\Invoice | App\Models\Quote | App\Models\Credit | App\Models\RecurringInvoice | App\Models\PurchaseOrder | App\Models\Payment $entity */ public $entity = false; public $reminder_template = ''; diff --git a/app/Transformers/ClientTransformer.php b/app/Transformers/ClientTransformer.php index 711c9cfec5ba..4cfb7742b3e4 100644 --- a/app/Transformers/ClientTransformer.php +++ b/app/Transformers/ClientTransformer.php @@ -17,6 +17,7 @@ use App\Models\ClientContact; use App\Models\ClientGatewayToken; use App\Models\CompanyLedger; use App\Models\Document; +use App\Models\GroupSetting; use App\Models\SystemLog; use App\Utils\Traits\MakesHash; use League\Fractal\Resource\Collection; @@ -42,6 +43,7 @@ class ClientTransformer extends EntityTransformer 'activities', 'ledger', 'system_logs', + 'group_settings', ]; /** @@ -96,6 +98,16 @@ class ClientTransformer extends EntityTransformer return $this->includeCollection($client->system_logs, $transformer, SystemLog::class); } + public function includeGroupSettings(Client $client) + { + if (!$client->group_settings) + return null; + + $transformer = new GroupSettingTransformer($this->serializer); + + return $this->includeItem($client->group_settings, $transformer, GroupSetting::class); + } + /** * @param Client $client * From 3ae1d3e694da8dc143a28ff25ac893350a0b26ed Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 20:03:26 +1100 Subject: [PATCH 22/38] v5.7.31 --- VERSION.txt | 2 +- app/Helpers/Epc/EpcQrGenerator.php | 7 ++++++- app/Helpers/SwissQr/SwissQrGenerator.php | 11 ++++++++--- config/ninja.php | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 25285daaef52..557bab7d4518 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.30 \ No newline at end of file +5.7.31 \ No newline at end of file diff --git a/app/Helpers/Epc/EpcQrGenerator.php b/app/Helpers/Epc/EpcQrGenerator.php index 62d61a0a15bb..342c5f775f06 100644 --- a/app/Helpers/Epc/EpcQrGenerator.php +++ b/app/Helpers/Epc/EpcQrGenerator.php @@ -50,7 +50,8 @@ class EpcQrGenerator ); $writer = new Writer($renderer); - $this->validateFields(); + if($this->validateFields()) + return ''; $qr = $writer->writeString($this->encodeMessage(), 'utf-8'); @@ -87,12 +88,16 @@ class EpcQrGenerator private function validateFields() { if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company2)) { + return true; nlog('The BIC field is not present and _may_ be a required fields for EPC QR codes'); } if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company1)) { + return true; nlog('The IBAN field is required'); } + + return false; } private function formatMoney($value) diff --git a/app/Helpers/SwissQr/SwissQrGenerator.php b/app/Helpers/SwissQr/SwissQrGenerator.php index abacbb57c056..d8c1ad77304c 100644 --- a/app/Helpers/SwissQr/SwissQrGenerator.php +++ b/app/Helpers/SwissQr/SwissQrGenerator.php @@ -174,9 +174,14 @@ class SwissQrGenerator return $html; } catch (\Exception $e) { - foreach ($qrBill->getViolations() as $key => $violation) { - nlog("qr"); - nlog($violation); + + if(is_iterable($qrBill->getViolations())) { + + foreach ($qrBill->getViolations() as $key => $violation) { + nlog("qr"); + nlog($violation); + } + } nlog($e->getMessage()); diff --git a/config/ninja.php b/config/ninja.php index 7ef548ab7efa..df657179e40b 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.30'), - 'app_tag' => env('APP_TAG','5.7.30'), + 'app_version' => env('APP_VERSION','5.7.31'), + 'app_tag' => env('APP_TAG','5.7.31'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), From 546e8277329a6537dc7b0bc4795dfaec201099dd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 18 Oct 2023 22:26:13 +1100 Subject: [PATCH 23/38] Checks for history in system logs --- app/Http/Controllers/EmailHistoryController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/EmailHistoryController.php b/app/Http/Controllers/EmailHistoryController.php index aea6a5e2008f..f29f1a79bd9d 100644 --- a/app/Http/Controllers/EmailHistoryController.php +++ b/app/Http/Controllers/EmailHistoryController.php @@ -32,7 +32,7 @@ class EmailHistoryController extends BaseController ->orderBy('id', 'DESC') ->cursor() ->filter(function ($system_log) { - return ($system_log->log['history'] && isset($system_log->log['history']['events']) && count($system_log->log['history']['events']) >=1) !== false; + return (isset($system_log->log['history']) && isset($system_log->log['history']['events']) && count($system_log->log['history']['events']) >=1) !== false; })->map(function ($system_log) { return $system_log->log['history']; })->values()->all(); From e17af36cf4309526acaf188765361792fbdedccd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 Oct 2023 07:34:40 +1100 Subject: [PATCH 24/38] Improvements for live preview --- app/Http/Controllers/PreviewController.php | 8 +++----- app/Http/Requests/Preview/PreviewInvoiceRequest.php | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 6777af3f941d..2797bae0eea1 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -61,11 +61,11 @@ class PreviewController extends BaseController } private function purgeCache() - { nlog(auth()->user()->id); + { Cache::pull("preview_".auth()->user()->id); } - public function newLivePreview(PreviewInvoiceRequest $request) + public function live(PreviewInvoiceRequest $request) { $time = time(); @@ -153,8 +153,6 @@ class PreviewController extends BaseController $pdf = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); - nlog("merpy derp {$time}"); - return response()->streamDownload(function () use ($pdf) { echo $pdf; }, 'preview.pdf', ['Content-Type' => 'application/pdf','Cache-Control:' => 'no-cache']); @@ -273,7 +271,7 @@ class PreviewController extends BaseController return $response; } - public function live(PreviewInvoiceRequest $request) + public function livex(PreviewInvoiceRequest $request) { // if(Cache::has("preview_".auth()->user()->id)) diff --git a/app/Http/Requests/Preview/PreviewInvoiceRequest.php b/app/Http/Requests/Preview/PreviewInvoiceRequest.php index 50cc0a7fdf73..2188efa37c98 100644 --- a/app/Http/Requests/Preview/PreviewInvoiceRequest.php +++ b/app/Http/Requests/Preview/PreviewInvoiceRequest.php @@ -92,10 +92,10 @@ class PreviewInvoiceRequest extends Request return $this->stubInvitation(); match($this->entity){ - 'invoice' => $invitation = Invoice::withTrashed()->where('invoice_id', $this->entity_id)->first(), - 'quote' => $invitation = Quote::withTrashed()->where('quote_id', $this->entity_id)->first(), - 'credit' => $invitation = Credit::withTrashed()->where('credit_id', $this->entity_id)->first(), - 'recurring_invoice' => $invitation = RecurringInvoice::withTrashed()->where('recurring_invoice_id', $this->entity_id)->first(), + 'invoice' => $invitation = InvoiceInvitation::withTrashed()->where('invoice_id', $this->entity_id)->first(), + 'quote' => $invitation = QuoteInvitation::withTrashed()->where('quote_id', $this->entity_id)->first(), + 'credit' => $invitation = CreditInvitation::withTrashed()->where('credit_id', $this->entity_id)->first(), + 'recurring_invoice' => $invitation = RecurringInvoiceInvitation::withTrashed()->where('recurring_invoice_id', $this->entity_id)->first(), }; if($invitation) From ec04f3fd1e71a4601e4961c8c7ef45dff107665e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 Oct 2023 08:53:28 +1100 Subject: [PATCH 25/38] Refactor for live previews --- app/Http/Controllers/PreviewController.php | 45 ++++++++++++------- .../Preview/PreviewInvoiceRequest.php | 20 ++++++++- .../RecurringInvoiceInvitationFactory.php | 30 +++++++++++++ 3 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 database/factories/RecurringInvoiceInvitationFactory.php diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 2797bae0eea1..15c5f1612a24 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -68,24 +68,34 @@ class PreviewController extends BaseController public function live(PreviewInvoiceRequest $request) { - $time = time(); - nlog($time); + $start = microtime(true); +nlog("1 ".$start); $invitation = $request->resolveInvitation(); + $client = $request->getClient(); + $settings = $client->getMergedSettings(); App::forgetInstance('translator'); $t = app('translator'); App::setLocale($invitation->contact->preferredLocale()); - $t->replace(Ninja::transformTranslations($invitation->{$request->entity}->client->getMergedSettings())); - - $html = new HtmlEngine($invitation); - $variables = $html->generateLabelsAndValues(); + $t->replace(Ninja::transformTranslations($settings)); +nlog("2 ".microtime(true)); + $entity_prop = str_replace("recurring_", "", $request->entity); $entity_obj = $invitation->{$request->entity}; - // if (! $invitation->{$request->entity}->id ?? true) { - // $invitation->{$request->entity}->service()->fillDefaults(); - // } + if(!$entity_obj->id) { + $entity_obj->design_id = intval($this->decodePrimaryKey($settings->{$entity_prop."_design_id"})); + $entity_obj->footer = $settings->{$entity_prop."_footer"}; + $entity_obj->terms = $settings->{$entity_prop."_terms"}; + $entity_obj->public_notes = $request->getClient()->public_notes; + $invitation->{$request->entity} = $entity_obj; + } + + $html = new HtmlEngine($invitation); + $html->settings = $settings; + $variables = $html->generateLabelsAndValues(); +nlog("3 ".microtime(true)); $design = \App\Models\Design::withTrashed()->find($entity_obj->design_id ?? 2); @@ -105,18 +115,18 @@ class PreviewController extends BaseController $state = [ 'template' => $template->elements([ - 'client' => $entity_obj->client, + 'client' => $client, 'entity' => $entity_obj, - 'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables, + 'pdf_variables' => (array) $settings->pdf_variables, '$product' => $design->design->product, 'variables' => $variables, ]), 'variables' => $variables, 'options' => [ - 'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'), - 'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'), + 'all_pages_header' => $client->getSetting('all_pages_header'), + 'all_pages_footer' => $client->getSetting('all_pages_footer'), ], - 'process_markdown' => $entity_obj->client->company->markdown_enabled, + 'process_markdown' => $client->company->markdown_enabled, ]; $maker = new PdfMaker($state); @@ -125,6 +135,8 @@ class PreviewController extends BaseController ->design($template) ->build(); +nlog("4 ".microtime(true)); + if (request()->query('html') == 'true') { return $maker->getCompiledHTML(); } @@ -153,9 +165,12 @@ class PreviewController extends BaseController $pdf = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); +nlog("5 ".microtime(true)); +nlog("total = ".microtime(true)-$start); + return response()->streamDownload(function () use ($pdf) { echo $pdf; - }, 'preview.pdf', ['Content-Type' => 'application/pdf','Cache-Control:' => 'no-cache']); + }, 'preview.pdf', ['Content-Disposition' => 'inline', 'Content-Type' => 'application/pdf','Cache-Control:' => 'no-cache', 'Server-Timing' => microtime(true)-$start]); } diff --git a/app/Http/Requests/Preview/PreviewInvoiceRequest.php b/app/Http/Requests/Preview/PreviewInvoiceRequest.php index 2188efa37c98..57bd0cf22ef0 100644 --- a/app/Http/Requests/Preview/PreviewInvoiceRequest.php +++ b/app/Http/Requests/Preview/PreviewInvoiceRequest.php @@ -15,9 +15,7 @@ use App\Models\Quote; use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; -use App\Libraries\MultiDB; use App\Http\Requests\Request; -use App\Models\CompanyGateway; use App\Models\QuoteInvitation; use App\Utils\Traits\MakesHash; use Illuminate\Validation\Rule; @@ -34,6 +32,8 @@ class PreviewInvoiceRequest extends Request private string $entity_plural = ''; + private ?Client $client = null; + /** * Determine if the user is authorized to make this request. * @@ -104,9 +104,25 @@ class PreviewInvoiceRequest extends Request $invitation = $this->stubInvitation(); } + public function getClient(): ?Client + { + if(!$this->client) + $this->client = Client::query()->with('contacts', 'company', 'user')->withTrashed()->find($this->client_id); + + return $this->client; + } + + public function setClient(Client $client): self + { + $this->client = $client; + + return $this; + } + public function stubInvitation() { $client = Client::query()->with('contacts', 'company', 'user')->withTrashed()->find($this->client_id); + $this->setClient($client); $invitation = false; match($this->entity) { diff --git a/database/factories/RecurringInvoiceInvitationFactory.php b/database/factories/RecurringInvoiceInvitationFactory.php new file mode 100644 index 000000000000..fe7db151f11d --- /dev/null +++ b/database/factories/RecurringInvoiceInvitationFactory.php @@ -0,0 +1,30 @@ + Str::random(40), + ]; + } +} From a93ff3e1e5c2154626ecd2be9ffd4d93f5a35192 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 Oct 2023 10:44:19 +1100 Subject: [PATCH 26/38] Clean up --- app/Http/Controllers/PreviewController.php | 141 +++++++++++------- .../Requests/Preview/DesignPreviewRequest.php | 13 +- 2 files changed, 99 insertions(+), 55 deletions(-) diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 15c5f1612a24..596024f30368 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -64,26 +64,39 @@ class PreviewController extends BaseController { Cache::pull("preview_".auth()->user()->id); } - - public function live(PreviewInvoiceRequest $request) + + /** + * Refactor - 2023-10-19 + * + * New method does not require Transactions. + * + * @param PreviewInvoiceRequest $request + * @return mixed + */ + public function live(PreviewInvoiceRequest $request): mixed { + if (Ninja::isHosted() && !in_array($request->getHost(), ['preview.invoicing.co','staging.invoicing.co'])) { + return response()->json(['message' => 'This server cannot handle this request.'], 400); + } + $start = microtime(true); -nlog("1 ".$start); + /** Build models */ $invitation = $request->resolveInvitation(); $client = $request->getClient(); $settings = $client->getMergedSettings(); + /** Set translations */ App::forgetInstance('translator'); $t = app('translator'); App::setLocale($invitation->contact->preferredLocale()); $t->replace(Ninja::transformTranslations($settings)); -nlog("2 ".microtime(true)); $entity_prop = str_replace("recurring_", "", $request->entity); $entity_obj = $invitation->{$request->entity}; + /** Update necessary objecty props */ if(!$entity_obj->id) { $entity_obj->design_id = intval($this->decodePrimaryKey($settings->{$entity_prop."_design_id"})); $entity_obj->footer = $settings->{$entity_prop."_footer"}; @@ -92,10 +105,10 @@ nlog("2 ".microtime(true)); $invitation->{$request->entity} = $entity_obj; } + /** Generate variables */ $html = new HtmlEngine($invitation); $html->settings = $settings; $variables = $html->generateLabelsAndValues(); -nlog("3 ".microtime(true)); $design = \App\Models\Design::withTrashed()->find($entity_obj->design_id ?? 2); @@ -135,15 +148,15 @@ nlog("3 ".microtime(true)); ->design($template) ->build(); -nlog("4 ".microtime(true)); + /** Generate HTML */ + $html = $maker->getCompiledHTML(true); - if (request()->query('html') == 'true') { - return $maker->getCompiledHTML(); - } + if (request()->query('html') == 'true') + return $html; //if phantom js...... inject here.. if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { - return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); + return (new Phantom)->convertHtmlToPdf($html); } /** @var \App\Models\User $user */ @@ -152,34 +165,69 @@ nlog("4 ".microtime(true)); if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { - $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); - + $pdf = (new NinjaPdf())->build($html); $numbered_pdf = $this->pageNumbering($pdf, $company); - if ($numbered_pdf) { + if ($numbered_pdf) $pdf = $numbered_pdf; - } return $pdf; } - $pdf = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + $pdf = (new PreviewPdf($html, $company))->handle(); -nlog("5 ".microtime(true)); -nlog("total = ".microtime(true)-$start); + if (Ninja::isHosted()) { + LightLogs::create(new LivePreview()) + ->increment() + ->batch(); + } + /** Return PDF */ return response()->streamDownload(function () use ($pdf) { echo $pdf; - }, 'preview.pdf', ['Content-Disposition' => 'inline', 'Content-Type' => 'application/pdf','Cache-Control:' => 'no-cache', 'Server-Timing' => microtime(true)-$start]); + }, 'preview.pdf', [ + 'Content-Disposition' => 'inline', + 'Content-Type' => 'application/pdf', + 'Cache-Control:' => 'no-cache', + 'Server-Timing' => microtime(true)-$start + ]); } + + /** + * Returns the mocked PDF for the invoice design preview. + * + * Only used in Settings > Invoice Design as a general overview + * + * @param DesignPreviewRequest $request + * @return mixed + */ + public function design(DesignPreviewRequest $request): mixed + { + $start = microtime(true); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + /** @var \App\Models\Company $company */ + $company = $user->company(); + + $pdf = (new PdfMock($request->all(), $company))->build()->getPdf(); + + $response = Response::make($pdf, 200); + $response->header('Content-Type', 'application/pdf'); + $response->header('Server-Timing', microtime(true)-$start); + + return $response; + } + /** * Returns a template filled with entity variables. - * + * + * Used in the Custom Designer to preview design changes * @return mixed */ - public function show() { if (request()->has('entity') && @@ -187,6 +235,7 @@ nlog("total = ".microtime(true)-$start); ! empty(request()->input('entity')) && ! empty(request()->input('entity_id')) && request()->has('body')) { + $design_object = json_decode(json_encode(request()->input('design'))); if (! is_object($design_object)) { @@ -243,56 +292,50 @@ nlog("total = ".microtime(true)-$start); return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); } - /** @var \App\Models\User $user */ $user = auth()->user(); - $company = $user->company(); if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); - $numbered_pdf = $this->pageNumbering($pdf, $company); - - if ($numbered_pdf) { + if ($numbered_pdf) $pdf = $numbered_pdf; - } return $pdf; + } - $file_path = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + $pdf = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + + return response()->streamDownload(function () use ($pdf) { + echo $pdf; + }, 'preview.pdf', [ + 'Content-Disposition' => 'inline', + 'Content-Type' => 'application/pdf', + 'Cache-Control:' => 'no-cache', + ]); + - return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); } return $this->blankEntity(); } - public function design(DesignPreviewRequest $request) - { - /** @var \App\Models\User $user */ - $user = auth()->user(); - - /** @var \App\Models\Company $company */ - $company = $user->company(); - - $pdf = (new PdfMock($request->all(), $company))->build()->getPdf(); - - $response = Response::make($pdf, 200); - $response->header('Content-Type', 'application/pdf'); - - return $response; - } - + + + /** + * @deprecated due to usage of transactions + * + * @param mixed $request + * @return void + */ public function livex(PreviewInvoiceRequest $request) { - // if(Cache::has("preview_".auth()->user()->id)) - // return response()->json(['message' => 'Please wait a few seconds before trying again, this many requests are not good.'], 400); - - nlog("should not see a lot of these"); + if(Cache::has("preview_".auth()->user()->id)) + return response()->json(['message' => 'Please wait a few seconds before trying again, this many requests are not good.'], 400); if (Ninja::isHosted() && !in_array($request->getHost(), ['preview.invoicing.co','staging.invoicing.co'])) { return response()->json(['message' => 'This server cannot handle this request.'], 400); @@ -463,7 +506,6 @@ nlog("total = ".microtime(true)-$start); $response->header('Content-Type', 'application/pdf'); $response->header('Server-Timing', microtime(true)-$start); - nlog("returning a response"); $this->purgeCache(); return $response; } @@ -652,7 +694,6 @@ nlog("total = ".microtime(true)-$start); $response = Response::make($file_path, 200); $response->header('Content-Type', 'application/pdf'); - return $response; } } diff --git a/app/Http/Requests/Preview/DesignPreviewRequest.php b/app/Http/Requests/Preview/DesignPreviewRequest.php index f3706c9f5b20..ac2b72fb1ad9 100644 --- a/app/Http/Requests/Preview/DesignPreviewRequest.php +++ b/app/Http/Requests/Preview/DesignPreviewRequest.php @@ -32,11 +32,14 @@ class DesignPreviewRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('create', Invoice::class) || - auth()->user()->can('create', Quote::class) || - auth()->user()->can('create', RecurringInvoice::class) || - auth()->user()->can('create', Credit::class) || - auth()->user()->can('create', PurchaseOrder::class); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('create', Invoice::class) || + $user->can('create', Quote::class) || + $user->can('create', RecurringInvoice::class) || + $user->can('create', Credit::class) || + $user->can('create', PurchaseOrder::class); } public function rules() From 9059117f99cc50d2e0cac6d39a04916a3d011ebf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 Oct 2023 10:45:34 +1100 Subject: [PATCH 27/38] v5.7.32 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 557bab7d4518..5a3ee7ad5530 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.31 \ No newline at end of file +5.7.32 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index df657179e40b..56b9f06de32f 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.31'), - 'app_tag' => env('APP_TAG','5.7.31'), + 'app_version' => env('APP_VERSION','5.7.32'), + 'app_tag' => env('APP_TAG','5.7.32'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), From c67f9cdaf49712cbee97222192ca2d0f99090254 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 Oct 2023 10:50:25 +1100 Subject: [PATCH 28/38] Minor fixes --- app/Http/Controllers/PreviewController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 596024f30368..ad68dd371c70 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -95,6 +95,7 @@ class PreviewController extends BaseController $entity_prop = str_replace("recurring_", "", $request->entity); $entity_obj = $invitation->{$request->entity}; + $entity_obj->fill($request->all()); /** Update necessary objecty props */ if(!$entity_obj->id) { From e5bd186b617edf962eb007356cb0270fc84b6f71 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 Oct 2023 11:23:13 +1100 Subject: [PATCH 29/38] Fixes for tax calculations --- app/Http/Controllers/ClientController.php | 3 +- app/Jobs/Client/UpdateTaxData.php | 65 +--------------------- app/Services/Tax/Providers/TaxProvider.php | 2 + 3 files changed, 5 insertions(+), 65 deletions(-) diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 7b305b09ff65..0bb9508f7646 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -308,7 +308,8 @@ class ClientController extends BaseController */ public function updateTaxData(PurgeClientRequest $request, Client $client) { - (new UpdateTaxData($client, $client->company))->handle(); + if($client->company->account->isPaid()) + (new UpdateTaxData($client, $client->company))->handle(); return $this->itemResponse($client->fresh()); } diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php index f28781bfe080..89a25bdd865c 100644 --- a/app/Jobs/Client/UpdateTaxData.php +++ b/app/Jobs/Client/UpdateTaxData.php @@ -52,7 +52,7 @@ class UpdateTaxData implements ShouldQueue { MultiDB::setDb($this->company->db); - if($this->company->account->isFreeHostedClient()) + if($this->company->account->isFreeHostedClient() || $this->client->country_id != 840) return; $tax_provider = new \App\Services\Tax\Providers\TaxProvider($this->company, $this->client); @@ -73,69 +73,6 @@ class UpdateTaxData implements ShouldQueue nlog("problem getting tax data => ".$e->getMessage()); } - /* - if(!$tax_provider->updatedTaxStatus() && $this->client->country_id == 840){ - - $calculated_state = false; - - if(array_key_exists($this->client->shipping_state, USStates::get())) { - $calculated_state = $this->client->shipping_state; - $calculated_postal_code = $this->client->shipping_postal_code; - $calculated_city = $this->client->shipping_city; - } - elseif(array_key_exists($this->client->state, USStates::get())){ - $calculated_state = $this->client->state; - $calculated_postal_code = $this->client->postal_code; - $calculated_city = $this->client->city; - } - else { - - try{ - $calculated_state = USStates::getState($this->client->shipping_postal_code); - $calculated_postal_code = $this->client->shipping_postal_code; - $calculated_city = $this->client->shipping_city; - } - catch(\Exception $e){ - nlog("could not calculate state from postal code => {$this->client->shipping_postal_code} or from state {$this->client->shipping_state}"); - } - - if(!$calculated_state) { - try { - $calculated_state = USStates::getState($this->client->postal_code); - $calculated_postal_code = $this->client->postal_code; - $calculated_city = $this->client->city; - } catch(\Exception $e) { - nlog("could not calculate state from postal code => {$this->client->postal_code} or from state {$this->client->state}"); - } - } - - if($this->company->tax_data?->seller_subregion) - $calculated_state = $this->company->tax_data?->seller_subregion; - - nlog("i am trying"); - - if(!$calculated_state) { - nlog("could not determine state"); - return; - } - - } - - $data = [ - 'seller_subregion' => $this->company->tax_data?->seller_subregion ?: '', - 'geoPostalCode' => $this->client->postal_code ?? '', - 'geoCity' => $this->client->city ?? '', - 'geoState' => $calculated_state, - 'taxSales' => $this->company->tax_data->regions->US->subregions?->{$calculated_state}?->taxSales ?? 0, - ]; - - $tax_data = new Response($data); - - $this->client->tax_data = $tax_data; - $this->client->saveQuietly(); - - } - */ } public function middleware() diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index 9598813b80f9..00b540bbc0a4 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -225,6 +225,8 @@ class TaxProvider */ private function configureEuTax(): self { + throw new \Exception("No tax region defined for this country"); + $this->provider = EuTax::class; return $this; From 574c800c2ea3eb52876258e3443cd1bb7e67f5fb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 07:46:45 +1100 Subject: [PATCH 30/38] v5.7.33 --- VERSION.txt | 2 +- app/Helpers/Epc/EpcQrGenerator.php | 14 ++++++++------ app/Http/Controllers/PreviewController.php | 9 +++++---- config/ninja.php | 4 ++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 5a3ee7ad5530..71c2c6e276ca 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.32 \ No newline at end of file +5.7.33 \ No newline at end of file diff --git a/app/Helpers/Epc/EpcQrGenerator.php b/app/Helpers/Epc/EpcQrGenerator.php index 342c5f775f06..965e2dad1f13 100644 --- a/app/Helpers/Epc/EpcQrGenerator.php +++ b/app/Helpers/Epc/EpcQrGenerator.php @@ -15,6 +15,7 @@ use App\Models\Company; use App\Models\Invoice; use App\Models\RecurringInvoice; use App\Utils\Ninja; +use BaconQrCode\Exception\InvalidArgumentException; use BaconQrCode\Renderer\Image\SvgImageBackEnd; use BaconQrCode\Renderer\ImageRenderer; use BaconQrCode\Renderer\RendererStyle\RendererStyle; @@ -50,8 +51,7 @@ class EpcQrGenerator ); $writer = new Writer($renderer); - if($this->validateFields()) - return ''; + $this->validateFields(); $qr = $writer->writeString($this->encodeMessage(), 'utf-8'); @@ -59,10 +59,15 @@ class EpcQrGenerator {$qr}"; } catch(\Throwable $e) { + nlog("EPC QR failure => ".$e->getMessage()); return ''; } catch(\Exception $e) { + nlog("EPC QR failure => ".$e->getMessage()); return ''; - } + } catch( InvalidArgumentException $e) { + nlog("EPC QR failure => ".$e->getMessage()); + return ''; + } } @@ -88,16 +93,13 @@ class EpcQrGenerator private function validateFields() { if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company2)) { - return true; nlog('The BIC field is not present and _may_ be a required fields for EPC QR codes'); } if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company1)) { - return true; nlog('The IBAN field is required'); } - return false; } private function formatMoney($value) diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index ad68dd371c70..8b5d499d8de8 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -111,11 +111,12 @@ class PreviewController extends BaseController $html->settings = $settings; $variables = $html->generateLabelsAndValues(); - $design = \App\Models\Design::withTrashed()->find($entity_obj->design_id ?? 2); + + $design = \App\Models\Design::query()->withTrashed()->find($entity_obj->design_id ?? 2); /* Catch all in case migration doesn't pass back a valid design */ if (! $design) { - $design = \App\Models\Design::find(2); + $design = \App\Models\Design::query()->find(2); } if ($design->is_custom) { @@ -329,8 +330,8 @@ class PreviewController extends BaseController /** * @deprecated due to usage of transactions * - * @param mixed $request - * @return void + * @param PreviewInvoiceRequest $request + * @return mixed */ public function livex(PreviewInvoiceRequest $request) { diff --git a/config/ninja.php b/config/ninja.php index 56b9f06de32f..d5f584da4e29 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.32'), - 'app_tag' => env('APP_TAG','5.7.32'), + 'app_version' => env('APP_VERSION','5.7.33'), + 'app_tag' => env('APP_TAG','5.7.33'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), From aca580780abbaf239fd17f36a474aa69de7c0adf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 09:22:33 +1100 Subject: [PATCH 31/38] Ensure order of Item exports --- app/Export/CSV/InvoiceItemExport.php | 16 +++++++++------- app/Export/CSV/PurchaseOrderItemExport.php | 1 + app/Export/CSV/QuoteItemExport.php | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 5ac3c9fb2fbf..ba2dc00da4f5 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -137,16 +137,16 @@ class InvoiceItemExport extends BaseExport if (str_contains($key, "item.")) { - $key = str_replace("item.", "", $key); + $tmp_key = str_replace("item.", "", $key); - if($key == 'type_id') - $key = 'type'; + if($tmp_key == 'type_id') + $tmp_key = 'type'; - if($key == 'tax_id') - $key = 'tax_category'; + if($tmp_key == 'tax_id') + $tmp_key = 'tax_category'; - if (property_exists($item, $key)) { - $item_array[$key] = $item->{$key}; + if (property_exists($item, $tmp_key)) { + $item_array[$key] = $item->{$tmp_key}; } else { $item_array[$key] = ''; @@ -156,6 +156,8 @@ class InvoiceItemExport extends BaseExport $transformed_items = array_merge($transformed_invoice, $item_array); $entity = $this->decorateAdvancedFields($invoice, $transformed_items); + + $entity = array_merge(array_flip(array_values($this->input['report_keys'])), $entity); $this->storage_array[] = $entity; diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index fb09ea6fa8e9..1ba5db7a12a6 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -147,6 +147,7 @@ class PurchaseOrderItemExport extends BaseExport $transformed_items = array_merge($transformed_purchase_order, $item_array); $entity = $this->decorateAdvancedFields($purchase_order, $transformed_items); + $entity = array_merge(array_flip(array_values($this->input['report_keys'])), $entity); $this->storage_array[] = $entity; } diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 48bc25b95e6a..5e327af612d8 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -152,6 +152,7 @@ class QuoteItemExport extends BaseExport $transformed_items = array_merge($transformed_quote, $item_array); $entity = $this->decorateAdvancedFields($quote, $transformed_items); + $entity = array_merge(array_flip(array_values($this->input['report_keys'])), $entity); $this->storage_array[] = $entity; } From e54dec8762f72eff1ef06d9947210be5cc6cf524 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 09:39:25 +1100 Subject: [PATCH 32/38] Fixes for exports --- app/Export/CSV/PurchaseOrderItemExport.php | 14 +++++++------- app/Export/CSV/QuoteItemExport.php | 14 +++++++------- app/Http/Requests/Client/StoreClientRequest.php | 2 -- .../GroupSetting/StoreGroupSettingRequest.php | 10 ++++++++-- tests/Feature/Export/ReportCsvGenerationTest.php | 3 +-- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 1ba5db7a12a6..3738ec5b6dd0 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -127,18 +127,18 @@ class PurchaseOrderItemExport extends BaseExport if (str_contains($key, "item.")) { - $key = str_replace("item.", "", $key); + $tmp_key = str_replace("item.", "", $key); - if($key == 'type_id') { - $keyval = 'type'; + if($tmp_key == 'type_id') { + $tmp_key = 'type'; } - if($key == 'tax_id') { - $keyval = 'tax_category'; + if($tmp_key == 'tax_id') { + $tmp_key = 'tax_category'; } - if (property_exists($item, $key)) { - $item_array[$key] = $item->{$key}; + if (property_exists($item, $tmp_key)) { + $item_array[$key] = $item->{$tmp_key}; } else { $item_array[$key] = ''; } diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 5e327af612d8..0db08629c664 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -133,16 +133,16 @@ class QuoteItemExport extends BaseExport if (str_contains($key, "item.")) { - $key = str_replace("item.", "", $key); + $tmp_key = str_replace("item.", "", $key); - if($key == 'type_id') - $key = 'type'; + if($tmp_key == 'type_id') + $tmp_key = 'type'; - if($key == 'tax_id') - $key = 'tax_category'; + if($tmp_key == 'tax_id') + $tmp_key = 'tax_category'; - if (property_exists($item, $key)) { - $item_array[$key] = $item->{$key}; + if (property_exists($item, $tmp_key)) { + $item_array[$key] = $item->{$tmp_key}; } else { $item_array[$key] = ''; diff --git a/app/Http/Requests/Client/StoreClientRequest.php b/app/Http/Requests/Client/StoreClientRequest.php index a8ae0fb5fbae..7262b6ef5d0b 100644 --- a/app/Http/Requests/Client/StoreClientRequest.php +++ b/app/Http/Requests/Client/StoreClientRequest.php @@ -180,8 +180,6 @@ class StoreClientRequest extends Request public function messages() { return [ - // 'unique' => ctrans('validation.unique', ['attribute' => ['email','number']), - //'required' => trans('validation.required', ['attribute' => 'email']), 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), 'currency_code' => 'Currency code does not exist', ]; diff --git a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php index ec629276068a..7099cc6317ae 100644 --- a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php @@ -26,12 +26,18 @@ class StoreGroupSettingRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('create', GroupSetting::class) && auth()->user()->account->hasFeature(Account::FEATURE_API); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('create', GroupSetting::class) && $user->account->hasFeature(Account::FEATURE_API); } public function rules() { - $rules['name'] = 'required|unique:group_settings,name,null,null,company_id,'.auth()->user()->companyId(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $rules['name'] = 'required|unique:group_settings,name,null,null,company_id,'.$user->companyId(); $rules['settings'] = new ValidClientGroupSettingsRule(); diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 87c967c883bf..90b12ab96adc 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -1171,7 +1171,7 @@ class ReportCsvGenerationTest extends TestCase public function testQuoteItemsCustomColumnsCsvGeneration() { - \App\Models\Quote::factory()->create([ + $q = \App\Models\Quote::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id, @@ -1217,7 +1217,6 @@ class ReportCsvGenerationTest extends TestCase $csv = $response->streamedContent(); - $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Item Quantity')); From 979036a27b66745f6672c5d28d4c96c4992466ac Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 10:34:17 +1100 Subject: [PATCH 33/38] Fixes for item exports --- app/Export/CSV/BaseExport.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index fd1c1b5a3ef4..84c92439569c 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -1154,9 +1154,9 @@ class BaseExport $clean_row[$key]['entity'] = $report_keys[0]; $clean_row[$key]['id'] = $report_keys[1] ?? $report_keys[0]; $clean_row[$key]['hashed_id'] = $report_keys[0] == $entity ? null : $resource->{$report_keys[0]}->hashed_id ?? null; - $clean_row[$key]['value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$report_keys[1]]; + $clean_row[$key]['value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$value]; $clean_row[$key]['identifier'] = $value; - $clean_row[$key]['display_value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$report_keys[1]]; + $clean_row[$key]['display_value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$value]; } From 238d6668e85eb372ade5bdbc37b17acfaa03980e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 16:30:11 +1100 Subject: [PATCH 34/38] Improvements to type casts --- app/DataMapper/Settings/SettingsData.php | 515 ++++++++++++++++++ .../GroupSetting/StoreGroupSettingRequest.php | 55 +- .../UpdateGroupSettingRequest.php | 15 +- tests/Feature/GroupSettingTest.php | 90 ++- tests/Unit/GroupSettingsTest.php | 4 + 5 files changed, 658 insertions(+), 21 deletions(-) create mode 100644 app/DataMapper/Settings/SettingsData.php diff --git a/app/DataMapper/Settings/SettingsData.php b/app/DataMapper/Settings/SettingsData.php new file mode 100644 index 000000000000..b76379806ff6 --- /dev/null +++ b/app/DataMapper/Settings/SettingsData.php @@ -0,0 +1,515 @@ + $value) { + + try{ + settype($object->{$key}, gettype($this->{$key})); + } + catch(\Exception | \Error | \Throwable $e){ + + if(property_exists($this, $key)) + $object->{$key} = $this->{$key}; + else + unset($object->{$key}); + + } + + // if(!property_exists($this, $key)) { + // unset($object->{$key}); + // } + // elseif(is_array($object->{$key}) && gettype($this->{$key} != 'array')){ + // $object->{$key} = $this->{$key}; + // } + // else { + // settype($object->{$key}, gettype($this->{$key})); + // } + } + } + $this->object = $object; + + return $this; + } + + public function toObject(): object + { + return (object)$this->object; + } + + public function toArray(): array + { + return (array)$this->object; + } +} \ No newline at end of file diff --git a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php index 7099cc6317ae..d35e7ac2bc78 100644 --- a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php @@ -11,11 +11,13 @@ namespace App\Http\Requests\GroupSetting; -use App\DataMapper\ClientSettings; -use App\Http\Requests\Request; -use App\Http\ValidationRules\ValidClientGroupSettingsRule; use App\Models\Account; use App\Models\GroupSetting; +use App\Http\Requests\Request; +use App\DataMapper\ClientSettings; +use App\DataMapper\CompanySettings; +use App\DataMapper\Settings\SettingsData; +use App\Http\ValidationRules\ValidClientGroupSettingsRule; class StoreGroupSettingRequest extends Request { @@ -48,15 +50,12 @@ class StoreGroupSettingRequest extends Request { $input = $this->all(); - $group_settings = ClientSettings::defaults(); - - if (array_key_exists('settings', $input) && ! empty($input['settings'])) { - foreach ($input['settings'] as $key => $value) { - $group_settings->{$key} = $value; - } + if (array_key_exists('settings', $input)) { + $input['settings'] = $this->filterSaveableSettings($input['settings']); + } + else { + $input['settings'] = (array)ClientSettings::defaults(); } - - $input['settings'] = (array)$group_settings; $this->replace($input); } @@ -67,4 +66,38 @@ class StoreGroupSettingRequest extends Request 'settings' => 'settings must be a valid json structure', ]; } + + /** + * For the hosted platform, we restrict the feature settings. + * + * This method will trim the company settings object + * down to the free plan setting properties which + * are saveable + * + * @param object $settings + * @return array $settings + */ + private function filterSaveableSettings($settings) + { + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $settings_data = new SettingsData(); + $settings = $settings_data->cast($settings)->toObject(); + + if (! $user->account->isFreeHostedClient()) { + return (array)$settings; + } + + $saveable_casts = CompanySettings::$free_plan_casts; + + foreach ($settings as $key => $value) { + if (! array_key_exists($key, $saveable_casts)) { + unset($settings->{$key}); + } + } + + return (array)$settings; + } + } diff --git a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php index 84120cc3475a..a264d74c018d 100644 --- a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php @@ -11,8 +11,9 @@ namespace App\Http\Requests\GroupSetting; -use App\DataMapper\CompanySettings; use App\Http\Requests\Request; +use App\DataMapper\CompanySettings; +use App\DataMapper\Settings\SettingsData; use App\Http\ValidationRules\ValidClientGroupSettingsRule; class UpdateGroupSettingRequest extends Request @@ -62,10 +63,14 @@ class UpdateGroupSettingRequest extends Request */ private function filterSaveableSettings($settings) { - $account = $this->group_setting->company->account; + /** @var \App\Models\User $user */ + $user = auth()->user(); - if (! $account->isFreeHostedClient()) { - return $settings; + $settings_data = new SettingsData(); + $settings = $settings_data->cast($settings)->toObject(); + + if (! $user->account->isFreeHostedClient()) { + return (array)$settings; } $saveable_casts = CompanySettings::$free_plan_casts; @@ -75,7 +80,7 @@ class UpdateGroupSettingRequest extends Request unset($settings->{$key}); } } - + return (array)$settings; } } diff --git a/tests/Feature/GroupSettingTest.php b/tests/Feature/GroupSettingTest.php index da637d39506c..98bad3daa703 100644 --- a/tests/Feature/GroupSettingTest.php +++ b/tests/Feature/GroupSettingTest.php @@ -11,20 +11,21 @@ namespace Tests\Feature; +use Tests\TestCase; +use Tests\MockAccountData; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; -use Tests\MockAccountData; -use Tests\TestCase; +use App\DataMapper\Settings\SettingsData; +use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; class GroupSettingTest extends TestCase { use MakesHash; - - //use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp(): void { parent::setUp(); @@ -38,6 +39,85 @@ class GroupSettingTest extends TestCase $this->makeTestData(); } + public function testCastingMagic() + { + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = ''; + $settings->tax_rate1 = 0; + $s = new SettingsData(); + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("", $settings->tax_name1); + $settings = null; + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = "1"; + $settings->tax_rate1 = 0; + + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("1", $settings->tax_name1); + + $settings = $s->cast($settings)->toArray(); + $this->assertEquals("1", $settings['tax_name1']); + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = []; + $settings->tax_rate1 = 0; + + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("", $settings->tax_name1); + + $settings = $s->cast($settings)->toArray(); + $this->assertEquals("", $settings['tax_name1']); + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = new \stdClass; + $settings->tax_rate1 = 0; + + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("", $settings->tax_name1); + + $settings = $s->cast($settings)->toArray(); + $this->assertEquals("", $settings['tax_name1']); + + + + // nlog(json_encode($settings)); + } + + public function testTaxNameInGroupFilters() + { + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = ''; + $settings->tax_rate1 = 0; + + $data = [ + 'name' => 'testX', + 'settings' => $settings, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/group_settings', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals("", (string)NULL); + $this->assertNotNull($arr['data']['settings']['tax_name1']); + } + public function testAddGroupFilters() { diff --git a/tests/Unit/GroupSettingsTest.php b/tests/Unit/GroupSettingsTest.php index 6dd5b662eba0..4874753e5fa2 100644 --- a/tests/Unit/GroupSettingsTest.php +++ b/tests/Unit/GroupSettingsTest.php @@ -28,6 +28,10 @@ class GroupSettingsTest extends TestCase use DatabaseTransactions; use ClientGroupSettingsSaver; + public $company_settings; + public $client_settings; + public $settings; + protected function setUp() :void { parent::setUp(); From 33c8c713b1099865a152102a99cf6bcd21692554 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 16:40:53 +1100 Subject: [PATCH 35/38] fixes for deleted payments being displayed on invoices with variable --- app/Services/Payment/DeletePayment.php | 6 +++++- app/Utils/HtmlEngine.php | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index 1e803faf10cd..8884dbdef622 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -109,7 +109,11 @@ class DeletePayment if ($paymentable_invoice->balance == $paymentable_invoice->amount) { $paymentable_invoice->service()->setStatus(Invoice::STATUS_SENT)->save(); - } else { + } + elseif($paymentable_invoice->balance == 0){ + $paymentable_invoice->service()->setStatus(Invoice::STATUS_PAID)->save(); + } + else { $paymentable_invoice->service()->setStatus(Invoice::STATUS_PARTIAL)->save(); } } else { diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 538b40b1d311..01d2d7d6fe43 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -670,16 +670,16 @@ class HtmlEngine $data['$payment.transaction_reference'] = ['value' => '', 'label' => ctrans('texts.transaction_reference')]; - if ($this->entity_string == 'invoice' && $this->entity->payments()->exists()) { + if ($this->entity_string == 'invoice' && $this->entity->net_payments()->exists()) { $payment_list = '

'; - foreach ($this->entity->payments as $payment) { + foreach ($this->entity->net_payments as $payment) { $payment_list .= ctrans('texts.payment_subject') . ": " . $this->formatDate($payment->date, $this->client->date_format()) . " :: " . Number::formatMoney($payment->amount, $this->client) ." :: ". GatewayType::getAlias($payment->gateway_type_id) . "
"; } $data['$payments'] = ['value' => $payment_list, 'label' => ctrans('texts.payments')]; - $payment = $this->entity->payments()->first(); + $payment = $this->entity->net_payments()->first(); $data['$payment.custom1'] = ['value' => $payment->custom_value1, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment1')]; $data['$payment.custom2'] = ['value' => $payment->custom_value2, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment2')]; From 28f3de3f284498f4ce3e13bc0d293d2edea30194 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 20 Oct 2023 21:36:45 +1100 Subject: [PATCH 36/38] Update postmark to only update existing records with message IDs --- app/Jobs/PostMark/ProcessPostmarkWebhook.php | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php index 18dde45e430c..8e4429ca148d 100644 --- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -56,6 +56,23 @@ class ProcessPostmarkWebhook implements ShouldQueue { } + private function getSystemLog(string $message_id): ?SystemLog + { + return SystemLog::query() + ->where('company_id', $this->invitation->company_id) + ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE) + ->whereJsonContains('log', ['MessageID' => $message_id]) + ->orderBy('id','desc') + ->first(); + + } + + private function updateSystemLog(SystemLog $system_log, array $data): void + { + $system_log->log = $data; + $system_log->save(); + } + /** * Execute the job. * @@ -135,6 +152,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl){ + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger( $data, SystemLog::CATEGORY_MAIL, @@ -166,6 +190,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger( $data, SystemLog::CATEGORY_MAIL, @@ -217,6 +248,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); // if(config('ninja.notification.slack')) @@ -263,6 +301,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); if (config('ninja.notification.slack')) { From 2e9a45c3bd852795aa1b31ff15d83cf7870b64b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 20 Oct 2023 18:57:13 +0200 Subject: [PATCH 37/38] Fixes for client identifier --- app/Export/CSV/ClientExport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index e99434b19863..16f9ca154d70 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -189,7 +189,7 @@ class ClientExport extends BaseExport $clean_row[$key]['id'] = $report_keys[1] ?? $report_keys[0]; $clean_row[$key]['hashed_id'] = $report_keys[0] == 'client' ? null : $resource->{$report_keys[0]}->hashed_id ?? null; $clean_row[$key]['value'] = $row[$column_key]; - $clean_row[$key]['identifier'] = $key; + $clean_row[$key]['identifier'] = $value; if(in_array($clean_row[$key]['id'], ['paid_to_date', 'balance', 'credit_balance','payment_balance'])) $clean_row[$key]['display_value'] = Number::formatMoney($row[$column_key], $resource); From 4a539988f1c67a109d61e2bcb8c3722f7a734d04 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 21 Oct 2023 07:50:58 +1100 Subject: [PATCH 38/38] Add client.number to maps --- app/Export/CSV/BaseExport.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 84c92439569c..fa6ab80ef33f 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -88,6 +88,7 @@ class BaseExport protected array $client_report_keys = [ "name" => "client.name", + "number" => "client.number", "user" => "client.user", "assigned_user" => "client.assigned_user", "balance" => "client.balance",