From 9641d88b145cc2752b699f6391dfe23913a00653 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 08:31:07 +1100 Subject: [PATCH 1/9] Remove .htaccess from update script --- CONTRIBUTING.md | 119 ++++++++++++++++++ app/Http/Controllers/BaseController.php | 22 ++++ app/Http/Controllers/SelfUpdateController.php | 2 + app/Models/Company.php | 5 + app/Transformers/CompanyTransformer.php | 11 ++ 5 files changed, 159 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..03f589a43a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,119 @@ +# Contributing to CONTRIBUTING.md + +First off, thanks for taking the time to contribute! + +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. + +> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: +> - Star the project +> - Tweet about it +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your friends/colleagues + + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Enhancements](#suggesting-enhancements) +- [Your First Code Contribution](#your-first-code-contribution) +- [Improving The Documentation](#improving-the-documentation) +- [Styleguides](#styleguides) +- [Commit Messages](#commit-messages) +- [Join The Project Team](#join-the-project-team) + + +## Code of Conduct + +This project and everyone participating in it is governed by the +[CONTRIBUTING.md Code of Conduct](blob/v5-stable/CODE_OF_CONDUCT.md). +By participating, you are expected to uphold this code. Please report unacceptable behavior +to <>. + + +## I Have a Question + +> If you want to ask a question, we assume that you have read the available [Documentation](https://invoiceninja.github.io), Searched the [Forum](https://forum.invoiceninja.com), or tried chatting with us on [Slack](https://invoiceninja.slack.com) [generate a slack invite here](http://slack.invoiceninja.com) + +Please reserve issues for bugs, general questions posted in the issues section will be closed and redirected to other support venues. + +## I Want To Contribute + + +> ### Legal Notice +> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. Please note that you'll need to sign the CLA for any PRs to be accepted + +### Reporting Bugs + + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://invoiceninja.github.io). If you are looking for support, you might want to check [this section](#i-have-a-question)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](issues?q=label%3Abug). +- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. +- Collect information about the bug: +- Stack trace (Traceback) +- OS, Platform and Version (Windows, Linux, macOS, x86, ARM) +- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. +- Possibly your input and the output +- Can you reliably reproduce the issue? And can you also reproduce it with older versions? + + +#### How Do I Submit a Good Bug Report? + +> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <>. + + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [Issue](/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. +- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). + + + + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for CONTRIBUTING.md, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. + + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration. +- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. + + +#### How Do I Submit a Good Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](/issues). + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +- **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration. + + + +### Your First Code Contribution + +All PRs should be created against the v5-develop branch. + + +## Attribution +This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)! diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 6eb9920a6b..b7db539320 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -109,6 +109,7 @@ class BaseController extends Controller 'company.bank_integrations', 'company.bank_transactions', 'company.bank_transaction_rules', + 'company.schedulers', ]; private $mini_load = [ @@ -128,6 +129,7 @@ class BaseController extends Controller 'company.subscriptions', 'company.bank_integrations', 'company.bank_transaction_rules', + 'company.schedulers', ]; public function __construct() @@ -465,6 +467,13 @@ class BaseController extends Controller $query->where('bank_transaction_rules.user_id', $user->id); } }, + 'company.schedulers'=> function ($query) use ($updated_at, $user) { + $query->where('updated_at', '>=', $updated_at); + + if (! $user->isAdmin()) { + $query->where('schedulers.user_id', $user->id); + } + }, ] ); @@ -545,6 +554,12 @@ class BaseController extends Controller $query->where('bank_transaction_rules.user_id', $user->id); } }, + 'company.schedulers'=> function ($query) use ($user) { + + if (! $user->isAdmin()) { + $query->where('schedulers.user_id', $user->id); + } + }, ] ); @@ -803,6 +818,13 @@ class BaseController extends Controller $query->where('bank_transactions.user_id', $user->id); } }, + 'company.schedulers'=> function ($query) use ($created_at, $user) { + $query->where('created_at', '>=', $created_at); + + if (! $user->isAdmin()) { + $query->where('schedulers.user_id', $user->id); + } + }, ] ); diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index c5738e692a..506c7a9156 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -108,6 +108,8 @@ class SelfUpdateController extends BaseController $zipFile->openFile($file); + $zipFile->deleteFromName(".htaccess"); + $zipFile->extractTo(base_path()); $zipFile->close(); diff --git a/app/Models/Company.php b/app/Models/Company.php index 0d9d679389..ac80e2b52f 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -172,6 +172,11 @@ class Company extends BaseModel return $this->morphMany(Document::class, 'documentable'); } + public function schedulers() + { + return $this->hasMany(Scheduler::class); + } + public function all_documents() { return $this->hasMany(Document::class); diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 77bcae0fa4..179c4bcd91 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -35,6 +35,7 @@ use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; +use App\Models\Scheduler; use App\Models\Subscription; use App\Models\SystemLog; use App\Models\Task; @@ -106,6 +107,7 @@ class CompanyTransformer extends EntityTransformer 'bank_integrations', 'bank_transactions', 'bank_transaction_rules', + 'task_schedulers', ]; /** @@ -233,6 +235,7 @@ class CompanyTransformer extends EntityTransformer return $this->includeCollection($company->tokens, $transformer, CompanyToken::class); } + public function includeBankTransactions(Company $company) { $transformer = new BankTransactionTransformer($this->serializer); @@ -241,6 +244,14 @@ class CompanyTransformer extends EntityTransformer } + public function includeTaskSchedulers(Company $company) + { + $transformer = new SchedulerTransformer($this->serializer); + + return $this->includeCollection($company->schedulers, $transformer, Scheduler::class); + } + + public function includeBankTransactionRules(Company $company) { $transformer = new BankTransactionRuleTransformer($this->serializer); From f84a3099644b66b41869cda5a262a5177f9bd18e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 08:37:15 +1100 Subject: [PATCH 2/9] Add task schedulers to login / refresh routes --- app/Http/Controllers/BaseController.php | 10 +++++----- app/Models/Company.php | 5 +++++ app/Transformers/CompanyTransformer.php | 7 +++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index b7db539320..3403d0e212 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -109,7 +109,7 @@ class BaseController extends Controller 'company.bank_integrations', 'company.bank_transactions', 'company.bank_transaction_rules', - 'company.schedulers', + 'company.task_schedulers', ]; private $mini_load = [ @@ -129,7 +129,7 @@ class BaseController extends Controller 'company.subscriptions', 'company.bank_integrations', 'company.bank_transaction_rules', - 'company.schedulers', + 'company.task_schedulers', ]; public function __construct() @@ -467,7 +467,7 @@ class BaseController extends Controller $query->where('bank_transaction_rules.user_id', $user->id); } }, - 'company.schedulers'=> function ($query) use ($updated_at, $user) { + 'company.task_schedulers'=> function ($query) use ($updated_at, $user) { $query->where('updated_at', '>=', $updated_at); if (! $user->isAdmin()) { @@ -554,7 +554,7 @@ class BaseController extends Controller $query->where('bank_transaction_rules.user_id', $user->id); } }, - 'company.schedulers'=> function ($query) use ($user) { + 'company.task_schedulers'=> function ($query) use ($user) { if (! $user->isAdmin()) { $query->where('schedulers.user_id', $user->id); @@ -818,7 +818,7 @@ class BaseController extends Controller $query->where('bank_transactions.user_id', $user->id); } }, - 'company.schedulers'=> function ($query) use ($created_at, $user) { + 'company.task_schedulers'=> function ($query) use ($created_at, $user) { $query->where('created_at', '>=', $created_at); if (! $user->isAdmin()) { diff --git a/app/Models/Company.php b/app/Models/Company.php index ac80e2b52f..0afac617a2 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -177,6 +177,11 @@ class Company extends BaseModel return $this->hasMany(Scheduler::class); } + public function task_schedulers() //alias for schedulers + { + return $this->hasMany(Scheduler::class); + } + public function all_documents() { return $this->hasMany(Document::class); diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 179c4bcd91..9027184560 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -108,6 +108,7 @@ class CompanyTransformer extends EntityTransformer 'bank_transactions', 'bank_transaction_rules', 'task_schedulers', + 'schedulers', ]; /** @@ -251,6 +252,12 @@ class CompanyTransformer extends EntityTransformer return $this->includeCollection($company->schedulers, $transformer, Scheduler::class); } + public function includeSchedulers(Company $company) + { + $transformer = new SchedulerTransformer($this->serializer); + + return $this->includeCollection($company->schedulers, $transformer, Scheduler::class); + } public function includeBankTransactionRules(Company $company) { From 67dfe8a4b99233e7df377fc01032f69ab6a94d4d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 09:43:11 +1100 Subject: [PATCH 3/9] Show terms/footers on Recurring Invoice Preview --- app/DataMapper/CompanySettings.php | 4 ++ app/Http/Controllers/PreviewController.php | 10 ++++- app/Mail/Engine/PaymentEmailEngine.php | 6 ++- app/Utils/HtmlEngine.php | 40 ++++++++++++++----- app/Utils/VendorHtmlEngine.php | 5 +++ .../views/email/template/client.blade.php | 2 +- 6 files changed, 52 insertions(+), 15 deletions(-) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 6e65ccd3a8..97459a8e5c 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -451,7 +451,11 @@ class CompanySettings extends BaseSettings public $email_alignment = 'center'; // center , left, right + public $show_email_footer = true; + + public static $casts = [ + 'show_email_footer' => 'bool', 'email_alignment' => 'string', 'auto_bill_standard_invoices' => 'bool', 'postmark_secret' => 'string', diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 03d49d7c5a..2b65e6b615 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -214,6 +214,12 @@ class PreviewController extends BaseController ->first(); } + if($request->has('footer') && !$request->filled('footer') && $request->input('entity') == 'recurring_invoice') + $request->merge(['footer' => $company->settings->invoice_footer]); + + if($request->has('terms') && !$request->filled('terms') && $request->input('entity') == 'recurring_invoice') + $request->merge(['terms' => $company->settings->invoice_terms]); + $entity_obj = $repo->save($request->all(), $entity_obj); if (! $request->has('entity_id')) { @@ -404,8 +410,8 @@ class PreviewController extends BaseController 'user_id' => auth()->user()->id, 'company_id' => auth()->user()->company()->id, 'client_id' => $client->id, - 'terms' => 'Sample Terms', - 'footer' => 'Sample Footer', + 'terms' => auth()->user()->company()->settings->invoice_terms, + 'footer' => auth()->user()->company()->settings->invoice_footer, 'public_notes' => 'Sample Public Notes', ]); diff --git a/app/Mail/Engine/PaymentEmailEngine.php b/app/Mail/Engine/PaymentEmailEngine.php index 608eca23de..5c4cdc7be6 100644 --- a/app/Mail/Engine/PaymentEmailEngine.php +++ b/app/Mail/Engine/PaymentEmailEngine.php @@ -156,7 +156,11 @@ class PaymentEmailEngine extends BaseEmailEngine $data['$payment2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment2', $this->payment->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment2')]; $data['$payment3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment3', $this->payment->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment3')]; $data['$payment4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'payment4', $this->payment->custom_value4, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment4')]; - // $data['$type'] = ['value' => $this->payment->type->name ?: '', 'label' => ctrans('texts.payment_type')]; + + $data['$custom1'] = &$data['$payment1']; + $data['$custom2'] = &$data['$payment2']; + $data['$custom3'] = &$data['$payment3']; + $data['$custom4'] = &$data['$payment4']; $data['$client1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'client1', $this->client->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'client1')]; $data['$client2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'client2', $this->client->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'client2')]; diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index be7d361201..51093b6f40 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -165,6 +165,16 @@ class HtmlEngine $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_invoice')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: ' ', 'label' => ctrans('texts.invoice_date')]; + $data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')]; + $data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')]; + $data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; + $data['$invoice.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; + + $data['$custom1'] = &$data['$invoice.custom1']; + $data['$custom2'] = &$data['$invoice.custom2']; + $data['$custom3'] = &$data['$invoice.custom3']; + $data['$custom4'] = &$data['$invoice.custom4']; + if($this->entity->project) { $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project')]; $data['$invoice.project'] = &$data['$project.name']; @@ -204,6 +214,16 @@ class HtmlEngine $data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_quote')]; $data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: ' ', 'label' => ctrans('texts.quote_date')]; + $data['$quote.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')]; + $data['$quote.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')]; + $data['$quote.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; + $data['$quote.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; + + $data['$custom1'] = &$data['$quote.custom1']; + $data['$custom2'] = &$data['$quote.custom2']; + $data['$custom3'] = &$data['$quote.custom3']; + $data['$custom4'] = &$data['$quote.custom4']; + if($this->entity->project) { $data['$project.name'] = ['value' => $this->entity->project->name, 'label' => ctrans('texts.project_name')]; $data['$invoice.project'] = &$data['$project.name']; @@ -234,6 +254,11 @@ class HtmlEngine $data['$credit.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; $data['$credit.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit4', $this->entity->custom_value4, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; + $data['$custom1'] = &$data['$credit.custom1']; + $data['$custom2'] = &$data['$credit.custom2']; + $data['$custom3'] = &$data['$credit.custom3']; + $data['$custom4'] = &$data['$credit.custom4']; + } $data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>'']; @@ -251,6 +276,7 @@ class HtmlEngine $data['$invoice.subtotal'] = &$data['$subtotal']; + /* Do not change the order of these */ if ($this->entity->partial > 0) { $data['$balance_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: ' ', 'label' => ctrans('texts.partial_due')]; $data['$balance_due_raw'] = ['value' => $this->entity->partial, 'label' => ctrans('texts.partial_due')]; @@ -287,10 +313,8 @@ class HtmlEngine $data['$amount_raw'] = ['value' => $this->entity->amount, 'label' => ctrans('texts.amount')]; } - // $data['$amount_in_words'] = ['value' => (new \NumberFormatter($this->client->locale(), \NumberFormatter::SPELLOUT))->format($this->entity->amount), 'label' => '']; - // $data['$balance_in_words'] = ['value' => (new \NumberFormatter($this->client->locale(), \NumberFormatter::SPELLOUT))->format($this->entity->balance), 'label' => '']; + /* Do not change the order of these */ - // $data['$balance_due'] = $data['$balance_due']; $data['$outstanding'] = &$data['$balance_due']; $data['$partial_due'] = ['value' => Number::formatMoney($this->entity->partial, $this->client) ?: ' ', 'label' => ctrans('texts.partial_due')]; $data['$partial'] = &$data['$partial_due']; @@ -323,10 +347,7 @@ class HtmlEngine $data['$user_iban'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')]; - $data['$invoice.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')]; - $data['$invoice.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')]; - $data['$invoice.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; - $data['$invoice.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; + $data['$invoice.public_notes'] = ['value' => Helpers::processReservedKeywords(\nl2br($this->entity->public_notes ?: ''), $this->client) ?: '', 'label' => ctrans('texts.public_notes')]; $data['$entity.public_notes'] = &$data['$invoice.public_notes']; $data['$public_notes'] = &$data['$invoice.public_notes']; @@ -345,10 +366,7 @@ class HtmlEngine $data['$valid_until'] = &$data['$quote.valid_until']; $data['$credit_amount'] = ['value' => Number::formatMoney($this->entity_calc->getTotal(), $this->client) ?: ' ', 'label' => ctrans('texts.credit_amount')]; $data['$credit_balance'] = ['value' => Number::formatMoney($this->entity->balance, $this->client) ?: ' ', 'label' => ctrans('texts.credit_balance')]; - $data['$quote.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice1', $this->entity->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')]; - $data['$quote.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice2', $this->entity->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')]; - $data['$quote.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; - $data['$quote.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; + $data['$credit_number'] = &$data['$number']; $data['$credit_no'] = &$data['$number']; diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index b32e447e6c..15eb1c2703 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -228,6 +228,11 @@ class VendorHtmlEngine $data['$purchase_order.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice3', $this->entity->custom_value3, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')]; $data['$purchase_order.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'invoice4', $this->entity->custom_value4, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')]; + $data['$custom1'] = &$data['$purchase_order.custom1']; + $data['$custom2'] = &$data['$purchase_order.custom2']; + $data['$custom3'] = &$data['$purchase_order.custom3']; + $data['$custom4'] = &$data['$purchase_order.custom4']; + $data['$vendor1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor1', $this->vendor->custom_value1, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor1')]; $data['$vendor2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor2', $this->vendor->custom_value2, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor2')]; $data['$vendor3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'vendor3', $this->vendor->custom_value3, $this->company) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'vendor3')]; diff --git a/resources/views/email/template/client.blade.php b/resources/views/email/template/client.blade.php index 5fcf2b9bdc..e0c07b1287 100644 --- a/resources/views/email/template/client.blade.php +++ b/resources/views/email/template/client.blade.php @@ -180,7 +180,7 @@

@endisset - @if(isset($company) && $company instanceof \App\Models\Company) + @if(isset($company) && $company instanceof \App\Models\Company && $company->getSetting('show_email_footer'))

{{ $company->present()->name() }}

From c51dd313b9d7b87c2390bdd3c8bf37c73cfe55a8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 11:00:12 +1100 Subject: [PATCH 4/9] Tests for emailing client statements --- app/DataMapper/BaseSettings.php | 12 +-- app/DataMapper/CompanySettings.php | 3 +- .../Controllers/ClientStatementController.php | 12 ++- app/Http/Controllers/PreviewController.php | 4 - app/Models/BaseModel.php | 23 ++--- app/Services/Client/ClientService.php | 95 ++++++++++++++++--- app/Services/Client/Statement.php | 15 +-- app/Services/Scheduler/SchedulerService.php | 36 +------ app/Utils/HtmlEngine.php | 2 - lang/en/texts.php | 3 +- tests/Feature/ClientApiTest.php | 58 +++++++++++ 11 files changed, 172 insertions(+), 91 deletions(-) diff --git a/app/DataMapper/BaseSettings.php b/app/DataMapper/BaseSettings.php index d97304ee4a..2a67d68627 100644 --- a/app/DataMapper/BaseSettings.php +++ b/app/DataMapper/BaseSettings.php @@ -12,15 +12,16 @@ namespace App\DataMapper; /** - * ClientSettings. + * BaseSettings. */ class BaseSettings { + //@deprecated public function __construct($obj) { - foreach ($obj as $key => $value) { - $obj->{$key} = $value; - } + // foreach ($obj as $key => $value) { + // $obj->{$key} = $value; + // } } public static function setCasts($obj, $casts) @@ -57,7 +58,4 @@ class BaseSettings } } - public static function castSingleAttribute($key, $data) - { - } } diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 97459a8e5c..e65eedfa9e 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -734,8 +734,9 @@ class CompanySettings extends BaseSettings * and always ensure an up to date class is returned. * * @param $obj + * @deprecated */ - public function __construct($obj) + public function __construct() { // parent::__construct($obj); } diff --git a/app/Http/Controllers/ClientStatementController.php b/app/Http/Controllers/ClientStatementController.php index 052ddc91a9..1e4db875f9 100644 --- a/app/Http/Controllers/ClientStatementController.php +++ b/app/Http/Controllers/ClientStatementController.php @@ -109,16 +109,24 @@ class ClientStatementController extends BaseController */ public function statement(CreateStatementRequest $request) { + $send_email = false; + + if($request->has('send_email') && $request->send_email == 'true') + $send_email = true; + $pdf = $request->client()->service()->statement( - $request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table', 'status']) + $request->only(['start_date', 'end_date', 'show_payments_table', 'show_aging_table', 'status']), $send_email ); + if($send_email) + return response()->json(['message' => ctrans('texts.email_queued')], 200); + if ($pdf) { return response()->streamDownload(function () use ($pdf) { echo $pdf; }, ctrans('texts.statement').'.pdf', ['Content-Type' => 'application/pdf']); } - return response()->json(['message' => 'Something went wrong. Please check logs.']); + return response()->json(['message' => ctrans('texts.error_title')], 500); } } diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 2b65e6b615..46c25b79f7 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -16,7 +16,6 @@ use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\QuoteFactory; use App\Factory\RecurringInvoiceFactory; -use App\Http\Requests\Invoice\StoreInvoiceRequest; use App\Http\Requests\Preview\PreviewInvoiceRequest; use App\Jobs\Util\PreviewPdf; use App\Libraries\MultiDB; @@ -44,7 +43,6 @@ 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\Lang; use Illuminate\Support\Facades\Response; use Turbo124\Beacon\Facades\LightLogs; @@ -109,8 +107,6 @@ class PreviewController extends BaseController $class = "App\Models\\$entity"; - $pdf_class = "App\Jobs\\$entity\\Create{$entity}Pdf"; - $entity_obj = $class::whereId($this->decodePrimaryKey(request()->input('entity_id')))->company()->first(); if (! $entity_obj) { diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index fdd51dff88..11b8becb2b 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -12,15 +12,12 @@ namespace App\Models; use App\DataMapper\ClientSettings; -use App\DataMapper\CompanySettings; -use App\Jobs\Entity\CreateEntityPdf; use App\Utils\Traits\MakesHash; use App\Utils\Traits\UserSessionAttributes; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; @@ -115,17 +112,17 @@ class BaseModel extends Model * reference to the parent class. * * @param $key The key of property - * @return + * @deprecated */ - public function getSettingsByKey($key) - { - /* Does Setting Exist @ client level */ - if (isset($this->getSettings()->{$key})) { - return $this->getSettings()->{$key}; - } else { - return (new CompanySettings($this->company->settings))->{$key}; - } - } + // public function getSettingsByKey($key) + // { + // /* Does Setting Exist @ client level */ + // if (isset($this->getSettings()->{$key})) { + // return $this->getSettings()->{$key}; + // } else { + // return (new CompanySettings($this->company->settings))->{$key}; + // } + // } public function setSettingsByEntity($entity, $settings) { diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index e60fee96e2..3fb7b920f0 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -15,23 +15,28 @@ use App\Models\Client; use App\Models\Credit; use App\Services\Client\Merge; use App\Services\Client\PaymentMethod; +use App\Services\Email\EmailObject; +use App\Services\Email\EmailService; use App\Utils\Number; -use Illuminate\Database\Eloquent\Collection; +use App\Utils\Traits\MakesDates; +use Illuminate\Mail\Mailables\Address; +use Illuminate\Support\Facades\DB; class ClientService { - private $client; + use MakesDates; - public function __construct(Client $client) - { - $this->client = $client; - } + private string $client_start_date; + + private string $client_end_date; + + public function __construct(private Client $client){} public function updateBalance(float $amount) { try { - \DB::connection(config('database.default'))->transaction(function () use($amount) { + DB::connection(config('database.default'))->transaction(function () use($amount) { $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client->balance += $amount; @@ -51,7 +56,7 @@ class ClientService { try { - \DB::connection(config('database.default'))->transaction(function () use($balance, $paid_to_date) { + DB::connection(config('database.default'))->transaction(function () use($balance, $paid_to_date) { $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client->balance += $balance; @@ -64,16 +69,14 @@ class ClientService nlog("DB ERROR " . $throwable->getMessage()); } - - return $this; + } public function updatePaidToDate(float $amount) { - // $this->client->paid_to_date += $amount; - \DB::connection(config('database.default'))->transaction(function () use($amount) { + DB::connection(config('database.default'))->transaction(function () use($amount) { $this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first(); $this->client->paid_to_date += $amount; @@ -82,17 +85,21 @@ class ClientService }, 1); return $this; + } public function adjustCreditBalance(float $amount) { + $this->client->credit_balance += $amount; return $this; + } public function getCreditBalance() :float { + $credits = Credit::withTrashed()->where('client_id', $this->client->id) ->where('is_deleted', false) ->where(function ($query) { @@ -102,6 +109,7 @@ class ClientService ->orderBy('created_at', 'ASC'); return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision); + } public function getCredits() @@ -132,12 +140,71 @@ class ClientService * Generate the client statement. * * @param array $options + * @param bool $send_email determines if we should send this statement direct to the client */ - public function statement(array $options = []) + public function statement(array $options = [], bool $send_email = false) { - return (new Statement($this->client, $options))->run(); + $statement = (new Statement($this->client, $options)); + + $pdf = $statement->run(); + + if($send_email) + return $this->emailStatement($pdf, $statement->options); + + return $pdf; } + /** + * Emails the statement to the client + * + * @param mixed $pdf The pdf blob + * @param array $options The statement options array + * @return void + */ + private function emailStatement($pdf, array $options): void + { + + $this->client_start_date = $this->translateDate($options['start_date'], $this->client->date_format(), $this->client->locale()); + $this->client_end_date = $this->translateDate($options['end_date'], $this->client->date_format(), $this->client->locale()); + + $email_service = new EmailService($this->buildStatementMailableData($pdf), $this->client->company); + + $email_service->send(); + + } + + /** + * Builds and returns an EmailObject for Client Statements + * + * @param mixed $pdf The PDF to send + * @return EmailObject The EmailObject to send + */ + public function buildStatementMailableData($pdf) :EmailObject + { + + $email_object = new EmailObject; + $email_object->to = [new Address($this->client->present()->email(), $this->client->present()->name())]; + $email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.statement') . ".pdf"]]; + $email_object->settings = $this->client->getMergedSettings(); + $email_object->company = $this->client->company; + $email_object->client = $this->client; + $email_object->email_template_subject = 'email_subject_statement'; + $email_object->email_template_body = 'email_template_statement'; + $email_object->variables = [ + '$client' => $this->client->present()->name(), + '$start_date' => $this->client_start_date, + '$end_date' => $this->client_end_date, + ]; + + return $email_object; + + } + + /** + * Saves the client instance + * + * @return Client The Client Model + */ public function save() :Client { $this->client->save(); diff --git a/app/Services/Client/Statement.php b/app/Services/Client/Statement.php index 2a91f7096f..9b0d52e84a 100644 --- a/app/Services/Client/Statement.php +++ b/app/Services/Client/Statement.php @@ -32,25 +32,16 @@ class Statement { use PdfMakerTrait; - protected Client $client; - /** * @var Invoice|Payment|null */ protected $entity; - protected array $options; - protected bool $rollback = false; - public function __construct(Client $client, array $options) - { - $this->client = $client; + public function __construct(protected Client $client, public array $options){} - $this->options = $options; - } - - public function run(): ?string + public function run() :?string { $this ->setupOptions() @@ -110,7 +101,7 @@ class Statement $maker = null; $state = null; - + return $pdf; } diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index d8b1612e55..f8c507ee9f 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -11,18 +11,10 @@ namespace App\Services\Scheduler; -use App\DataMapper\EmailTemplateDefaults; -use App\Mail\Client\ClientStatement; use App\Models\Client; use App\Models\Scheduler; -use App\Services\Email\EmailMailable; -use App\Services\Email\EmailObject; -use App\Services\Email\EmailService; -use App\Utils\Ninja; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; -use Illuminate\Mail\Mailables\Address; -use Illuminate\Support\Str; class SchedulerService { @@ -54,7 +46,6 @@ class SchedulerService if(count($this->scheduler->parameters['clients']) >= 1) $query->where('id', $this->transformKeys($this->scheduler->parameters['clients'])); - $query->cursor() ->each(function ($_client){ @@ -62,10 +53,7 @@ class SchedulerService $statement_properties = $this->calculateStatementProperties(); //work out the date range - $pdf = $_client->service()->statement($statement_properties); - - $email_service = new EmailService($this->buildMailableData($pdf), $_client->company); - $email_service->send(); + $pdf = $_client->service()->statement($statement_properties,true); //calculate next run dates; @@ -77,9 +65,6 @@ class SchedulerService { $start_end = $this->calculateStartAndEndDates(); - $this->client_start_date = $this->translateDate($start_end[0], $this->client->date_format(), $this->client->locale()); - $this->client_end_date = $this->translateDate($start_end[1], $this->client->date_format(), $this->client->locale()); - return [ 'start_date' =>$start_end[0], 'end_date' =>$start_end[1], @@ -104,26 +89,7 @@ class SchedulerService }; } - private function buildMailableData($pdf) - { - $email_object = new EmailObject; - $email_object->to = [new Address($this->client->present()->email(), $this->client->present()->name())]; - $email_object->attachments = [['file' => base64_encode($pdf), 'name' => ctrans('texts.statement') . ".pdf"]]; - $email_object->settings = $this->client->getMergedSettings(); - $email_object->company = $this->client->company; - $email_object->client = $this->client; - $email_object->email_template_subject = 'email_subject_statement'; - $email_object->email_template_body = 'email_template_statement'; - $email_object->variables = [ - '$client' => $this->client->present()->name(), - '$start_date' => $this->client_start_date, - '$end_date' => $this->client_end_date, - ]; - - return $email_object; - - } } \ No newline at end of file diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 51093b6f40..d85c5b5324 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -20,12 +20,10 @@ use App\Models\GatewayType; use App\Models\InvoiceInvitation; use App\Models\QuoteInvitation; use App\Models\RecurringInvoiceInvitation; -use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; use App\Utils\Ninja; use App\Utils\Number; use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesDates; -use App\Utils\transformTranslations; use Exception; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; diff --git a/lang/en/texts.php b/lang/en/texts.php index f983eaf023..bb2ad322e4 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4924,7 +4924,8 @@ $LANG = array( 'action_add_to_invoice' => 'Add To Invoice', 'danger_zone' => 'Danger Zone', 'import_completed' => 'Import completed', - 'client_statement_body' => 'Your statement from :start_date to :end_date is attached.' + 'client_statement_body' => 'Your statement from :start_date to :end_date is attached.', + 'email_queued' => 'Email queued', ); diff --git a/tests/Feature/ClientApiTest.php b/tests/Feature/ClientApiTest.php index 40b11fc70a..62dde8293c 100644 --- a/tests/Feature/ClientApiTest.php +++ b/tests/Feature/ClientApiTest.php @@ -53,6 +53,64 @@ class ClientApiTest extends TestCase Model::reguard(); } + public function testClientStatement() + { + + $response = null; + + $data = [ + 'client_id' => $this->client->hashed_id, + 'start_date' => '2000-01-01', + 'end_date' => '2023-01-01', + 'show_aging_table' => true, + 'show_payments_table' => true, + 'status' => 'paid', + ]; + + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/client_statement', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + } + + public function testClientStatementEmail() + { + + $response = null; + + $data = [ + 'client_id' => $this->client->hashed_id, + 'start_date' => '2000-01-01', + 'end_date' => '2023-01-01', + 'show_aging_table' => true, + 'show_payments_table' => true, + 'status' => 'paid', + ]; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/client_statement?send_email=true', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + } + + public function testCsvImportRepositoryPersistance() { Client::unguard(); From b387528a4ac0193e16344a2437360dec9ef41500 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 11:05:25 +1100 Subject: [PATCH 5/9] Fixes for tests --- tests/Feature/ClientApiTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Feature/ClientApiTest.php b/tests/Feature/ClientApiTest.php index 62dde8293c..45813aa56d 100644 --- a/tests/Feature/ClientApiTest.php +++ b/tests/Feature/ClientApiTest.php @@ -72,7 +72,7 @@ class ClientApiTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->post('/api/v1/client_statement', $data); + ])->postJson('/api/v1/client_statement', $data); } catch (ValidationException $e) { $message = json_decode($e->validator->getMessageBag(), 1); nlog($message); @@ -100,12 +100,17 @@ class ClientApiTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->post('/api/v1/client_statement?send_email=true', $data); + ])->postJson('/api/v1/client_statement?send_email=true', $data); } catch (ValidationException $e) { $message = json_decode($e->validator->getMessageBag(), 1); nlog($message); } + $response->assertJson([ + 'message' => ctrans('texts.email_queued'), + ]); + + $response->assertStatus(200); } From 8bc298daa45f0b62371d64a3d193d8a9242b76e1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 11:06:41 +1100 Subject: [PATCH 6/9] Add checks for content type of statement object --- tests/Feature/ClientApiTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Feature/ClientApiTest.php b/tests/Feature/ClientApiTest.php index 45813aa56d..5d561c346d 100644 --- a/tests/Feature/ClientApiTest.php +++ b/tests/Feature/ClientApiTest.php @@ -78,6 +78,8 @@ class ClientApiTest extends TestCase nlog($message); } + $this->assertTrue($response->headers->get('content-type') == 'application/pdf'); + $response->assertStatus(200); } From 86a731ff476da557ca12175e00bfe867c12509a0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 11:39:19 +1100 Subject: [PATCH 7/9] Handle failures around subscription webhooks --- app/Utils/Traits/SubscriptionHooker.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Utils/Traits/SubscriptionHooker.php b/app/Utils/Traits/SubscriptionHooker.php index cd4d73a852..52b05fbb1e 100644 --- a/app/Utils/Traits/SubscriptionHooker.php +++ b/app/Utils/Traits/SubscriptionHooker.php @@ -51,7 +51,12 @@ trait SubscriptionHooker $error = json_decode($e->getResponse()->getBody()->getContents()); - if(property_exists($error, 'message')) + if(is_null($error)){ + nlog("empty response"); + nlog($e->getMessage()); + } + + if($error && property_exists($error, 'message')) $message = $error->message; return array_merge($body, ['message' => $message, 'status_code' => 500]); From 52a982d6341c07347c9b45f114ef0fc337f1a456 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 12:21:36 +1100 Subject: [PATCH 8/9] Email Tests --- app/Http/Controllers/SelfUpdateController.php | 1 - app/Services/Email/EmailService.php | 19 +++-- tests/Feature/Email/EmailTest.php | 84 +++++++++++++++++++ 3 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/Email/EmailTest.php diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index 506c7a9156..abe82f44ef 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -16,7 +16,6 @@ use App\Models\Client; use App\Utils\Ninja; use App\Utils\Traits\AppSetup; use App\Utils\Traits\ClientGroupSettingsSaver; -use Beganovich\Snappdf\Snappdf; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Storage; diff --git a/app/Services/Email/EmailService.php b/app/Services/Email/EmailService.php index 7bc635d9d7..2e2d465636 100644 --- a/app/Services/Email/EmailService.php +++ b/app/Services/Email/EmailService.php @@ -89,24 +89,27 @@ class EmailService if($this->company->is_disabled && !$this->override) return true; + if(Ninja::isSelfHost()) + return false; + /* To handle spam users we drop all emails from flagged accounts */ - if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged) + if($this->company->account && $this->company->account->is_flagged) return true; /* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */ - if(Ninja::isHosted() && $this->hasValidEmails()) + if($this->hasInValidEmails()) return true; /* GMail users are uncapped */ - if(Ninja::isHosted() && in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) + if(in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) return false; /* On the hosted platform, if the user is over the email quotas, we do not send the email. */ - if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded()) + if($this->company->account && $this->company->account->emailQuotaExceeded()) return true; /* If the account is verified, we allow emails to flow */ - if(Ninja::isHosted() && $this->company->account && $this->company->account->is_verified_account) { + if($this->company->account && $this->company->account->is_verified_account) { //11-01-2022 @@ -118,7 +121,7 @@ class EmailService } /* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */ - if(Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified){ + if($this->company->account && !$this->company->account->account_sms_verified){ if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run(); @@ -133,7 +136,7 @@ class EmailService return false; } - private function hasValidEmails(): bool + private function hasInValidEmails(): bool { foreach($this->email_object->to as $address_object) @@ -145,6 +148,8 @@ class EmailService if(!str_contains($address_object->address, "@")) return true; + if($address_object->address == " ") + return true; } diff --git a/tests/Feature/Email/EmailTest.php b/tests/Feature/Email/EmailTest.php new file mode 100644 index 0000000000..40dadc5ca6 --- /dev/null +++ b/tests/Feature/Email/EmailTest.php @@ -0,0 +1,84 @@ +markTestSkipped('Skip test not needed in this environment'); + + $this->makeTestData(); + + $this->email_object = new EmailObject(); + $this->email_object->to = [new Address("testing@gmail.com", "Cool Name")]; + $this->email_object->attachments = []; + $this->email_object->settings = $this->client->getMergedSettings(); + $this->email_object->company = $this->client->company; + $this->email_object->client = $this->client; + $this->email_object->email_template_subject = 'email_subject_statement'; + $this->email_object->email_template_body = 'email_template_statement'; + $this->email_object->variables = [ + '$client' => $this->client->present()->name(), + '$start_date' => '2022-01-01', + '$end_date' => '2023-01-01', + ]; + + $this->email_service = new EmailService($this->email_object, $this->company); + + } + + public function testPreFlightChecksHosted() + { + + config(['ninja.environment' => 'hosted']); + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + } + + public function testPreFlightChecksSelfHost() + { + + config(['ninja.environment' => 'selfhost']); + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + } + + + +} From dcbfc6c170a4ac533db3cc3cd910e8b6b33208d8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 17 Jan 2023 15:39:20 +1100 Subject: [PATCH 9/9] Improvements for cache when running setup for the first time --- VERSION.txt | 2 +- app/Console/Commands/CreateAccount.php | 34 ++++++++++++-------------- app/Exceptions/Handler.php | 23 ++++++----------- config/ninja.php | 4 +-- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index eea8514679..cae032632b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.55 \ No newline at end of file +5.5.56 \ No newline at end of file diff --git a/app/Console/Commands/CreateAccount.php b/app/Console/Commands/CreateAccount.php index c0eaed16bc..8e6140a74e 100644 --- a/app/Console/Commands/CreateAccount.php +++ b/app/Console/Commands/CreateAccount.php @@ -134,6 +134,9 @@ class CreateAccount extends Command (new CreateCompanyPaymentTerms($company, $user))->handle(); (new CreateCompanyTaskStatuses($company, $user))->handle(); (new VersionCheck())->handle(); + + $this->warmCache(); + } private function warmCache() @@ -142,25 +145,20 @@ class CreateAccount extends Command $cached_tables = config('ninja.cached_tables'); foreach ($cached_tables as $name => $class) { - if (! Cache::has($name)) { - // check that the table exists in case the migration is pending - if (! Schema::hasTable((new $class())->getTable())) { - continue; - } - if ($name == 'payment_terms') { - $orderBy = 'num_days'; - } elseif ($name == 'fonts') { - $orderBy = 'sort_order'; - } elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) { - $orderBy = 'name'; - } else { - $orderBy = 'id'; - } - $tableData = $class::orderBy($orderBy)->get(); - if ($tableData->count()) { - Cache::forever($name, $tableData); - } + if ($name == 'payment_terms') { + $orderBy = 'num_days'; + } elseif ($name == 'fonts') { + $orderBy = 'sort_order'; + } elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) { + $orderBy = 'name'; + } else { + $orderBy = 'id'; } + $tableData = $class::orderBy($orderBy)->get(); + if ($tableData->count()) { + Cache::forever($name, $tableData); + } + } } } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 2dd5218762..3286ebb730 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -16,12 +16,10 @@ use App\Exceptions\InternalPDFFailure; use App\Exceptions\PhantomPDFFailure; use App\Exceptions\StripeConnectFailure; use App\Utils\Ninja; -use Exception; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Database\Eloquent\RelationNotFoundException; -use Illuminate\Database\QueryException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Http\Exceptions\ThrottleRequestsException; use Illuminate\Http\Request; @@ -34,7 +32,6 @@ use Illuminate\Validation\ValidationException; use PDOException; use Sentry\Laravel\Integration; use Sentry\State\Scope; -use Swift_TransportException; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; @@ -50,7 +47,6 @@ class Handler extends ExceptionHandler */ protected $dontReport = [ PDOException::class, - //Swift_TransportException::class, MaxAttemptsExceededException::class, CommandNotFoundException::class, ValidationException::class, @@ -80,11 +76,10 @@ class Handler extends ExceptionHandler { if (! Schema::hasTable('accounts')) { info('account table not found'); - return; } - if (Ninja::isHosted() && ! ($exception instanceof ValidationException)) { + if (Ninja::isHosted()) { Integration::configureScope(function (Scope $scope): void { $name = 'hosted@invoiceninja.com'; @@ -105,7 +100,10 @@ class Handler extends ExceptionHandler ]); }); - Integration::captureUnhandledException($exception); + if ($this->validException($exception)) { + Integration::captureUnhandledException($exception); + } + } elseif (app()->bound('sentry') && $this->shouldReport($exception)) { Integration::configureScope(function (Scope $scope): void { if (auth()->guard('contact') && auth()->guard('contact')->user() && auth()->guard('contact')->user()->company->account->report_errors) { @@ -180,8 +178,8 @@ class Handler extends ExceptionHandler return response()->json(['message' => $exception->getMessage()], 500); } elseif ($exception instanceof ThrottleRequestsException && $request->expectsJson()) { return response()->json(['message'=>'Too many requests'], 429); - } elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) { - return response()->json(['message'=>'Fatal error'], 500); + // } elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) { + // return response()->json(['message'=>'Fatal error'], 500); //@deprecated } elseif ($exception instanceof AuthorizationException) { return response()->json(['message'=> $exception->getMessage()], 401); } elseif ($exception instanceof TokenMismatchException) { @@ -208,13 +206,6 @@ class Handler extends ExceptionHandler return response()->json(['message' => $exception->getMessage()], 400); } - - // elseif ($exception instanceof QueryException) { - // return response()->json(['message' => 'We had a problem executing this query. Please retry.'], 500); - // } - - - return parent::render($request, $exception); } diff --git a/config/ninja.php b/config/ninja.php index 0afd46c0f6..0b3a99c33a 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.5.55', - 'app_tag' => '5.5.55', + 'app_version' => '5.5.56', + 'app_tag' => '5.5.56', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''),