From 50c26bee4cec6c6e76159d63c03c27a96772a140 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 May 2023 12:37:38 +1000 Subject: [PATCH 01/34] Additional translations --- app/Services/Tax/Providers/TaxProvider.php | 8 ++++---- app/Services/Tax/TaxService.php | 5 +++++ config/ninja.php | 4 ---- lang/en/texts.php | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index 4d71522cf617..c00950e33324 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -52,7 +52,7 @@ class TaxProvider private mixed $api_credentials; - public function __construct(protected Company $company, protected Client $client) + public function __construct(public Company $company, public Client $client) { } @@ -108,15 +108,15 @@ class TaxProvider ]; - $tax_provider = new $this->provider(); + $tax_provider = new $this->provider($billing_details); $tax_provider->setApiCredentials($this->api_credentials); $tax_data = $tax_provider->run(); - $this->company->tax_data = $tax_data; + $this->client->tax_data = $tax_data; - $this->company->save(); + $this->client->save(); return $this; diff --git a/app/Services/Tax/TaxService.php b/app/Services/Tax/TaxService.php index 7213b023c5f1..89dd1ba7c31c 100644 --- a/app/Services/Tax/TaxService.php +++ b/app/Services/Tax/TaxService.php @@ -31,4 +31,9 @@ class TaxService return $this; } + + public function initTaxProvider() + { + + } } \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 297e6880d877..f61cf2828fd5 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -219,8 +219,4 @@ return [ 'client_id' => env('SHOPIFY_CLIENT_ID', null), 'client_secret' => env('SHOPIFY_CLIENT_SECRET', null), ], - 'tax_api' => [ - 'provider' => env('TAX_API_PROVIDER', false), - 'api_key' => env('TAX_API_KEY', false), - ] ]; diff --git a/lang/en/texts.php b/lang/en/texts.php index 34915a268f85..886b2f0e77e3 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5086,6 +5086,7 @@ $LANG = array( 'e_invoice' => 'E-Invoice', 'light_dark_mode' => 'Light/Dark Mode', 'activities' => 'Activities', + 'recent_transactions' => "Here are your company's most recent transactions:", ); From 63da663afa12409d5f55387ad9a30afd650541fa Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 May 2023 13:57:00 +1000 Subject: [PATCH 02/34] Rules --- app/DataMapper/Tax/DE/Rule.php | 29 ++++++++++++++++++++++ app/DataMapper/Tax/US/Rule.php | 27 +++++++++++++++++++- app/Services/Tax/Providers/TaxProvider.php | 3 ++- app/Services/Tax/Providers/ZipTax.php | 6 ++++- lang/en/texts.php | 2 +- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index 2118adf02e98..98f7016eb5fd 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -74,12 +74,27 @@ class Rule extends BaseRule implements RuleInterface Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), + Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated(), + Product::PRODUCT_TYPE_REVERSE_TAX => $this->reverseTax(), default => $this->default(), }; return $this; } + /** + * Calculates the tax rate for a reduced tax product + * + * @return self + */ + public function reverseTax(): self + { + $this->tax_rate1 = 0; + $this->tax_name1 = 'ermäßigte MwSt.'; + + return $this; + } + /** * Calculates the tax rate for a reduced tax product * @@ -93,6 +108,20 @@ class Rule extends BaseRule implements RuleInterface return $this; } + /** + * Calculates the tax rate for a zero rated tax product + * + * @return self + */ + public function zeroRated(): self + { + $this->tax_rate1 = 0; + $this->tax_name1 = 'ermäßigte MwSt.'; + + return $this; + } + + /** * Calculates the tax rate for a tax exempt product * diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 854a689cae2a..f22e09b612e4 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -64,7 +64,8 @@ class Rule extends BaseRule implements RuleInterface Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), + Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated(), default => $this->default(), }; @@ -169,6 +170,16 @@ class Rule extends BaseRule implements RuleInterface return $this; } + public function zeroRated(): self + { + + $this->tax_rate1 = 0; + $this->tax_name1 = "{$this->tax_data->geoState} Zero Rated Tax"; + + return $this; + + } + /** * Calculates the tax rate for a reduced tax product * @@ -190,4 +201,18 @@ class Rule extends BaseRule implements RuleInterface { return $this; } + + /** + * Calculates the tax rate for a reverse tax product + * + * @return self + */ + public function reverseTax(): self + { + $this->default(); + + return $this; + } + + } diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index c00950e33324..73ecb3a7f119 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -54,7 +54,6 @@ class TaxProvider public function __construct(public Company $company, public Client $client) { - } @@ -114,6 +113,8 @@ class TaxProvider $tax_data = $tax_provider->run(); + nlog($tax_data); + $this->client->tax_data = $tax_data; $this->client->save(); diff --git a/app/Services/Tax/Providers/ZipTax.php b/app/Services/Tax/Providers/ZipTax.php index b6f90815db11..7c73c0aeba95 100644 --- a/app/Services/Tax/Providers/ZipTax.php +++ b/app/Services/Tax/Providers/ZipTax.php @@ -27,8 +27,9 @@ class ZipTax implements TaxProviderInterface public function run() { + $string_address = implode(" ", $this->address); - $response = $this->callApi(['key' => $this->api_key, 'address' => $this->address]); + $response = $this->callApi(['key' => $this->api_key, 'address' => $string_address]); if($response->successful()) return $response->json(); @@ -36,6 +37,9 @@ class ZipTax implements TaxProviderInterface if(isset($this->address['postal_code'])) { $response = $this->callApi(['key' => $this->api_key, 'address' => $this->address['postal_code']]); + nlog($response->json()); + nlog($response->body()); + if($response->successful()) return $response->json(); diff --git a/lang/en/texts.php b/lang/en/texts.php index 886b2f0e77e3..009e9dee94e6 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5092,4 +5092,4 @@ $LANG = array( return $LANG; -?> +?> \ No newline at end of file From 913599334b15eb1d1ea0622615f87dc3178f2ae2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 May 2023 14:38:11 +1000 Subject: [PATCH 03/34] Fixes for Zip Tax Response --- app/DataMapper/Tax/US/Rule.php | 2 ++ app/Services/Tax/Providers/ZipTax.php | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index f22e09b612e4..d012aae93089 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -144,6 +144,8 @@ class Rule extends BaseRule implements RuleInterface */ public function default(): self { +nlog("default rate"); +nlog($this->tax_data); if($this->tax_data?->stateSalesTax == 0) { diff --git a/app/Services/Tax/Providers/ZipTax.php b/app/Services/Tax/Providers/ZipTax.php index 7c73c0aeba95..dd882833a278 100644 --- a/app/Services/Tax/Providers/ZipTax.php +++ b/app/Services/Tax/Providers/ZipTax.php @@ -31,17 +31,17 @@ class ZipTax implements TaxProviderInterface $response = $this->callApi(['key' => $this->api_key, 'address' => $string_address]); - if($response->successful()) - return $response->json(); + if($response->successful()){ + + return $this->parseResponse($response->json()); + + } if(isset($this->address['postal_code'])) { $response = $this->callApi(['key' => $this->api_key, 'address' => $this->address['postal_code']]); - nlog($response->json()); - nlog($response->body()); - if($response->successful()) - return $response->json(); + return $this->parseResponse($response->json()); } @@ -69,4 +69,11 @@ class ZipTax implements TaxProviderInterface return $response; } + + private function parseResponse($response) + { + $tax = $response['results']['0']; + + return $tax; + } } From 7a88d631dc938b83bbd090055c5f0c922a27717d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 May 2023 21:20:47 +1000 Subject: [PATCH 04/34] Updates for e-invoice signatures --- app/DataMapper/Tax/BaseRule.php | 38 ++++---- app/DataMapper/Tax/DE/Rule.php | 48 +++++----- app/DataMapper/Tax/RuleInterface.php | 18 ++-- app/DataMapper/Tax/US/Rule.php | 93 ++++++++++--------- app/Helpers/Invoice/InvoiceItemSum.php | 4 - app/Http/Controllers/CompanyController.php | 8 ++ .../Requests/Company/UpdateCompanyRequest.php | 3 +- app/Models/Company.php | 3 +- app/Transformers/CompanyTransformer.php | 2 + ...023_05_15_103212_e_invoice_ssl_storage.php | 32 +++++++ 10 files changed, 152 insertions(+), 97 deletions(-) create mode 100644 database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 6f664dd8148f..37299abc5115 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -14,7 +14,6 @@ namespace App\DataMapper\Tax; use App\Models\Client; use App\Models\Invoice; use App\Models\Product; -use App\DataMapper\Tax\TaxData; use App\DataProviders\USStates; use App\DataMapper\Tax\ZipTax\Response; @@ -147,21 +146,25 @@ class BaseRule implements RuleInterface private function configTaxData(): self { - + /* If the client Country is not in the region_codes, we force the company country onto the client? @TODO */ if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) { $this->client->country_id = $this->invoice->company->settings->country_id; $this->client->saveQuietly(); + nlog('Automatic tax calculations not supported for this country - defaulting to company country'); } + /** Harvest the client_region */ $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; + /** If the tax data is already set and the invoice is marked as sent, do not adjust the rates */ if($this->invoice->tax_data && $this->invoice->status_id > 1) return $this; - //determine if we are taxing locally or if we are taxing globally + //Pass the client tax data into the invoice tax data object $tax_data = is_object($this->invoice->client->tax_data) ? $this->invoice->client->tax_data : new Response([]); + /** If no Origin / Destination has been set and the seller and client sub regions are not the same, force destination tax */ if(strlen($this->invoice->tax_data?->originDestination) == 0 && $this->client->company->tax_data->seller_subregion != $this->client_subregion) { $tax_data->originDestination = "D"; $tax_data->geoState = $this->client_subregion; @@ -235,18 +238,21 @@ class BaseRule implements RuleInterface { if ($this->client->is_tax_exempt) { - return $this->taxExempt(); + + return $this->taxExempt($item); + } elseif($this->client_region == $this->seller_region && $this->isTaxableRegion()) { - $this->taxByType($item->tax_id); + $this->taxByType($item); return $this; + } elseif($this->isTaxableRegion()) { //other regions outside of US match(intval($item->tax_id)) { - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item), default => $this->defaultForeign(), }; @@ -260,42 +266,42 @@ class BaseRule implements RuleInterface return $this; } - public function taxReduced(): self + public function taxReduced($item): self { return $this; } - public function taxExempt(): self + public function taxExempt($item): self { return $this; } - public function taxDigital(): self + public function taxDigital($item): self { return $this; } - public function taxService(): self + public function taxService($item): self { return $this; } - public function taxShipping(): self + public function taxShipping($item): self { return $this; } - public function taxPhysical(): self + public function taxPhysical($item): self { return $this; } - public function default(): self + public function default($item): self { return $this; } - public function override(): self + public function override($item): self { return $this; } diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index 98f7016eb5fd..288a5aec2e6a 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -56,27 +56,27 @@ class Rule extends BaseRule implements RuleInterface /** * Sets the correct tax rate based on the product type. * - * @param mixed $product_tax_type + * @param mixed $item * @return self */ - public function taxByType($product_tax_type): self + public function taxByType($item): self { if ($this->client->is_tax_exempt) { - return $this->taxExempt(); + return $this->taxExempt($item); } - match(intval($product_tax_type)){ - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(), - Product::PRODUCT_TYPE_SERVICE => $this->taxService(), - Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), - Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), - Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated(), - Product::PRODUCT_TYPE_REVERSE_TAX => $this->reverseTax(), - default => $this->default(), + match(intval($item->tax_id)){ + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item), + Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item), + Product::PRODUCT_TYPE_SERVICE => $this->taxService($item), + Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item), + Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item), + Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item), + Product::PRODUCT_TYPE_REVERSE_TAX => $this->reverseTax($item), + default => $this->default($item), }; return $this; @@ -87,7 +87,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function reverseTax(): self + public function reverseTax($item): self { $this->tax_rate1 = 0; $this->tax_name1 = 'ermäßigte MwSt.'; @@ -100,7 +100,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxReduced(): self + public function taxReduced($item): self { $this->tax_rate1 = $this->reduced_tax_rate; $this->tax_name1 = 'ermäßigte MwSt.'; @@ -113,7 +113,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function zeroRated(): self + public function zeroRated($item): self { $this->tax_rate1 = 0; $this->tax_name1 = 'ermäßigte MwSt.'; @@ -127,7 +127,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxExempt(): self + public function taxExempt($item): self { $this->tax_name1 = ''; $this->tax_rate1 = 0; @@ -140,7 +140,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxDigital(): self + public function taxDigital($item): self { $this->tax_rate1 = $this->tax_rate; @@ -154,7 +154,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxService(): self + public function taxService($item): self { $this->tax_rate1 = $this->tax_rate; @@ -168,7 +168,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxShipping(): self + public function taxShipping($item): self { $this->tax_rate1 = $this->tax_rate; @@ -182,7 +182,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxPhysical(): self + public function taxPhysical($item): self { $this->tax_rate1 = $this->tax_rate; @@ -196,7 +196,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function default(): self + public function default($item): self { $this->tax_name1 = ''; @@ -210,7 +210,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function override(): self + public function override($item): self { return $this; } diff --git a/app/DataMapper/Tax/RuleInterface.php b/app/DataMapper/Tax/RuleInterface.php index 6cd933567645..e4ff2c0e2da9 100644 --- a/app/DataMapper/Tax/RuleInterface.php +++ b/app/DataMapper/Tax/RuleInterface.php @@ -15,25 +15,25 @@ interface RuleInterface { public function init(); - public function tax($item = null); + public function tax($item); public function taxByType($type); - public function taxExempt(); + public function taxExempt($item); - public function taxDigital(); + public function taxDigital($item); - public function taxService(); + public function taxService($item); - public function taxShipping(); + public function taxShipping($item); - public function taxPhysical(); + public function taxPhysical($item); - public function taxReduced(); + public function taxReduced($item); - public function default(); + public function default($item); - public function override(); + public function override($item); public function calculateRates(); } \ No newline at end of file diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index d012aae93089..24f65c9b5dfa 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -41,32 +41,39 @@ class Rule extends BaseRule implements RuleInterface /** * Override tax class, we use this when we do not modify the input taxes * + * @param mixed $item * @return self */ - public function override(): self + public function override($item): self { + + $this->tax_rate1 = $item->tax_rate1; + + $this->tax_name1 = $item->tax_name1; + return $this; + } /** * Sets the correct tax rate based on the product type. * - * @param mixed $product_tax_type + * @param mixed $item * @return self */ - public function taxByType($product_tax_type): self + public function taxByType($item): self { - match(intval($product_tax_type)) { - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(), - Product::PRODUCT_TYPE_SERVICE => $this->taxService(), - Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), - Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), - Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated(), - default => $this->default(), + match(intval($item->tax_id)) { + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item), + Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item), + Product::PRODUCT_TYPE_SERVICE => $this->taxService($item), + Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item), + Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item), + Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item), + default => $this->default($item), }; return $this; @@ -74,10 +81,11 @@ class Rule extends BaseRule implements RuleInterface /** * Sets the tax as exempt (0) + * @param mixed $item * * @return self */ - public function taxExempt(): self + public function taxExempt($item): self { $this->tax_name1 = ''; $this->tax_rate1 = 0; @@ -87,25 +95,27 @@ class Rule extends BaseRule implements RuleInterface /** * Calculates the tax rate for a digital product + * @param mixed $item * * @return self */ - public function taxDigital(): self + public function taxDigital($item): self { - $this->default(); + $this->default($item); return $this; } /** * Calculates the tax rate for a service product + * @param mixed $item * * @return self */ - public function taxService(): self + public function taxService($item): self { if($this->tax_data?->txbService == 'Y') { - $this->default(); + $this->default($item); } return $this; @@ -113,13 +123,14 @@ class Rule extends BaseRule implements RuleInterface /** * Calculates the tax rate for a shipping product + * @param mixed $item * * @return self */ - public function taxShipping(): self + public function taxShipping($item): self { if($this->tax_data?->txbFreight == 'Y') { - $this->default(); + $this->default($item); } return $this; @@ -127,12 +138,13 @@ class Rule extends BaseRule implements RuleInterface /** * Calculates the tax rate for a physical product + * @param mixed $item * * @return self */ - public function taxPhysical(): self + public function taxPhysical($item): self { - $this->default(); + $this->default($item); return $this; } @@ -142,10 +154,8 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function default(): self + public function default($item): self { -nlog("default rate"); -nlog($this->tax_data); if($this->tax_data?->stateSalesTax == 0) { @@ -172,7 +182,7 @@ nlog($this->tax_data); return $this; } - public function zeroRated(): self + public function zeroRated($item): self { $this->tax_rate1 = 0; @@ -187,13 +197,25 @@ nlog($this->tax_data); * * @return self */ - public function taxReduced(): self + public function taxReduced($item): self { - $this->default(); + $this->default($item); return $this; } - + + /** + * Calculates the tax rate for a reverse tax product + * + * @return self + */ + public function reverseTax($item): self + { + $this->default($item); + + return $this; + } + /** * Calculates the tax rates to be applied * @@ -204,17 +226,4 @@ nlog($this->tax_data); return $this; } - /** - * Calculates the tax rate for a reverse tax product - * - * @return self - */ - public function reverseTax(): self - { - $this->default(); - - return $this; - } - - } diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index dd2d9b2e434e..fd1cc9c390ff 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -144,12 +144,8 @@ class InvoiceItemSum return $this; } - //should we be filtering by client country here? do we need to reflect at the company <=> client level? - // if (in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions - nlog($this->client->company->country()->iso_3166_2); - $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; $this->rule = new $class(); diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 6b32a6bee2e4..108d0d74abbe 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -41,6 +41,7 @@ use App\Utils\Traits\Uploadable; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Response; use Illuminate\Support\Facades\Storage; +use Str; use Turbo124\Beacon\Facades\LightLogs; /** @@ -417,6 +418,13 @@ class CompanyController extends BaseController $this->saveDocuments($request->input('documents'), $company, false); } + if($request->has('e_invoice_certificate')){ + + $company->e_invoice_certificate = base64_encode($request->file("e_invoice_certificate")->get()); + $company->save(); + + } + $this->uploadLogo($request->file('company_logo'), $company, $company); return $this->itemResponse($company); diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index f1c66b0a3a4f..f4070350a887 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -53,7 +53,8 @@ class UpdateCompanyRequest extends Request $rules['country_id'] = 'integer|nullable'; $rules['work_email'] = 'email|nullable'; $rules['matomo_id'] = 'nullable|integer'; - + $rules['e_invoice_certificate_passphrase'] = 'sometimes|nullable'; + $rules['e_invoice_certificate'] = 'sometimes|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin'; // $rules['client_registration_fields'] = 'array'; if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { diff --git a/app/Models/Company.php b/app/Models/Company.php index 2b0d05a12cbc..a0d926b86f23 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -339,6 +339,7 @@ class Company extends BaseModel 'notify_vendor_when_paid', 'calculate_taxes', 'tax_data', + 'e_invoice_certificate_passphrase', ]; protected $hidden = [ @@ -357,6 +358,7 @@ class Company extends BaseModel 'deleted_at' => 'timestamp', 'client_registration_fields' => 'array', 'tax_data' => 'object', + 'e_invoice_certificate_passphrase' => 'encrypted', ]; protected $with = []; @@ -365,7 +367,6 @@ class Company extends BaseModel self::ENTITY_RECURRING_INVOICE => 1, self::ENTITY_CREDIT => 2, self::ENTITY_QUOTE => 4, - // @phpstan-ignore-next-line self::ENTITY_TASK => 8, self::ENTITY_EXPENSE => 16, self::ENTITY_PROJECT => 32, diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index eff5e27b6be7..4152fa1988ff 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -199,6 +199,8 @@ class CompanyTransformer extends EntityTransformer 'invoice_task_hours' => (bool) $company->invoice_task_hours, 'calculate_taxes' => (bool) $company->calculate_taxes, 'tax_data' => $company->tax_data ?: new \stdClass, + 'has_e_invoice_certificate' => $company->e_invoice_certificate ? true : false, + 'has_e_invoice_certificate_passphrase' => $company->e_invoice_certificate_passphrase ? true : false, ]; } diff --git a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php new file mode 100644 index 000000000000..f6dab48bd698 --- /dev/null +++ b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php @@ -0,0 +1,32 @@ +text('e_invoice_certificate')->nullable(); + $table->string('e_invoice_certificate_passphrase')->nullable(); + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; From 25a7038a115328be11a0bde7fe8a981972811d2a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 May 2023 21:40:57 +1000 Subject: [PATCH 05/34] Fixes for tests --- app/DataMapper/Tax/US/Rule.php | 2 +- app/Http/Controllers/CompanyController.php | 2 +- .../Requests/Company/UpdateCompanyRequest.php | 2 +- tests/Feature/CompanyTest.php | 2 + tests/Unit/Tax/TaxConfigTest.php | 71 +++++++++++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Tax/TaxConfigTest.php diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 24f65c9b5dfa..c1005a42cb86 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -114,7 +114,7 @@ class Rule extends BaseRule implements RuleInterface */ public function taxService($item): self { - if($this->tax_data?->txbService == 'Y') { + if(in_array($this->tax_data?->txbService,['Y','L'])) { $this->default($item); } diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 108d0d74abbe..a7900a3f173c 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -418,7 +418,7 @@ class CompanyController extends BaseController $this->saveDocuments($request->input('documents'), $company, false); } - if($request->has('e_invoice_certificate')){ + if($request->has('e_invoice_certificate') && !is_null($request->file("e_invoice_certificate"))){ $company->e_invoice_certificate = base64_encode($request->file("e_invoice_certificate")->get()); $company->save(); diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index f4070350a887..602bb3a0fe18 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -54,7 +54,7 @@ class UpdateCompanyRequest extends Request $rules['work_email'] = 'email|nullable'; $rules['matomo_id'] = 'nullable|integer'; $rules['e_invoice_certificate_passphrase'] = 'sometimes|nullable'; - $rules['e_invoice_certificate'] = 'sometimes|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin'; + $rules['e_invoice_certificate'] = 'sometimes|nullable|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin'; // $rules['client_registration_fields'] = 'array'; if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index a8eb0c8e894c..af02f493ea4b 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -143,6 +143,8 @@ class CompanyTest extends TestCase $company->settings = $settings; + nlog($company->toArray()); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, diff --git a/tests/Unit/Tax/TaxConfigTest.php b/tests/Unit/Tax/TaxConfigTest.php new file mode 100644 index 000000000000..df579795ad21 --- /dev/null +++ b/tests/Unit/Tax/TaxConfigTest.php @@ -0,0 +1,71 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->withoutExceptionHandling(); + + $this->makeTestData(); + + if(!config('services.tax.zip_tax.key')) + $this->markTestSkipped('No API keys to test with.'); + } + + public TaxProvider $tp; + + private function bootApi(Client $client) + { + $this->tp = new TaxProvider($this->company, $client); + } + + public function testStateResolution() + { + //infer state from zip + $client = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'address1' => '400 Evelyn Pl', + 'city' =>'Beverley Hills', + 'state' =>'CA', + 'postal_code' =>90210, + 'country_id' => 840, + ]); + + $this->bootApi($client); + + $this->tp->updateClientTaxData(); + + } + +} \ No newline at end of file From 44b0bcd019bec48956db3b1357c19a953379e78f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 15 May 2023 21:49:38 +1000 Subject: [PATCH 06/34] Fixes for tests --- app/Services/Tax/Providers/ZipTax.php | 6 ++++-- tests/Unit/Tax/TaxConfigTest.php | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/Services/Tax/Providers/ZipTax.php b/app/Services/Tax/Providers/ZipTax.php index dd882833a278..9f0e40b8b4de 100644 --- a/app/Services/Tax/Providers/ZipTax.php +++ b/app/Services/Tax/Providers/ZipTax.php @@ -72,8 +72,10 @@ class ZipTax implements TaxProviderInterface private function parseResponse($response) { - $tax = $response['results']['0']; + if(isset($response['results']['0'])) + return $response['results']['0']; - return $tax; + throw new \Exception("Error resolving tax (code) = " . $response['rCode']); + } } diff --git a/tests/Unit/Tax/TaxConfigTest.php b/tests/Unit/Tax/TaxConfigTest.php index df579795ad21..c08c84a25ec1 100644 --- a/tests/Unit/Tax/TaxConfigTest.php +++ b/tests/Unit/Tax/TaxConfigTest.php @@ -11,6 +11,7 @@ namespace Tests\Unit\Tax; +use App\DataProviders\USStates; use Tests\TestCase; use App\Models\Client; use Tests\MockAccountData; @@ -56,15 +57,18 @@ class TaxConfigTest extends TestCase 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'address1' => '400 Evelyn Pl', - 'city' =>'Beverley Hills', - 'state' =>'CA', - 'postal_code' =>90210, + 'city' => 'Beverley Hills', + 'state' => '', + 'postal_code' => 90210, 'country_id' => 840, ]); - $this->bootApi($client); - $this->tp->updateClientTaxData(); + $this->assertEquals('CA', USStates::getState('90210')); + + // $this->bootApi($client); + + // $this->tp->updateClientTaxData(); } From 269fbea1f0603d242c221ff8999bdef66b11f003 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 07:43:26 +1000 Subject: [PATCH 07/34] Update client tax data when updating the client record --- app/DataMapper/Tax/BaseRule.php | 1 + app/Http/Controllers/EmailController.php | 68 --------------------- app/Jobs/Client/UpdateTaxData.php | 78 ++++++++++++++++++++++++ app/Observers/ClientObserver.php | 16 ++++- tests/Unit/Tax/TaxConfigTest.php | 8 +-- 5 files changed, 96 insertions(+), 75 deletions(-) create mode 100644 app/Jobs/Client/UpdateTaxData.php diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 37299abc5115..fc71eaa498ae 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -152,6 +152,7 @@ class BaseRule implements RuleInterface $this->client->saveQuietly(); nlog('Automatic tax calculations not supported for this country - defaulting to company country'); + } /** Harvest the client_region */ diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 8cd59930b6d2..b7e692d3726d 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -46,74 +46,6 @@ class EmailController extends BaseController parent::__construct(); } - /** - * Returns a template filled with entity variables. - * - * @param SendEmailRequest $request - * @return Response - * - * @OA\Post( - * path="/api/v1/emails", - * operationId="sendEmailTemplate", - * tags={"emails"}, - * summary="Sends an email for an entity", - * description="Sends an email for an entity", - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\RequestBody( - * description="The template subject and body", - * required=true, - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * type="object", - * @OA\Property( - * property="subject", - * description="The email subject", - * type="string", - * ), - * @OA\Property( - * property="body", - * description="The email body", - * type="string", - * ), - * @OA\Property( - * property="entity", - * description="The entity name", - * type="string", - * ), - * @OA\Property( - * property="entity_id", - * description="The entity_id", - * type="string", - * ), - * @OA\Property( - * property="template", - * description="The template required", - * type="string", - * ), - * ) - * ) - * ), - * @OA\Response( - * response=200, - * description="success", - * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), - * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), - * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * @OA\JsonContent(ref="#/components/schemas/Template"), - * ), - * @OA\Response( - * response=422, - * description="Validation error", - * @OA\JsonContent(ref="#/components/schemas/ValidationError"), - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - */ public function send(SendEmailRequest $request) { $entity = $request->input('entity'); diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php new file mode 100644 index 000000000000..105360fda062 --- /dev/null +++ b/app/Jobs/Client/UpdateTaxData.php @@ -0,0 +1,78 @@ +company->db); + + if(!config('services.tax.zip_tax.key')) + return; + + $tax_provider = new \App\Services\Tax\Providers\TaxProvider($this->company, $this->client); + + try { + + $tax_provider->updateClientTaxData(); + + + if (!$this->client->state && $this->client->postal_code) { + + $this->client->state = USStates::getState($this->client->postal_code); + + $this->client->save(); + + } + + + }catch(\Exception $e){ + nlog("problem getting tax data => ".$e->getMessage()); + } + + + } +} \ No newline at end of file diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index ed0a1e7c3310..066afd3c71c1 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -11,9 +11,10 @@ namespace App\Observers; -use App\Jobs\Util\WebhookHandler; use App\Models\Client; use App\Models\Webhook; +use App\Jobs\Util\WebhookHandler; +use App\Jobs\Company\UpdateTaxData; class ClientObserver { @@ -27,6 +28,11 @@ class ClientObserver */ public function created(Client $client) { + + if ($client->country_id == 840 && $client->company->calculate_taxes) { + UpdateTaxData::dispatch($client, $client->company); + } + $subscriptions = Webhook::where('company_id', $client->company_id) ->where('event_id', Webhook::EVENT_CREATE_CLIENT) ->exists(); @@ -44,6 +50,11 @@ class ClientObserver */ public function updated(Client $client) { + if($client->getOriginal('postal_code') != $client->postal_code && $client->country_id == 840 && $client->company->calculate_taxes) + { + UpdateTaxData::dispatch($client, $client->company); + } + $event = Webhook::EVENT_UPDATE_CLIENT; if ($client->getOriginal('deleted_at') && !$client->deleted_at) { @@ -53,8 +64,7 @@ class ClientObserver if ($client->is_deleted) { $event = Webhook::EVENT_DELETE_CLIENT; } - - + $subscriptions = Webhook::where('company_id', $client->company_id) ->where('event_id', $event) ->exists(); diff --git a/tests/Unit/Tax/TaxConfigTest.php b/tests/Unit/Tax/TaxConfigTest.php index c08c84a25ec1..459c36240391 100644 --- a/tests/Unit/Tax/TaxConfigTest.php +++ b/tests/Unit/Tax/TaxConfigTest.php @@ -59,16 +59,16 @@ class TaxConfigTest extends TestCase 'address1' => '400 Evelyn Pl', 'city' => 'Beverley Hills', 'state' => '', - 'postal_code' => 90210, + 'postal_code' => '', 'country_id' => 840, ]); - $this->assertEquals('CA', USStates::getState('90210')); + // $this->assertEquals('CA', USStates::getState('90210')); - // $this->bootApi($client); + $this->bootApi($client); - // $this->tp->updateClientTaxData(); + $this->tp->updateClientTaxData(); } From a35934f7b1d3aafa96e278b506fcb38d33c32eb9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 07:53:29 +1000 Subject: [PATCH 08/34] Update addresses --- app/Jobs/Client/UpdateTaxData.php | 2 +- app/Observers/ClientObserver.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php index 105360fda062..1f640f9a0b34 100644 --- a/app/Jobs/Client/UpdateTaxData.php +++ b/app/Jobs/Client/UpdateTaxData.php @@ -9,7 +9,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Jobs\Company; +namespace App\Jobs\Client; use App\DataProviders\USStates; use App\Models\Client; diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index 066afd3c71c1..814ee7f0c421 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -14,7 +14,7 @@ namespace App\Observers; use App\Models\Client; use App\Models\Webhook; use App\Jobs\Util\WebhookHandler; -use App\Jobs\Company\UpdateTaxData; +use App\Jobs\Client\UpdateTaxData; class ClientObserver { From 4767c1a14a0a991f6917020e5af7d4cd9579604c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 15:35:29 +1000 Subject: [PATCH 09/34] Updates for chart permissions --- app/Http/Controllers/ChartController.php | 11 +++++++---- app/Services/Chart/ChartService.php | 24 ++++++++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php index 8800768fc81d..651ded493fbb 100644 --- a/app/Http/Controllers/ChartController.php +++ b/app/Http/Controllers/ChartController.php @@ -13,8 +13,6 @@ namespace App\Http\Controllers; use App\Http\Requests\Chart\ShowChartRequest; use App\Services\Chart\ChartService; -use Illuminate\Http\Request; -use Illuminate\Http\Response; class ChartController extends BaseController { @@ -67,14 +65,19 @@ class ChartController extends BaseController */ public function totals(ShowChartRequest $request) { - $cs = new ChartService(auth()->user()->company()); + /** @var \App\Models\User auth()->user() */ + $user = auth()->user(); + $cs = new ChartService($user->company(), $user, $user->isAdmin()); return response()->json($cs->totals($request->input('start_date'), $request->input('end_date')), 200); } public function chart_summary(ShowChartRequest $request) { - $cs = new ChartService(auth()->user()->company()); + + /** @var \App\Models\User auth()->user() */ + $user = auth()->user(); + $cs = new ChartService($user->company(), $user, $user->isAdmin()); return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); } diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 52673fe5403b..877ab5393a2d 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -11,6 +11,7 @@ namespace App\Services\Chart; +use App\Models\User; use App\Models\Client; use App\Models\Company; use App\Models\Expense; @@ -20,11 +21,8 @@ class ChartService { use ChartQueries; - public Company $company; - - public function __construct(Company $company) + public function __construct(public Company $company, private User $user, private bool $is_admin) { - $this->company = $company; } /** @@ -37,8 +35,12 @@ class ChartService $currencies = Client::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) - ->distinct() - ->pluck('settings->currency_id as id'); + ->distinct(); + + if(!$this->is_admin) + $currencies->where('user_id', $this->user->id); + + $currencies->pluck('settings->currency_id as id'); /* Push the company currency on also */ $currencies->push((int) $this->company->settings->currency_id); @@ -47,8 +49,14 @@ class ChartService $expense_currencies = Expense::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) - ->distinct() - ->pluck('currency_id as id'); + ->distinct(); + + + if (!$this->is_admin) { + $expense_currencies->where('user_id', $this->user->id); + } + + $expense_currencies->pluck('currency_id as id'); /* Merge and filter by unique */ $currencies = $currencies->merge($expense_currencies)->unique(); From 9668f6ceb7b96c5ec9559c61c74b252d8526e441 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 17:41:54 +1000 Subject: [PATCH 10/34] Fixes for zoho imports --- .../Transformer/Zoho/ClientTransformer.php | 6 +- .../Transformer/Zoho/InvoiceTransformer.php | 76 ++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/app/Import/Transformer/Zoho/ClientTransformer.php b/app/Import/Transformer/Zoho/ClientTransformer.php index b5014960c4d7..6eafc140e6d3 100644 --- a/app/Import/Transformer/Zoho/ClientTransformer.php +++ b/app/Import/Transformer/Zoho/ClientTransformer.php @@ -28,7 +28,7 @@ class ClientTransformer extends BaseTransformer public function transform($data) { if (isset($data['Company Name']) && $this->hasClient($data['Company Name'])) { - throw new ImportException('Client already exists'); + throw new ImportException('Client already exists => '. $data['Company Name']); } $settings = new \stdClass; @@ -40,7 +40,7 @@ class ClientTransformer extends BaseTransformer $client_id_proxy = array_key_exists('Customer ID', $data) ? 'Customer ID' : 'Primary Contact ID'; - return [ + $data = [ 'company_id' => $this->company->id, 'name' => $this->getString($data, 'Display Name'), 'phone' => $this->getString($data, 'Phone'), @@ -72,5 +72,7 @@ class ClientTransformer extends BaseTransformer ], ], ]; + + return $data; } } diff --git a/app/Import/Transformer/Zoho/InvoiceTransformer.php b/app/Import/Transformer/Zoho/InvoiceTransformer.php index 1ec99eca3520..45b72b39ce88 100644 --- a/app/Import/Transformer/Zoho/InvoiceTransformer.php +++ b/app/Import/Transformer/Zoho/InvoiceTransformer.php @@ -40,7 +40,8 @@ class InvoiceTransformer extends BaseTransformer $transformed = [ 'company_id' => $this->company->id, - 'client_id' => $this->getClient($this->getString($invoice_data, 'Customer ID'), $this->getString($invoice_data, 'Primary Contact EmailID')), + // 'client_id' => $this->getClient($this->getString($invoice_data, 'Customer ID'), $this->getString($invoice_data, 'Primary Contact EmailID')), + 'client_id' => $this->harvestClient($invoice_data), 'number' => $this->getString($invoice_data, 'Invoice Number'), 'date' => isset($invoice_data['Invoice Date']) ? date('Y-m-d', strtotime($invoice_data['Invoice Date'])) : null, 'due_date' => isset($invoice_data['Due Date']) ? date('Y-m-d', strtotime($invoice_data['Due Date'])) : null, @@ -80,4 +81,77 @@ class InvoiceTransformer extends BaseTransformer return $transformed; } + + private function harvestClient($invoice_data) + { + + $client_email = $this->getString($invoice_data, 'Primary Contact EmailID'); + + if (strlen($client_email) > 2) { + $contacts = \App\Models\ClientContact::whereHas('client', function ($query) { + $query->where('is_deleted', false); + }) + ->where('company_id', $this->company->id) + ->where('email', $client_email); + + if ($contacts->count() >= 1) { + return $contacts->first()->client_id; + } + } + + $client_name = $this->getString($invoice_data, 'Customer Name'); + + if(strlen($client_name) >= 2) { + $client_name_search = \App\Models\Client::where('company_id', $this->company->id) + ->where('is_deleted', false) + ->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [ + strtolower(str_replace(' ', '', $client_name)), + ]); + + if ($client_name_search->count() >= 1) { + return $client_name_search->first()->id; + } + } + + $customer_id = $this->getString($invoice_data, 'Customer ID'); + + $client_id_search = \App\Models\Client::where('company_id', $this->company->id) + ->where('is_deleted', false) + ->where('id_number', trim($customer_id)); + + if ($client_id_search->count() >= 1) { + return $client_id_search->first()->id; + } + + + $client_repository = app()->make(\App\Repositories\ClientRepository::class); + $client_repository->import_mode = true; + + $client = $client_repository->save( + [ + 'name' => $client_name, + 'contacts' => [ + [ + 'first_name' => $client_name, + 'email' => $client_email, + ], + ], + 'address1' => $this->getString($invoice_data, 'Billing Address'), + 'city' => $this->getString($invoice_data, 'Billing City'), + 'state' => $this->getString($invoice_data, 'Billing State'), + 'postal_code' => $this->getString($invoice_data, 'Billing Code'), + 'country_id' => $this->getCountryId($this->getString($invoice_data, 'Billing Country')), + ], + + \App\Factory\ClientFactory::create( + $this->company->id, + $this->company->owner()->id + ) + ); + + $client_repository = null; + + return $client->id; + + } } From a66c9cc0461b478ffac9a74ee7dafd8ca9a43b89 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 17:50:05 +1000 Subject: [PATCH 11/34] Fixes for chart queries --- app/Services/Chart/ChartService.php | 18 ++++-------------- tests/Unit/Chart/ChartCurrencyTest.php | 6 +++--- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 877ab5393a2d..17660ae89df1 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -35,12 +35,8 @@ class ChartService $currencies = Client::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) - ->distinct(); - - if(!$this->is_admin) - $currencies->where('user_id', $this->user->id); - - $currencies->pluck('settings->currency_id as id'); + ->distinct() + ->pluck('settings->currency_id as id'); /* Push the company currency on also */ $currencies->push((int) $this->company->settings->currency_id); @@ -49,14 +45,8 @@ class ChartService $expense_currencies = Expense::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) - ->distinct(); - - - if (!$this->is_admin) { - $expense_currencies->where('user_id', $this->user->id); - } - - $expense_currencies->pluck('currency_id as id'); + ->distinct() + ->pluck('currency_id as id'); /* Merge and filter by unique */ $currencies = $currencies->merge($expense_currencies)->unique(); diff --git a/tests/Unit/Chart/ChartCurrencyTest.php b/tests/Unit/Chart/ChartCurrencyTest.php index 888d5c35f0a1..ed9558d6d79b 100644 --- a/tests/Unit/Chart/ChartCurrencyTest.php +++ b/tests/Unit/Chart/ChartCurrencyTest.php @@ -50,7 +50,7 @@ class ChartCurrencyTest extends TestCase $this->assertDatabaseHas('invoices', ['number' => 'db_record']); - $cs = new ChartService($this->company); + $cs = new ChartService($this->company, $this->user, true); // nlog($cs->getRevenueQuery(now()->subDays(20)->format('Y-m-d'), now()->addDays(100)->format('Y-m-d'))); $data = [ @@ -86,7 +86,7 @@ class ChartCurrencyTest extends TestCase 'settings' => $settings, ]); - $cs = new ChartService($this->company); + $cs = new ChartService($this->company, $this->user, true); $this->assertTrue(is_array($cs->getCurrencyCodes())); @@ -131,7 +131,7 @@ class ChartCurrencyTest extends TestCase ], ]; - $cs = new ChartService($this->company); + $cs = new ChartService($this->company, $this->user, true); // nlog($cs->totals(now()->subYears(10), now())); From 22d6d31399d124a0e4cbecb46072db1ce72ac95c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 17:52:53 +1000 Subject: [PATCH 12/34] Fixers for passphrase logic --- app/Http/Requests/Company/UpdateCompanyRequest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index 602bb3a0fe18..4b72c48ee8bf 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -83,6 +83,10 @@ class UpdateCompanyRequest extends Request $input['settings'] = (array)$this->filterSaveableSettings($input['settings']); } + if(array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) { + unset($input['e_invoice_certificate_passphrase']); + } + $this->replace($input); } From 9872dab2abe1923e5d8989cd688052f2dc5fb415 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 17:57:28 +1000 Subject: [PATCH 13/34] Fixes for chart queries --- app/Services/Chart/ChartService.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 17660ae89df1..c9a8be50eda7 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -35,6 +35,9 @@ class ChartService $currencies = Client::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) + ->when(!$this->is_admin, function ($query) { + $query->where('user_id', $this->user->id); + }) ->distinct() ->pluck('settings->currency_id as id'); @@ -45,6 +48,9 @@ class ChartService $expense_currencies = Expense::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) + ->when(!$this->is_admin, function ($query) { + $query->where('user_id', $this->user->id); + }) ->distinct() ->pluck('currency_id as id'); From 202efc205cd8fb359e11cb72c0a56ee7a6446f48 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 18:00:39 +1000 Subject: [PATCH 14/34] Add nullable encrypted casts helper --- app/Casts/EncryptedCast.php | 27 +++++++++++++++++++++++++++ app/Models/Company.php | 17 +++++++++-------- 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 app/Casts/EncryptedCast.php diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php new file mode 100644 index 000000000000..1d41b6ce333f --- /dev/null +++ b/app/Casts/EncryptedCast.php @@ -0,0 +1,27 @@ + ! is_null($value) ? encrypt($value) : null]; + } +} diff --git a/app/Models/Company.php b/app/Models/Company.php index a0d926b86f23..0a921451711a 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -11,18 +11,19 @@ namespace App\Models; -use App\DataMapper\CompanySettings; -use App\Models\Presenters\CompanyPresenter; -use App\Services\Notification\NotificationService; use App\Utils\Ninja; +use App\Casts\EncryptedCast; use App\Utils\Traits\AppSetup; -use App\Utils\Traits\CompanySettingsSaver; use App\Utils\Traits\MakesHash; -use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Notifications\Notification; +use App\DataMapper\CompanySettings; use Illuminate\Support\Facades\Cache; use Laracasts\Presenter\PresentableTrait; +use App\Utils\Traits\CompanySettingsSaver; +use Illuminate\Notifications\Notification; +use App\Models\Presenters\CompanyPresenter; +use App\Services\Notification\NotificationService; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * App\Models\Company @@ -358,7 +359,7 @@ class Company extends BaseModel 'deleted_at' => 'timestamp', 'client_registration_fields' => 'array', 'tax_data' => 'object', - 'e_invoice_certificate_passphrase' => 'encrypted', + 'e_invoice_certificate_passphrase' => EncryptedCast::class, ]; protected $with = []; From 04fa9dde8ca59382868ea99a77241b760aa4c540 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 18:17:11 +1000 Subject: [PATCH 15/34] Fixes for encrypted casts --- app/Casts/EncryptedCast.php | 4 ++-- .../migrations/2023_05_15_103212_e_invoice_ssl_storage.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php index 1d41b6ce333f..a65e00aab386 100644 --- a/app/Casts/EncryptedCast.php +++ b/app/Casts/EncryptedCast.php @@ -16,8 +16,8 @@ use Illuminate\Contracts\Database\Eloquent\CastsAttributes; class EncryptedCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) - { - return ! is_null($value) ? decrypt($value) : null; + {nlog($value); + return !$value ? null : decrypt($value); } public function set($model, string $key, $value, array $attributes) diff --git a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php index f6dab48bd698..2875e0221acb 100644 --- a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php +++ b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php @@ -15,7 +15,7 @@ return new class extends Migration { Schema::table('companies', function (Illuminate\Database\Schema\Blueprint $table) { $table->text('e_invoice_certificate')->nullable(); - $table->string('e_invoice_certificate_passphrase')->nullable(); + $table->text('e_invoice_certificate_passphrase')->nullable(); }); } From aec715fbfd3ee42705c2bc7ebfdb210e15b5971f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 18:19:04 +1000 Subject: [PATCH 16/34] Fixes for encrypted casts --- app/Casts/EncryptedCast.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php index a65e00aab386..da17fac8b916 100644 --- a/app/Casts/EncryptedCast.php +++ b/app/Casts/EncryptedCast.php @@ -16,8 +16,8 @@ use Illuminate\Contracts\Database\Eloquent\CastsAttributes; class EncryptedCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) - {nlog($value); - return !$value ? null : decrypt($value); + { + return ! is_null($value) ? decrypt($value) : null; } public function set($model, string $key, $value, array $attributes) From 5ee5426784962c4ec73bc5a4caf5fc4c629ce404 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 16 May 2023 20:37:04 +1000 Subject: [PATCH 17/34] User filters for chart / dashboard data --- app/Casts/EncryptedCast.php | 4 +- app/Jobs/Entity/EmailEntity.php | 17 ------- .../Invoice/InvoiceEmailedNotification.php | 14 ------ app/Services/Chart/ChartQueries.php | 47 +++++++++++++++---- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php index da17fac8b916..e13c69625d32 100644 --- a/app/Casts/EncryptedCast.php +++ b/app/Casts/EncryptedCast.php @@ -17,11 +17,11 @@ class EncryptedCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) { - return ! is_null($value) ? decrypt($value) : null; + return strlen($value) > 1 ? decrypt($value) : null; } public function set($model, string $key, $value, array $attributes) - { + { return [$key => ! is_null($value) ? encrypt($value) : null]; } } diff --git a/app/Jobs/Entity/EmailEntity.php b/app/Jobs/Entity/EmailEntity.php index 82cbef192937..41473f1b8fd1 100644 --- a/app/Jobs/Entity/EmailEntity.php +++ b/app/Jobs/Entity/EmailEntity.php @@ -150,23 +150,6 @@ class EmailEntity implements ShouldQueue return ''; } - /** - * @deprecated - * @unused - */ - // private function entityEmailFailed($message) - // { - // switch ($this->entity_string) { - // case 'invoice': - // event(new InvoiceWasEmailedAndFailed($this->invitation, $this->company, $message, $this->reminder_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); - // break; - - // default: - // // code... - // break; - // } - // } - /* Builds the email builder object */ private function resolveEmailBuilder() { diff --git a/app/Listeners/Invoice/InvoiceEmailedNotification.php b/app/Listeners/Invoice/InvoiceEmailedNotification.php index bf0859e2da25..1f37e7938dcb 100644 --- a/app/Listeners/Invoice/InvoiceEmailedNotification.php +++ b/app/Listeners/Invoice/InvoiceEmailedNotification.php @@ -63,20 +63,6 @@ class InvoiceEmailedNotification implements ShouldQueue if (($key = array_search('mail', $methods)) !== false) { unset($methods[$key]); - // $template = $event->template ?? ''; - - // if(isset($event->reminder)){ - - // $template = match($event->reminder){ - // 63 => 'reminder1', - // 64 => 'reminder2', - // 65 => 'reminder3', - // 66 => 'endless_reminder', - // default => '' - // }; - - // } - $nmo = new NinjaMailerObject; $nmo->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'invoice', $event->template))->build()); $nmo->company = $invoice->company; diff --git a/app/Services/Chart/ChartQueries.php b/app/Services/Chart/ChartQueries.php index f4d0fb509d39..7f284736c58b 100644 --- a/app/Services/Chart/ChartQueries.php +++ b/app/Services/Chart/ChartQueries.php @@ -23,20 +23,26 @@ trait ChartQueries */ public function getExpenseQuery($start_date, $end_date) { - return DB::select(DB::raw(' + $user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id; + + return DB::select(DB::raw(" SELECT sum(expenses.amount) as amount, IFNULL(expenses.currency_id, :company_currency) as currency_id FROM expenses WHERE expenses.is_deleted = 0 AND expenses.company_id = :company_id AND (expenses.date BETWEEN :start_date AND :end_date) + {$user_filter} GROUP BY currency_id - '), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); + "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); } public function getExpenseChartQuery($start_date, $end_date, $currency_id) { - return DB::select(DB::raw(' + + $user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id; + + return DB::select(DB::raw(" SELECT sum(expenses.amount) as total, expenses.date, @@ -45,9 +51,10 @@ trait ChartQueries WHERE (expenses.date BETWEEN :start_date AND :end_date) AND expenses.company_id = :company_id AND expenses.is_deleted = 0 + {$user_filter} GROUP BY expenses.date HAVING currency_id = :currency_id - '), [ + "), [ 'company_currency' => $this->company->settings->currency_id, 'currency_id' => $currency_id, 'company_id' => $this->company->id, @@ -61,15 +68,19 @@ trait ChartQueries */ public function getPaymentQuery($start_date, $end_date) { - return DB::select(DB::raw(' + + $user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id; + + return DB::select(DB::raw(" SELECT sum(payments.amount) as amount, IFNULL(payments.currency_id, :company_currency) as currency_id FROM payments WHERE payments.is_deleted = 0 + {$user_filter} AND payments.company_id = :company_id AND (payments.date BETWEEN :start_date AND :end_date) GROUP BY currency_id - '), [ + "), [ 'company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, @@ -79,7 +90,10 @@ trait ChartQueries public function getPaymentChartQuery($start_date, $end_date, $currency_id) { - return DB::select(DB::raw(' + + $user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id; + + return DB::select(DB::raw(" SELECT sum(payments.amount - payments.refunded) as total, payments.date, @@ -87,11 +101,12 @@ trait ChartQueries FROM payments WHERE payments.company_id = :company_id AND payments.is_deleted = 0 + {$user_filter} AND payments.status_id IN (4,5,6) AND (payments.date BETWEEN :start_date AND :end_date) GROUP BY payments.date HAVING currency_id = :currency_id - '), [ + "), [ 'company_currency' => $this->company->settings->currency_id, 'currency_id' => $currency_id, 'company_id' => $this->company->id, @@ -105,6 +120,9 @@ trait ChartQueries */ public function getOutstandingQuery($start_date, $end_date) { + + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.balance) as amount, @@ -116,6 +134,7 @@ trait ChartQueries WHERE invoices.status_id IN (2,3) AND invoices.company_id = :company_id AND clients.is_deleted = 0 + {$user_filter} AND invoices.is_deleted = 0 AND invoices.balance > 0 AND (invoices.date BETWEEN :start_date AND :end_date) @@ -125,6 +144,8 @@ trait ChartQueries public function getRevenueQuery($start_date, $end_date) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.paid_to_date) as paid_to_date, @@ -134,6 +155,7 @@ trait ChartQueries on invoices.client_id = clients.id WHERE invoices.company_id = :company_id AND clients.is_deleted = 0 + {$user_filter} AND invoices.is_deleted = 0 AND invoices.amount > 0 AND invoices.status_id IN (3,4) @@ -144,6 +166,8 @@ trait ChartQueries public function getInvoicesQuery($start_date, $end_date) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.amount) as invoiced_amount, @@ -153,6 +177,7 @@ trait ChartQueries on invoices.client_id = clients.id WHERE invoices.status_id IN (2,3,4) AND invoices.company_id = :company_id + {$user_filter} AND invoices.amount > 0 AND clients.is_deleted = 0 AND invoices.is_deleted = 0 @@ -163,6 +188,8 @@ trait ChartQueries public function getOutstandingChartQuery($start_date, $end_date, $currency_id) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.balance) as total, @@ -175,6 +202,7 @@ trait ChartQueries AND invoices.company_id = :company_id AND clients.is_deleted = 0 AND invoices.is_deleted = 0 + {$user_filter} AND (invoices.date BETWEEN :start_date AND :end_date) GROUP BY invoices.date HAVING currency_id = :currency_id @@ -190,6 +218,8 @@ trait ChartQueries public function getInvoiceChartQuery($start_date, $end_date, $currency_id) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.amount) as total, @@ -201,6 +231,7 @@ trait ChartQueries WHERE invoices.company_id = :company_id AND clients.is_deleted = 0 AND invoices.is_deleted = 0 + {$user_filter} AND invoices.status_id IN (2,3,4) AND (invoices.date BETWEEN :start_date AND :end_date) GROUP BY invoices.date From fde5e28cb13e36d5e504a990f4156de4a356c4c9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 09:16:43 +1000 Subject: [PATCH 18/34] Minor fixes for Stripe --- app/Jobs/Company/CreateCompany.php | 135 +++++++++++++++++- app/Observers/CompanyObserver.php | 6 + app/PaymentDrivers/Stripe/Charge.php | 4 +- app/PaymentDrivers/StripePaymentDriver.php | 3 +- .../Invoice/EInvoice/FacturaEInvoice.php | 66 +++++++++ 5 files changed, 210 insertions(+), 4 deletions(-) diff --git a/app/Jobs/Company/CreateCompany.php b/app/Jobs/Company/CreateCompany.php index 1c0328539a60..c05ff750c44a 100644 --- a/app/Jobs/Company/CreateCompany.php +++ b/app/Jobs/Company/CreateCompany.php @@ -13,12 +13,14 @@ namespace App\Jobs\Company; use App\Utils\Ninja; use App\Models\Company; +use App\Models\Country; use App\Libraries\MultiDB; use App\Utils\Traits\MakesHash; use App\DataMapper\Tax\TaxModel; use App\DataMapper\CompanySettings; use Illuminate\Foundation\Bus\Dispatchable; use App\DataMapper\ClientRegistrationFields; +use App\Factory\TaxRateFactory; class CreateCompany { @@ -53,6 +55,10 @@ class CreateCompany $settings->name = isset($this->request['name']) ? $this->request['name'] : ''; + if($country_id = $this->resolveCountry()){ + $settings->country_id = $country_id; + } + $company = new Company(); $company->account_id = $this->account->id; $company->company_key = $this->createHash(); @@ -74,8 +80,135 @@ class CreateCompany $company->subdomain = ''; } - $company->save(); + /** Location Specific Configuration */ + match($settings->country_id) { + '724' => $company = $this->spanishSetup($company), + '36' => $company = $this->australiaSetup($company), + default => $company->save(), + }; return $company; } + + /** + * Resolve Country + * + * @return string + */ + private function resolveCountry(): string + { + try{ + + $ip = request()->ip(); + + if(request()->hasHeader('cf-ipcountry')){ + + $c = Country::where('iso_3166_2', request()->header('cf-ipcountry'))->first(); + + if($c) + return (string)$c->id; + + } + + $details = json_decode(file_get_contents("http://ip-api.com/json/{$ip}")); + + if($details && property_exists($details, 'countryCode')){ + + $c = Country::where('iso_3166_2', $details->countryCode)->first(); + + if($c) + return (string)$c->id; + + } + } + catch(\Exception $e){ + nlog("Could not resolve country => {$e->getMessage()}"); + } + + return '840'; + + } + + private function spanishSetup(Company $company): Company + { + try { + + $custom_fields = new \stdClass; + $custom_fields->contact1 = "Rol|CONTABLE,FISCAL,GESTOR,RECEPTOR,TRAMITADOR,PAGADOR,PROPONENTE,B2B_FISCAL,B2B_PAYER,B2B_BUYER,B2B_COLLECTOR,B2B_SELLER,B2B_PAYMENT_RECEIVER,B2B_COLLECTION_RECEIVER,B2B_ISSUER"; + $custom_fields->contact2 = "Code|single_line_text"; + $custom_fields->contact3 = "Nombre|single_line_text"; + $custom_fields->client1 = "Administración Pública|switch"; + + $company->custom_fields = $custom_fields; + $company->enabled_item_tax_rates = 1; + + $settings = $company->settings; + $settings->language_id = '7'; + $settings->e_invoice_type = 'Facturae_3.2.2'; + $settings->currency_id = '3'; + $settings->timezone_id = '42'; + + $company->settings = $settings; + + $company->save(); + + $user = $company->account->users()->first(); + + $tax_rate = TaxRateFactory::create($company->id, $user->id); + $tax_rate->name = $company->tax_data->regions->EU->subregions->ES->tax_name; + $tax_rate->rate = $company->tax_data->regions->EU->subregions->ES->tax_rate; + $tax_rate->save(); + + return $company; + + } + catch(\Exception $e){ + nlog("SETUP: could not complete setup for Spanish Locale"); + } + + $company->save(); + + return $company; + + } + + private function australiaSetup(Company $company): Company + { + try { + + $company->enabled_item_tax_rates = 1; + $company->enabled_tax_rates = 1; + + $translations = new \stdClass; + $translations->invoice = "Tax Invoice"; + + $settings = $company->settings; + $settings->currency_id = '12'; + $settings->timezone_id = '109'; + $settings->translations = $translations; + + $company->settings = $settings; + + $company->save(); + + $user = $company->account->users()->first(); + + $tax_rate = TaxRateFactory::create($company->id, $user->id); + $tax_rate->name = $company->tax_data->regions->AU->subregions->AU->tax_name; + $tax_rate->rate = $company->tax_data->regions->AU->subregions->AU->tax_rate; + $tax_rate->save(); + + return $company; + + } + catch(\Exception $e){ + nlog("SETUP: could not complete setup for Spanish Locale"); + } + + $company->save(); + + return $company; + + } + } diff --git a/app/Observers/CompanyObserver.php b/app/Observers/CompanyObserver.php index 122518568224..93c2aeaf68c7 100644 --- a/app/Observers/CompanyObserver.php +++ b/app/Observers/CompanyObserver.php @@ -40,6 +40,12 @@ class CompanyObserver //fire event to build new custom portal domain \Modules\Admin\Jobs\Domain\CustomDomain::dispatch($company->getOriginal('portal_domain'), $company)->onQueue('domain'); } + + // if($company->wasChanged()) { + // nlog("updated event"); + // nlog($company->getChanges()); + // } + } /** diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 05833ccecc63..749b52770903 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -43,7 +43,7 @@ class Charge * Create a charge against a payment method. * @param ClientGatewayToken $cgt * @param PaymentHash $payment_hash - * @return bool success/failure + * @return mixed success/failure * @throws \Laracasts\Presenter\Exceptions\PresenterException */ public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) @@ -86,7 +86,7 @@ class Charge $data['off_session'] = true; } - $response = $this->stripe->createPaymentIntent($data, array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st", true)])); + $response = $this->stripe->createPaymentIntent($data); SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company); } catch (\Exception $e) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index bdac90e9c0b2..e5cbc4b895c4 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -777,7 +777,8 @@ class StripePaymentDriver extends BaseDriver ->where('token', $request->data['object']['payment_method']) ->first(); - $clientgateway->delete(); + if($clientgateway) + $clientgateway->delete(); return response()->json([], 200); } elseif ($request->data['object']['status'] == "pending") { diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index 5a2bcce7c35b..79840dad0beb 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -18,6 +18,7 @@ use josemmo\Facturae\FacturaeItem; use josemmo\Facturae\FacturaeParty; use Illuminate\Support\Facades\Storage; use josemmo\Facturae\Common\FacturaeSigner; +use josemmo\Facturae\FacturaeCentre; class FacturaEInvoice extends AbstractService { @@ -25,6 +26,24 @@ class FacturaEInvoice extends AbstractService private $calc; + private $centre_codes = [ + 'CONTABLE' => FacturaeCentre::ROLE_CONTABLE, + 'FISCAL' => FacturaeCentre::ROLE_FISCAL, + 'GESTOR' => FacturaeCentre::ROLE_GESTOR, + 'RECEPTOR' => FacturaeCentre::ROLE_RECEPTOR, + 'TRAMITADOR' => FacturaeCentre::ROLE_TRAMITADOR, + 'PAGADOR' => FacturaeCentre::ROLE_PAGADOR, + 'PROPONENTE' => FacturaeCentre::ROLE_PAGADOR, + 'B2B_FISCAL' => FacturaeCentre::ROLE_B2B_FISCAL, + 'B2B_PAYER' => FacturaeCentre::ROLE_B2B_PAYER, + 'B2B_BUYER' => FacturaeCentre::ROLE_B2B_BUYER, + 'B2B_COLLECTOR' => FacturaeCentre::ROLE_B2B_COLLECTOR, + 'B2B_SELLER' => FacturaeCentre::ROLE_B2B_SELLER, + 'B2B_PAYMENT_RECEIVER' => FacturaeCentre::ROLE_B2B_PAYMENT_RECEIVER , + 'B2B_COLLECTION_RECEIVER' => FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER , + 'B2B_ISSUER' => FacturaeCentre::ROLE_B2B_ISSUER, + ]; + // Facturae::SCHEMA_3_2 Invoice Format 3.2 // Facturae::SCHEMA_3_2_1 Invoice Format 3.2.1 // Facturae::SCHEMA_3_2_2 Invoice Format 3.2.2 @@ -111,6 +130,25 @@ class FacturaEInvoice extends AbstractService // FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER Collection receiver in FACeB2B // FacturaeCentre::ROLE_B2B_ISSUER Issuer in FACeB2B + /* + const ROLE_CONTABLE = "01"; + const ROLE_FISCAL = "01"; + const ROLE_GESTOR = "02"; + const ROLE_RECEPTOR = "02"; + const ROLE_TRAMITADOR = "03"; + const ROLE_PAGADOR = "03"; + const ROLE_PROPONENTE = "04"; + + const ROLE_B2B_FISCAL = "Fiscal"; + const ROLE_B2B_PAYER = "Payer"; + const ROLE_B2B_BUYER = "Buyer"; + const ROLE_B2B_COLLECTOR = "Collector"; + const ROLE_B2B_SELLER = "Seller"; + const ROLE_B2B_PAYMENT_RECEIVER = "Payment receiver"; + const ROLE_B2B_COLLECTION_RECEIVER = "Collection receiver"; + const ROLE_B2B_ISSUER = "Issuer"; + */ + public function __construct(public Invoice $invoice, private mixed $profile) { @@ -146,6 +184,33 @@ class FacturaEInvoice extends AbstractService } + /** Check if this is a public administration body */ + private function setFace(): array + { + $facturae_centres = []; + + if($this->invoice->client->custom_value1 == 'yes') + { + + foreach($this->invoice->client->contacts() as $contact) + { + + if(in_array($contact->custom_value1, array_keys($this->centre_codes))) + { + $facturae_centres[] = new FacturaeCentre([ + 'role' => $this->centre_codes[$contact->custom_value1], + 'code' => $contact->custom_value2, + 'name' => $contact->custom_value3, + ]); + } + + } + + } + + return $facturae_centres; + } + private function setPoNumber(): self { if(strlen($this->invoice->po_number) > 1) { @@ -280,6 +345,7 @@ class FacturaEInvoice extends AbstractService "fax" => "", "website" => substr($company->settings->website, 0, 50), "contactPeople" => substr($company->owner()->present()->name(), 0, 40), + 'centres' => $this->setFace(), // "cnoCnae" => "04647", // Clasif. Nacional de Act. Económicas // "ineTownCode" => "280796" // Cód. de municipio del INE ]); From 4e8c603d3a947fd883d50a068cf1bdcf65ec4e98 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 09:32:23 +1000 Subject: [PATCH 19/34] Fixes for reminder activities --- app/Jobs/Company/CreateCompany.php | 2 +- .../Invoice/InvoiceReminderEmailActivity.php | 11 ++++++++++- app/Models/Invoice.php | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/Jobs/Company/CreateCompany.php b/app/Jobs/Company/CreateCompany.php index c05ff750c44a..f288b1b9f52f 100644 --- a/app/Jobs/Company/CreateCompany.php +++ b/app/Jobs/Company/CreateCompany.php @@ -152,7 +152,7 @@ class CreateCompany $company->save(); - $user = $company->account->users()->first(); + $user = $this->account->users()->orderBy('id','asc')->first(); $tax_rate = TaxRateFactory::create($company->id, $user->id); $tax_rate->name = $company->tax_data->regions->EU->subregions->ES->tax_name; diff --git a/app/Listeners/Invoice/InvoiceReminderEmailActivity.php b/app/Listeners/Invoice/InvoiceReminderEmailActivity.php index 1cf801afec62..9b41a3b096b6 100644 --- a/app/Listeners/Invoice/InvoiceReminderEmailActivity.php +++ b/app/Listeners/Invoice/InvoiceReminderEmailActivity.php @@ -46,12 +46,21 @@ class InvoiceReminderEmailActivity implements ShouldQueue $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->invoice->user_id; + $reminder = match($event->template){ + 'reminder1' => 63, + 'reminder2' => 64, + 'reminder3' => 65, + 'reminder_endless' => 66, + 'endless_reminder' => 66, + default => 6, + }; + $fields->user_id = $user_id; $fields->invoice_id = $event->invitation->invoice_id; $fields->company_id = $event->invitation->company_id; $fields->client_contact_id = $event->invitation->client_contact_id; $fields->client_id = $event->invitation->invoice->client_id; - $fields->activity_type_id = $event->reminder; + $fields->activity_type_id = $reminder; $this->activity_repo->save($fields, $event->invitation, $event->event_vars); } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 54d37fb968ca..dfdf67c2691a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -824,7 +824,7 @@ class Invoice extends BaseModel case 'custom1': case 'custom2': case 'custom3': - event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); + event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); break; default: // code... From 446fb59b5b5f999510885fe432a5d8e65f8122b0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 09:55:43 +1000 Subject: [PATCH 20/34] Rate limiter for 404's --- app/Http/Middleware/QueryLogging.php | 2 +- app/Providers/RouteServiceProvider.php | 8 ++++++++ routes/client.php | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index a41de37b2cb2..b5a164a5e84b 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -32,7 +32,7 @@ class QueryLogging * @return mixed */ public function handle(Request $request, Closure $next) - { + {nlog("yoyo"); // Enable query logging for development if (! Ninja::isHosted() || ! config('beacon.enabled')) { return $next($request); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 44cd2b1b70a0..9bcdc1f4682a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -78,6 +78,14 @@ class RouteServiceProvider extends ServiceProvider } }); + RateLimiter::for('404', function (Request $request) { + if (Ninja::isSelfHost()) { + return Limit::none(); + } else { + return Limit::perMinute(25)->by($request->ip()); + } + }); + } /** diff --git a/routes/client.php b/routes/client.php index d8fd795cc9b7..ecb1dd11226f 100644 --- a/routes/client.php +++ b/routes/client.php @@ -158,4 +158,4 @@ Route::fallback(function () { abort(404); -}); +})->middleware('throttle:404'); From cfda826c8990f55f85f41d961da598c36ef3a626 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 09:56:25 +1000 Subject: [PATCH 21/34] Rate limiter for 404's --- app/Http/Middleware/QueryLogging.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index b5a164a5e84b..a41de37b2cb2 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -32,7 +32,7 @@ class QueryLogging * @return mixed */ public function handle(Request $request, Closure $next) - {nlog("yoyo"); + { // Enable query logging for development if (! Ninja::isHosted() || ! config('beacon.enabled')) { return $next($request); From 102ff163d4cd29d951db4708be633ac8359ae430 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 10:36:41 +1000 Subject: [PATCH 22/34] Fixes for requests in schedules --- .../Requests/Client/UpdateClientRequest.php | 4 +- .../UpdateRecurringQuoteRequest.php | 2 +- .../TaskScheduler/StoreSchedulerRequest.php | 3 +- .../TaskScheduler/UpdateSchedulerRequest.php | 2 +- lang/ca/texts.php | 233 ++++++++++-------- lang/fr_CA/texts.php | 6 + routes/api.php | 2 +- 7 files changed, 144 insertions(+), 108 deletions(-) diff --git a/app/Http/Requests/Client/UpdateClientRequest.php b/app/Http/Requests/Client/UpdateClientRequest.php index 8b4059c9aac4..24cf207bad0f 100644 --- a/app/Http/Requests/Client/UpdateClientRequest.php +++ b/app/Http/Requests/Client/UpdateClientRequest.php @@ -143,8 +143,8 @@ class UpdateClientRequest extends Request * down to the free plan setting properties which * are saveable * - * @param object $settings - * @return stdClass $settings + * @param \stdClass $settings + * @return \stdClass $settings */ private function filterSaveableSettings($settings) { diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index d7004836bd9c..d74dee599711 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -81,7 +81,7 @@ class UpdateRecurringQuoteRequest extends Request * off / optin / optout will reset the status of this field to off to allow * the client to choose whether to auto_bill or not. * - * @param enum $auto_bill off/always/optin/optout + * @param string $auto_bill off/always/optin/optout * * @return bool */ diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index f1e5f3b58082..016ec63e008d 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -60,6 +60,7 @@ class StoreSchedulerRequest extends Request $this->merge(['next_run_client' => $input['next_run']]); } - return $input; + $this->replace($input); + } } diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index d8bf21970622..4ed95aba2611 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -57,6 +57,6 @@ class UpdateSchedulerRequest extends Request $this->merge(['next_run_client' => $input['next_run']]); } - return $input; + $this->replace($input); } } diff --git a/lang/ca/texts.php b/lang/ca/texts.php index 3918a8576834..7d268f0c3eec 100644 --- a/lang/ca/texts.php +++ b/lang/ca/texts.php @@ -20,17 +20,17 @@ $LANG = array( 'additional_info' => 'Informació adicional', 'payment_terms' => 'Condicions de pagament', 'currency_id' => 'Moneda', - 'size_id' => 'Tamany de l\'empresa', - 'industry_id' => 'Industria', + 'size_id' => 'Mida de l\'empresa', + 'industry_id' => 'Sector industrial', 'private_notes' => 'Notes privades', 'invoice' => 'Factura', 'client' => 'Client', 'invoice_date' => 'Data factura', 'due_date' => 'Data venciment', 'invoice_number' => 'Número de factura', - 'invoice_number_short' => 'Factura #', + 'invoice_number_short' => 'Núm. factura', 'po_number' => 'Apartat de correus', - 'po_number_short' => 'Apartat de correus #', + 'po_number_short' => 'Núm apt correus', 'frequency_id' => 'Quant sovint', 'discount' => 'Descompte', 'taxes' => 'Impostos', @@ -44,7 +44,7 @@ $LANG = array( 'net_subtotal' => 'Net', 'paid_to_date' => 'Pagat', 'balance_due' => 'Pendent', - 'invoice_design_id' => 'Diseny', + 'invoice_design_id' => 'Disseny', 'terms' => 'Condicions', 'your_invoice' => 'La teva factura', 'remove_contact' => 'Esborra contacte', @@ -54,22 +54,22 @@ $LANG = array( 'enable' => 'Activa', 'learn_more' => 'Aprèn més', 'manage_rates' => 'Administrar tarifes', - 'note_to_client' => 'Nota al client', - 'invoice_terms' => 'Condicions factura', + 'note_to_client' => 'Nota per al client', + 'invoice_terms' => 'Condicions de la factura', 'save_as_default_terms' => 'Guarda com a condicions per defecte', 'download_pdf' => 'Descarrega PDF', 'pay_now' => 'Paga ara', 'save_invoice' => 'Guarda factura', - 'clone_invoice' => 'Clonar a fatura', - 'archive_invoice' => 'Arxivar factura', + 'clone_invoice' => 'Clona a fatura', + 'archive_invoice' => 'Arxiva factura', 'delete_invoice' => 'Suprimex factura', - 'email_invoice' => 'Enviar factura per correu electrónic', - 'enter_payment' => 'Introduir pagament', + 'email_invoice' => 'Envia factura per correu electrònic', + 'enter_payment' => 'Introdueix pagament', 'tax_rates' => 'Impostos', 'rate' => 'Preu', 'settings' => 'Paràmetres', - 'enable_invoice_tax' => 'Activar especificar impost', - 'enable_line_item_tax' => 'Activar especificar impost per línea', + 'enable_invoice_tax' => 'Activa especificar impost', + 'enable_line_item_tax' => 'Activa especificar impost per línea', 'dashboard' => 'Tauler de control', 'dashboard_totals_in_all_currencies_help' => 'Nota: afegiu un :link anomenat ":name" per mostrar els totals utilitzant una moneda base única.', 'clients' => 'Clients', @@ -83,28 +83,33 @@ $LANG = array( 'company_details' => 'Detalls de l\'empresa', 'online_payments' => 'Pagaments en línia', 'notifications' => 'Notificacions', - 'import_export' => 'Importar | Exportar', + 'import_export' => 'Importació | Exportació', 'done' => 'Fet', 'save' => 'Desa', - 'create' => 'Crear', - 'upload' => 'Penjar', - 'import' => 'Importar', - 'download' => 'Baixar', - 'cancel' => 'Cancel·lar', - 'close' => 'Tancar', + 'create' => 'Crea', + 'upload' => 'Penja', + 'import' => 'Importa', + 'download' => 'Baixa', + 'cancel' => 'Cancel·la', + 'close' => 'Tanca', 'provide_email' => 'Si us plau, indica una adreça de correu electrònic vàlida', 'powered_by' => 'Funciona amb', - 'no_items' => 'No hi ha conceptes', + 'no_items' => 'No hi ha cap element', 'recurring_invoices' => 'Factures recurrents', 'recurring_help' => '

Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.

-

Utilitzeu: MONTH,: TRIMESTRE o: YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: MES-1

-', +

Utilitzeu :MONTH, :QUARTER o :YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: :MONTH-1

+

Exemples de variables dinàmiques de factures:

+
    +
  • "Quota gimnàs pel mes de :MONTH" >> "Quota gimnàs pel mes de juliol"
  • +
  • "Subscripció anual :YEAR+1" >> "Subscripció anual 2015"
  • +
  • "Pagament consultor del :QUARTER+1" >> "Pagament consultor del Q2"
  • +
', 'recurring_quotes' => 'Pressupostos recurrents', 'in_total_revenue' => 'en ingressos totals', 'billed_client' => 'client facturat', 'billed_clients' => 'clients facturats', - 'active_client' => 'Client actiu', - 'active_clients' => 'Clients actius', + 'active_client' => 'client actiu', + 'active_clients' => 'clients actius', 'invoices_past_due' => 'Factures vençudes', 'upcoming_invoices' => 'Properes factures', 'average_invoice' => 'Mitjana de facturació', @@ -2407,7 +2412,7 @@ $LANG = array( 'currency_vanuatu_vatu' => 'Vanuatu Vatu', 'currency_cuban_peso' => 'Cuban Peso', - 'currency_bz_dollar' => 'BZ Dollar', + 'currency_bz_dollar' => 'Dòlar BZ', 'review_app_help' => 'We hope you\'re enjoying using the app.
If you\'d consider :link we\'d greatly appreciate it!', 'writing_a_review' => 'escriu una ressenya', @@ -4007,6 +4012,7 @@ $LANG = array( 'notification_invoice_reminder1_sent_subject' => 'Reminder 1 for Invoice :invoice was sent to :client', 'notification_invoice_reminder2_sent_subject' => 'Reminder 2 for Invoice :invoice was sent to :client', 'notification_invoice_reminder3_sent_subject' => 'Reminder 3 for Invoice :invoice was sent to :client', + 'notification_invoice_custom_sent_subject' => 'Custom reminder for Invoice :invoice was sent to :client', 'notification_invoice_reminder_endless_sent_subject' => 'Endless reminder for Invoice :invoice was sent to :client', 'assigned_user' => 'Assigned User', 'setup_steps_notice' => 'To proceed to next step, make sure you test each section.', @@ -4255,9 +4261,9 @@ $LANG = array( 'klarna' => 'Klarna', 'eps' => 'EPS', 'becs' => 'BECS Direct Debit', - 'bacs' => 'BACS Direct Debit', - 'payment_type_BACS' => 'BACS Direct Debit', - 'missing_payment_method' => 'Please add a payment method first, before trying to pay.', + 'bacs' => 'Dèbit directe BACS', + 'payment_type_BACS' => 'Dèbit directe BACS', + 'missing_payment_method' => 'Introduïu un sistema de pagament primer, abans d\'intentar pagar.', 'becs_mandate' => 'By providing your bank account details, you agree to this Direct Debit Request and the Direct Debit Request service agreement, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.', 'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.', 'direct_debit' => 'Direct Debit', @@ -4384,7 +4390,7 @@ $LANG = array( 'imported_customers' => 'Successfully started importing customers', 'login_success' => 'Successful Login', 'login_failure' => 'Failed Login', - 'exported_data' => 'Once the file is ready you\'ll receive an email with a download link', + 'exported_data' => 'Quan l\'arxiu estigui llest rebreu un missatge de correu amb l\'enllaç de descàrrega', 'include_deleted_clients' => 'Include Deleted Clients', 'include_deleted_clients_help' => 'Load records belonging to deleted clients', 'step_1_sign_in' => 'Step 1: Sign In', @@ -4473,7 +4479,7 @@ $LANG = array( 'activity_123' => ':user deleted recurring expense :recurring_expense', 'activity_124' => ':user restored recurring expense :recurring_expense', 'fpx' => "FPX", - 'to_view_entity_set_password' => 'To view the :entity you need to set a password.', + 'to_view_entity_set_password' => 'Per a veure :entity haureu d\'establir una contrasenya.', 'unsubscribe' => 'Unsubscribe', 'unsubscribed' => 'Unsubscribed', 'unsubscribed_text' => 'You have been removed from notifications for this document', @@ -4571,7 +4577,7 @@ $LANG = array( 'purchase_order_number' => 'Purchase Order Number', 'purchase_order_number_short' => 'Purchase Order #', 'inventory_notification_subject' => 'Inventory threshold notification for product: :product', - 'inventory_notification_body' => 'Threshold of :amount has been reached for product: :product', + 'inventory_notification_body' => 'S\'ha arribat al límit de :amount per al producte :product ', 'activity_130' => ':user created purchase order :purchase_order', 'activity_131' => ':user updated purchase order :purchase_order', 'activity_132' => ':user archived purchase order :purchase_order', @@ -4603,7 +4609,7 @@ $LANG = array( 'vendor_document_upload' => 'Vendor Document Upload', 'vendor_document_upload_help' => 'Enable vendors to upload documents', 'are_you_enjoying_the_app' => 'Are you enjoying the app?', - 'yes_its_great' => 'Yes, it\'s great!', + 'yes_its_great' => 'Sí, genial!', 'not_so_much' => 'Not so much', 'would_you_rate_it' => 'Great to hear! Would you like to rate it?', 'would_you_tell_us_more' => 'Sorry to hear it! Would you like to tell us more?', @@ -4908,75 +4914,75 @@ $LANG = array( 'update_payment' => 'Update Payment', 'markup' => 'Markup', 'unlock_pro' => 'Unlock Pro', - 'upgrade_to_paid_plan_to_schedule' => 'Upgrade to a paid plan to create schedules', - 'next_run' => 'Next Run', - 'all_clients' => 'All Clients', - 'show_aging_table' => 'Show Aging Table', - 'show_payments_table' => 'Show Payments Table', + 'upgrade_to_paid_plan_to_schedule' => 'Actualitzeu a un pla de pagament per a crear calendaris', + 'next_run' => 'Següent volta', + 'all_clients' => 'Tots els clients', + 'show_aging_table' => 'Veure taula de compliment', + 'show_payments_table' => 'Veure taula de pagaments', 'email_statement' => 'Email Statement', - 'once' => 'Once', - 'schedules' => 'Schedules', - 'new_schedule' => 'New Schedule', - 'edit_schedule' => 'Edit Schedule', - 'created_schedule' => 'Successfully created schedule', - 'updated_schedule' => 'Successfully updated schedule', - 'archived_schedule' => 'Successfully archived schedule', - 'deleted_schedule' => 'Successfully deleted schedule', - 'removed_schedule' => 'Successfully removed schedule', - 'restored_schedule' => 'Successfully restored schedule', - 'search_schedule' => 'Search Schedule', - 'search_schedules' => 'Search Schedules', - 'update_product' => 'Update Product', - 'create_purchase_order' => 'Create Purchase Order', - 'update_purchase_order' => 'Update Purchase Order', - 'sent_invoice' => 'Sent Invoice', - 'sent_quote' => 'Sent Quote', - 'sent_credit' => 'Sent Credit', - 'sent_purchase_order' => 'Sent Purchase Order', - 'image_url' => 'Image URL', - 'max_quantity' => 'Max Quantity', - 'test_url' => 'Test URL', - 'auto_bill_help_off' => 'Option is not shown', - 'auto_bill_help_optin' => 'Option is shown but not selected', - 'auto_bill_help_optout' => 'Option is shown and selected', - 'auto_bill_help_always' => 'Option is not shown', - 'view_all' => 'View All', - 'edit_all' => 'Edit All', - 'accept_purchase_order_number' => 'Accept Purchase Order Number', - 'accept_purchase_order_number_help' => 'Enable clients to provide a PO number when approving a quote', - 'from_email' => 'From Email', - 'show_preview' => 'Show Preview', - 'show_paid_stamp' => 'Show Paid Stamp', - 'show_shipping_address' => 'Show Shipping Address', - 'no_documents_to_download' => 'There are no documents in the selected records to download', - 'pixels' => 'Pixels', - 'logo_size' => 'Logo Size', - 'failed' => 'Failed', - 'client_contacts' => 'Client Contacts', - 'sync_from' => 'Sync From', - 'gateway_payment_text' => 'Invoices: :invoices for :amount for client :client', - 'gateway_payment_text_no_invoice' => 'Payment with no invoice for amount :amount for client :client', - 'click_to_variables' => 'Client here to see all variables.', - 'ship_to' => 'Ship to', - 'stripe_direct_debit_details' => 'Please transfer into the nominated bank account above.', - 'branch_name' => 'Branch Name', - 'branch_code' => 'Branch Code', - 'bank_name' => 'Bank Name', - 'bank_code' => 'Bank Code', + 'once' => 'Una volta', + 'schedules' => 'Calendaris', + 'new_schedule' => 'Nou calendari', + 'edit_schedule' => 'Edita calendari', + 'created_schedule' => 'Calendari creat correctament', + 'updated_schedule' => 'Calendari editat correctament', + 'archived_schedule' => 'Calendari arxivat correctament', + 'deleted_schedule' => 'Calendari esborrat correctament', + 'removed_schedule' => 'Calendari eliminat correctament ', + 'restored_schedule' => 'Calendari restaurat correctament', + 'search_schedule' => 'Cerca calendari', + 'search_schedules' => 'Cerca calendaris', + 'update_product' => 'Actualitza producte', + 'create_purchase_order' => 'Crea ordre de compra', + 'update_purchase_order' => 'Actualitza ordre de compra', + 'sent_invoice' => 'Factura enviada', + 'sent_quote' => 'Pressupost enviat', + 'sent_credit' => 'Crèdit enviat', + 'sent_purchase_order' => 'Ordre de compra enviada', + 'image_url' => 'URL de la imatge', + 'max_quantity' => 'Quantitat màxims', + 'test_url' => 'URL de prova', + 'auto_bill_help_off' => 'L\'opció no es mostra', + 'auto_bill_help_optin' => 'L\'opció es mostra però no se selecciona', + 'auto_bill_help_optout' => 'L\'opció es mostra i selecciona', + 'auto_bill_help_always' => 'L\'opció no es mostra', + 'view_all' => 'Mostra-ho tot', + 'edit_all' => 'Edita-ho tot', + 'accept_purchase_order_number' => 'Accepta el número d\'ordre de compra', + 'accept_purchase_order_number_help' => 'Permet als clients afegir un número d\'ordre de compra quan aprovin un pressupost', + 'from_email' => 'De correu electrònic', + 'show_preview' => 'Mostra previsualització', + 'show_paid_stamp' => 'Mostra segell de "Pagat"', + 'show_shipping_address' => 'Mostra adreça d\'enviament', + 'no_documents_to_download' => 'No hi ha cap documents a descarregar en cap dels elements selecctionats', + 'pixels' => 'Píxels', + 'logo_size' => 'Mida del logo', + 'failed' => 'Fallat', + 'client_contacts' => 'Contactes del client', + 'sync_from' => 'Sincronitza de', + 'gateway_payment_text' => 'Factures: :invoices de :amount per al client :client', + 'gateway_payment_text_no_invoice' => 'Pagament sense factura de :amount per al client :client', + 'click_to_variables' => 'Pitgeu aquí per veure totes les variables.', + 'ship_to' => 'Envia a', + 'stripe_direct_debit_details' => 'Transferiu al compte bancari especificat a dalt, si us plau.', + 'branch_name' => 'Nom de l\'oficina', + 'branch_code' => 'Codi de l\'oficina', + 'bank_name' => 'Nom del banc', + 'bank_code' => 'Codi del banc', 'bic' => 'BIC', - 'change_plan_description' => 'Upgrade or downgrade your current plan.', - 'add_company_logo' => 'Add Logo', - 'add_stripe' => 'Add Stripe', - 'invalid_coupon' => 'Invalid Coupon', - 'no_assigned_tasks' => 'No billable tasks for this project', - 'authorization_failure' => 'Insufficient permissions to perform this action', - 'authorization_sms_failure' => 'Please verify your account to send emails.', - 'white_label_body' => 'Thank you for purchasing a white label license.

Your license key is:

:license_key', + 'change_plan_description' => 'Actualitzeu el vostre pla.', + 'add_company_logo' => 'Afegiu logo', + 'add_stripe' => 'Afegiu Stripe', + 'invalid_coupon' => 'Cupó invàlid', + 'no_assigned_tasks' => 'No hi ha cap tasca cobrable a aquest projecte', + 'authorization_failure' => 'Permisos insuficients per a realitzar aquesta acció', + 'authorization_sms_failure' => 'Verifiqueu el vostre compte per a poder enviar missatges de correu.', + 'white_label_body' => 'Gràcies per comprar una llicència de marca blanca.

La vostra clau de llicència és:

:license_key', 'payment_type_Klarna' => 'Klarna', - 'payment_type_Interac E Transfer' => 'Interac E Transfer', + 'payment_type_Interac E Transfer' => 'Transferència Interac E', 'xinvoice_payable' => 'Payable within :payeddue days net until :paydate', 'xinvoice_no_buyers_reference' => "No buyer's reference given", - 'xinvoice_online_payment' => 'The invoice needs to be payed online via the provided link', + 'xinvoice_online_payment' => 'The invoice needs to be paid online via the provided link', 'pre_payment' => 'Pre Payment', 'number_of_payments' => 'Number of payments', 'number_of_payments_helper' => 'The number of times this payment will be made', @@ -4984,11 +4990,6 @@ $LANG = array( 'notification_payment_emailed' => 'Payment :payment was emailed to :client', 'notification_payment_emailed_subject' => 'Payment :payment was emailed', 'record_not_found' => 'Record not found', - 'product_tax_exempt' => 'Product Tax Exempt', - 'product_type_physical' => 'Physical Goods', - 'product_type_digital' => 'Digital Goods', - 'product_type_service' => 'Services', - 'product_type_freight' => 'Shipping', 'minimum_payment_amount' => 'Minimum Payment Amount', 'client_initiated_payments' => 'Client Initiated Payments', 'client_initiated_payments_help' => 'Support making a payment in the client portal without an invoice', @@ -5057,6 +5058,34 @@ $LANG = array( 'here' => 'here', 'industry_Restaurant & Catering' => 'Restaurant & Catering', 'show_credits_table' => 'Show Credits Table', + 'manual_payment' => 'Payment Manual', + 'tax_summary_report' => 'Tax Summary Report', + 'tax_category' => 'Tax Category', + 'physical_goods' => 'Physical Goods', + 'digital_products' => 'Digital Products', + 'services' => 'Services', + 'shipping' => 'Shipping', + 'tax_exempt' => 'Tax Exempt', + 'late_fee_added_locked_invoice' => 'Late fee for invoice :invoice added on :date', + 'lang_Khmer' => 'Khmer', + 'routing_id' => 'Routing ID', + 'enable_e_invoice' => 'Enable E-Invoice', + 'e_invoice_type' => 'E-Invoice Type', + 'reduced_tax' => 'Reduced Tax', + 'override_tax' => 'Override Tax', + 'zero_rated' => 'Zero Rated', + 'reverse_tax' => 'Reverse Tax', + 'updated_tax_category' => 'Successfully updated the tax category', + 'updated_tax_categories' => 'Successfully updated the tax categories', + 'set_tax_category' => 'Set Tax Category', + 'payment_manual' => 'Payment Manual', + 'expense_payment_type' => 'Expense Payment Type', + 'payment_type_Cash App' => 'Cash App', + 'rename' => 'Rename', + 'renamed_document' => 'Successfully renamed document', + 'e_invoice' => 'E-Invoice', + 'light_dark_mode' => 'Light/Dark Mode', + 'activities' => 'Activities', ); diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 4af6cff18301..380a7c18a10c 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -5072,6 +5072,12 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'set_tax_category' => 'Définir la catégorie de taxe', 'payment_manual' => 'Paiement manuel', 'expense_payment_type' => 'Type de paiement de dépense', + 'payment_type_Cash App' => 'Cash App', + 'rename' => 'Renommer', + 'renamed_document' => 'Le document a été renommé', + 'e_invoice' => 'Facture électronique', + 'light_dark_mode' => 'Mode clair/sombre', + 'activities' => 'Activités', ); diff --git a/routes/api.php b/routes/api.php index 350e547333e5..0806d44c6ac5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -394,4 +394,4 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); -Route::fallback([BaseController::class, 'notFound']); \ No newline at end of file +Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404'); \ No newline at end of file From 618c3a41e792f6d409aad1d3ec66ffbd17e42a63 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 10:45:56 +1000 Subject: [PATCH 23/34] Update scheduler list for reports --- .../TaskScheduler/UpdateSchedulerRequest.php | 2 +- app/Services/Scheduler/EmailReport.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index 4ed95aba2611..48c52a3e3e0f 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -42,7 +42,7 @@ class UpdateSchedulerRequest extends Request 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'], - 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_summary_report,ar_detail_report,tax_summary_report,user_sales_report,client_sales_report,client_balance_report,product_sales_report'], + 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'], 'parameters.date_key' => ['bail','sometimes', 'string'], ]; diff --git a/app/Services/Scheduler/EmailReport.php b/app/Services/Scheduler/EmailReport.php index 8ec4c020630e..ce76e54fad04 100644 --- a/app/Services/Scheduler/EmailReport.php +++ b/app/Services/Scheduler/EmailReport.php @@ -77,13 +77,13 @@ class EmailReport match($this->scheduler->parameters['report_name']) { - 'product_sales_report' => $export = (new ProductSalesExport($this->scheduler->company, $data)), - 'email_ar_detailed_report' => $export = (new ARDetailReport($this->scheduler->company, $data)), - 'email_ar_summary_report' => $export = (new ARSummaryReport($this->scheduler->company, $data)), - 'email_tax_summary_report' => $export = (new TaxSummaryReport($this->scheduler->company, $data)), - 'email_client_balance_report' => $export = (new ClientBalanceReport($this->scheduler->company, $data)), - 'email_client_sales_report' => $export = (new ClientSalesReport($this->scheduler->company, $data)), - 'email_user_sales_report' => $export = (new UserSalesReport($this->scheduler->company, $data)), + 'product_sales' => $export = (new ProductSalesExport($this->scheduler->company, $data)), + 'ar_detailed' => $export = (new ARDetailReport($this->scheduler->company, $data)), + 'ar_summary' => $export = (new ARSummaryReport($this->scheduler->company, $data)), + 'tax_summary' => $export = (new TaxSummaryReport($this->scheduler->company, $data)), + 'client_balance' => $export = (new ClientBalanceReport($this->scheduler->company, $data)), + 'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)), + 'user_sales' => $export = (new UserSalesReport($this->scheduler->company, $data)), 'clients' => $export = (new ClientExport($this->scheduler->company, $data)), 'client_contacts' => $export = (new ContactExport($this->scheduler->company, $data)), 'credits' => $export = (new CreditExport($this->scheduler->company, $data)), From 4e92f9ad3cc61f303d93e52679089a272794a7f9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 10:46:13 +1000 Subject: [PATCH 24/34] Update scheduler list for reports --- app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index 016ec63e008d..f613c7881a55 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -45,7 +45,7 @@ class StoreSchedulerRequest extends Request 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'], - 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_summary_report,ar_detail_report,tax_summary_report,user_sales_report,client_sales_report,client_balance_report,product_sales_report'], + 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'], 'parameters.date_key' => ['bail','sometimes', 'string'], ]; From 36269e4e1b75758ab9c48d2d3f51eb282a149d27 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 11:15:50 +1000 Subject: [PATCH 25/34] Fixes for scheduler tests --- app/DataMapper/Tax/BaseRule.php | 4 ++-- app/Helpers/Invoice/InvoiceItemSum.php | 5 ++++- tests/Feature/Scheduler/SchedulerTest.php | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index fc71eaa498ae..2b114e9990aa 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -148,8 +148,8 @@ class BaseRule implements RuleInterface { /* If the client Country is not in the region_codes, we force the company country onto the client? @TODO */ if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) { - $this->client->country_id = $this->invoice->company->settings->country_id; - $this->client->saveQuietly(); + // $this->client->country_id = $this->invoice->company->settings->country_id; + // $this->client->saveQuietly(); nlog('Automatic tax calculations not supported for this country - defaulting to company country'); diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index fd1cc9c390ff..5930f8856bbb 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -144,7 +144,10 @@ class InvoiceItemSum return $this; } - if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions + if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) && in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions + + nlog($this->client->country->iso_3166_2); + nlog($this->client->company->country()->iso_3166_2); $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 579186e7c597..c4f58e235ff5 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -100,7 +100,7 @@ class SchedulerTest extends TestCase 'clients' => [], 'report_keys' => [], 'client_id' => $this->client->hashed_id, - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; @@ -147,7 +147,7 @@ class SchedulerTest extends TestCase 'clients' => [$this->client->hashed_id], 'report_keys' => [], 'client_id' => null, - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; @@ -193,7 +193,7 @@ class SchedulerTest extends TestCase 'clients' => [], 'report_keys' => [], 'client_id' => null, - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; @@ -234,7 +234,7 @@ class SchedulerTest extends TestCase 'parameters' => [ 'date_range' => EmailStatement::LAST_MONTH, 'clients' => [], - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; From 8908bc318cf2890afbc4741e52312108f9af4a5d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 14:07:48 +1000 Subject: [PATCH 26/34] Fixes for taxes --- app/DataMapper/Tax/BaseRule.php | 105 +++++++++++++----- app/DataMapper/Tax/US/Rule.php | 15 +-- app/DataProviders/USStates.php | 2 +- app/Helpers/Invoice/InvoiceItemSum.php | 3 - app/Models/Company.php | 1 + app/Services/Tax/Providers/TaxProvider.php | 20 ++-- ...023_05_15_103212_e_invoice_ssl_storage.php | 2 + 7 files changed, 95 insertions(+), 53 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 2b114e9990aa..c724a3efd5aa 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -16,6 +16,7 @@ use App\Models\Invoice; use App\Models\Product; use App\DataProviders\USStates; use App\DataMapper\Tax\ZipTax\Response; +use App\Services\Tax\Providers\TaxProvider; class BaseRule implements RuleInterface { @@ -103,9 +104,6 @@ class BaseRule implements RuleInterface /** EU TAXES */ - /** US TAXES */ - /** US TAXES */ - public string $tax_name1 = ''; public float $tax_rate1 = 0; @@ -129,75 +127,132 @@ class BaseRule implements RuleInterface { return $this; } - + + /** + * Initializes the tax rule for the entity. + * + * @param mixed $invoice + * @return self + */ public function setEntity(mixed $invoice): self { $this->invoice = $invoice; $this->client = $invoice->client; - $this->configTaxData() - ->resolveRegions(); + $this->resolveRegions() + ->configTaxData(); $this->tax_data = new Response($this->invoice->tax_data); return $this; } - + + /** + * Configigures the Tax Data for the entity + * + * @return self + */ private function configTaxData(): self { - /* If the client Country is not in the region_codes, we force the company country onto the client? @TODO */ + /* We should only apply taxes for configured states */ if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) { - // $this->client->country_id = $this->invoice->company->settings->country_id; - // $this->client->saveQuietly(); - nlog('Automatic tax calculations not supported for this country - defaulting to company country'); - + nlog("With new logic, we should never see this"); } /** Harvest the client_region */ - $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; /** If the tax data is already set and the invoice is marked as sent, do not adjust the rates */ if($this->invoice->tax_data && $this->invoice->status_id > 1) return $this; - //Pass the client tax data into the invoice tax data object - $tax_data = is_object($this->invoice->client->tax_data) ? $this->invoice->client->tax_data : new Response([]); + /** + * Origin - Company Tax Data + * Destination - Client Tax Data + * + */ + $tax_data = new Response([]); - /** If no Origin / Destination has been set and the seller and client sub regions are not the same, force destination tax */ - if(strlen($this->invoice->tax_data?->originDestination) == 0 && $this->client->company->tax_data->seller_subregion != $this->client_subregion) { - $tax_data->originDestination = "D"; - $tax_data->geoState = $this->client_subregion; + if($this->seller_region == 'US'){ + + $company = $this->invoice->company; + + /** If no company tax data has been configured, lets do that now. */ + if(!$company->origin_tax_data && \DB::transactionLevel() == 0) + { + + $tp = new TaxProvider($company); + $tp->updateCompanyTaxData(); + $company->fresh(); - if($this->invoice instanceof Invoice) { - $this->invoice->tax_data = $tax_data; - $this->invoice->saveQuietly(); } - + + /** If we are in a Origin based state, force the company tax here */ + if($company->origin_tax_data->originDestination == 'O' && ($company->tax_data->seller_subregion == $this->client_subregion)) { + + $tax_data = $company->origin_tax_data; + + } + else{ + + /** Ensures the client tax data has been updated */ + if(!$this->client->tax_data && \DB::transactionLevel() == 0) { + + $tp = new TaxProvider($company, $this->client); + $tp->updateClientTaxData(); + $this->client->fresh(); + } + + $tax_data = $this->client->tax_data; + + } + } + /** Applies the tax data to the invoice */ + if($this->invoice instanceof Invoice && \DB::transactionLevel() == 0) { + $this->invoice->tax_data = $tax_data; + $this->invoice->saveQuietly(); + } + return $this; + } - // Refactor to support switching between shipping / billing country / region / subregion + + /** + * Resolve Regions & Subregions + * + * @return self + */ private function resolveRegions(): self { + + $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; match($this->client_region){ - 'US' => $this->client_subregion = strlen($this->invoice?->tax_data?->geoState) > 1 ? $this->invoice?->tax_data?->geoState : $this->getUSState(), + 'US' => $this->client_subregion = strlen($this->invoice?->client?->tax_data?->geoState) > 1 ? $this->invoice->client->tax_data->geoState : $this->getUSState(), 'EU' => $this->client_subregion = $this->client->country->iso_3166_2, 'AU' => $this->client_subregion = 'AU', default => $this->client_subregion = $this->client->country->iso_3166_2, }; return $this; + } private function getUSState(): string { try { + + $states = USStates::$states; + + if(isset($states[$this->client->state])) + return $this->client->state; + return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code); + } catch (\Exception $e) { return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA'; } diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index c1005a42cb86..2a4e46b1dc33 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -159,18 +159,8 @@ class Rule extends BaseRule implements RuleInterface if($this->tax_data?->stateSalesTax == 0) { - if($this->tax_data->originDestination == "O"){ - $tax_region = $this->client->company->tax_data->seller_subregion; - $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->US->subregions->{$tax_region}->tax_rate; - $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; - } else { - $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; - $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; - - if($this->client_region == 'US') - $this->tax_name1 = "{$this->client_subregion} ".$this->tax_name1; - - } + $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; + $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; return $this; } @@ -178,7 +168,6 @@ class Rule extends BaseRule implements RuleInterface $this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; - return $this; } diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php index d973bfe5f4e7..3f07c4ecf427 100644 --- a/app/DataProviders/USStates.php +++ b/app/DataProviders/USStates.php @@ -16,7 +16,7 @@ use Illuminate\Support\Facades\Http; class USStates { - protected static array $states = [ + public static array $states = [ 'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 5930f8856bbb..29675981d418 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -146,9 +146,6 @@ class InvoiceItemSum if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) && in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions - nlog($this->client->country->iso_3166_2); - nlog($this->client->company->country()->iso_3166_2); - $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; $this->rule = new $class(); diff --git a/app/Models/Company.php b/app/Models/Company.php index 0a921451711a..c0bb8f2df705 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -359,6 +359,7 @@ class Company extends BaseModel 'deleted_at' => 'timestamp', 'client_registration_fields' => 'array', 'tax_data' => 'object', + 'origin_tax_data' => 'object', 'e_invoice_certificate_passphrase' => EncryptedCast::class, ]; diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index 73ecb3a7f119..ff92d455ed65 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -52,14 +52,14 @@ class TaxProvider private mixed $api_credentials; - public function __construct(public Company $company, public Client $client) + public function __construct(public Company $company, public ?Client $client = null) { } public function updateCompanyTaxData(): self { - $this->configureProvider($this->provider); //hard coded for now to one provider, but we'll be able to swap these out later + $this->configureProvider($this->provider, $this->company->country()->iso_3166_2); //hard coded for now to one provider, but we'll be able to swap these out later $company_details = [ 'address1' => $this->company->settings->address1, @@ -76,7 +76,7 @@ class TaxProvider $tax_data = $tax_provider->run(); - $this->company->tax_data = $tax_data; + $this->company->origin_tax_data = $tax_data; $this->company->save(); @@ -86,7 +86,7 @@ class TaxProvider public function updateClientTaxData(): self { - $this->configureProvider($this->provider); //hard coded for now to one provider, but we'll be able to swap these out later + $this->configureProvider($this->provider, $this->client->country->iso_3166_2); //hard coded for now to one provider, but we'll be able to swap these out later $billing_details =[ 'address1' => $this->client->address1, @@ -112,9 +112,7 @@ class TaxProvider $tax_provider->setApiCredentials($this->api_credentials); $tax_data = $tax_provider->run(); - - nlog($tax_data); - + $this->client->tax_data = $tax_data; $this->client->save(); @@ -123,10 +121,10 @@ class TaxProvider } - private function configureProvider(?string $provider): self + private function configureProvider(?string $provider, string $country_code): self { - match($this->client->country->iso_3166_2){ + match($country_code){ 'US' => $this->configureZipTax(), "AT" => $this->configureEuTax(), "BE" => $this->configureEuTax(), @@ -169,11 +167,11 @@ class TaxProvider return $this; } - private function noTaxRegionDefined(): self + private function noTaxRegionDefined() { throw new \Exception("No tax region defined for this country"); - return $this; + // return $this; } private function configureZipTax(): self diff --git a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php index 2875e0221acb..5f13774326e0 100644 --- a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php +++ b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php @@ -13,9 +13,11 @@ return new class extends Migration */ public function up() { + Schema::table('companies', function (Illuminate\Database\Schema\Blueprint $table) { $table->text('e_invoice_certificate')->nullable(); $table->text('e_invoice_certificate_passphrase')->nullable(); + $table->text('origin_tax_data')->nullable(); }); } From 7b91911c2439331379858615c76ea05e4ee9b70d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 14:39:59 +1000 Subject: [PATCH 27/34] Fixes for taxes --- app/DataMapper/Tax/BaseRule.php | 4 +- app/Helpers/Invoice/InvoiceItemSum.php | 32 ++++++++++- tests/Unit/Tax/UsTaxTest.php | 78 ++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index c724a3efd5aa..cab06d7a4fae 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -174,7 +174,7 @@ class BaseRule implements RuleInterface */ $tax_data = new Response([]); - if($this->seller_region == 'US'){ + if($this->seller_region == 'US' && $this->client_region == 'US'){ $company = $this->invoice->company; @@ -237,7 +237,7 @@ class BaseRule implements RuleInterface 'AU' => $this->client_subregion = 'AU', default => $this->client_subregion = $this->client->country->iso_3166_2, }; - + return $this; } diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 29675981d418..a3b1c3cee835 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -29,6 +29,36 @@ class InvoiceItemSum use Discounter; use Taxer; + private array $eu_tax_jurisdictions = [ + 'AT', // Austria + 'BE', // Belgium + 'BG', // Bulgaria + 'CY', // Cyprus + 'CZ', // Czech Republic + 'DE', // Germany + 'DK', // Denmark + 'EE', // Estonia + 'ES', // Spain + 'FI', // Finland + 'FR', // France + 'GR', // Greece + 'HR', // Croatia + 'HU', // Hungary + 'IE', // Ireland + 'IT', // Italy + 'LT', // Lithuania + 'LU', // Luxembourg + 'LV', // Latvia + 'MT', // Malta + 'NL', // Netherlands + 'PL', // Poland + 'PT', // Portugal + 'RO', // Romania + 'SE', // Sweden + 'SI', // Slovenia + 'SK', // Slovakia + ]; + private array $tax_jurisdictions = [ // 'AT', // Austria // 'BE', // Belgium @@ -144,7 +174,7 @@ class InvoiceItemSum return $this; } - if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) && in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions + if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) ) { //only calculate for supported tax jurisdictions $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; diff --git a/tests/Unit/Tax/UsTaxTest.php b/tests/Unit/Tax/UsTaxTest.php index 610b1b5218c0..b1af855f06d3 100644 --- a/tests/Unit/Tax/UsTaxTest.php +++ b/tests/Unit/Tax/UsTaxTest.php @@ -98,6 +98,8 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); $client = Client::factory()->create([ @@ -147,6 +149,78 @@ class UsTaxTest extends TestCase return $invoice; } + public function testInterstateWithNoTax() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '30002', + 'shipping_country_id' => 840, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'GA' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + + public function testSameSubregionAndExemptProduct() { @@ -166,6 +240,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([ @@ -236,6 +311,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([ @@ -305,6 +381,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([ @@ -373,6 +450,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([ From 0b01e7585e3e0c9ca8473f22538032ed838353fb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 15:37:52 +1000 Subject: [PATCH 28/34] Fixes for tests --- tests/Unit/Tax/EuTaxTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 537cfaa5fd19..2087cec9f57f 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -444,6 +444,8 @@ class EuTaxTest extends TestCase 'company_id' => $company->id, 'country_id' => 840, 'shipping_country_id' => 840, + 'state' => 'CA', + 'postal_code' => '90210', 'has_valid_vat_number' => false, ]); @@ -453,7 +455,7 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'status_id' => Invoice::STATUS_SENT, 'tax_data' => new Response([ - 'geoState' => 'CA', + 'geoState' => 'CA', ]), ]); From 4dba4ec35ae040254c3f0d1e61b4e5439b6ffda6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 15:45:06 +1000 Subject: [PATCH 29/34] Return early if we are not in a taxable region --- app/DataMapper/Tax/BaseRule.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index cab06d7a4fae..5674a4d57717 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -140,8 +140,12 @@ class BaseRule implements RuleInterface $this->client = $invoice->client; - $this->resolveRegions() - ->configTaxData(); + $this->resolveRegions(); + + if(!$this->isTaxableRegion()) + return $this; + + $this->configTaxData(); $this->tax_data = new Response($this->invoice->tax_data); From fa9f9ab462baffbb06374e3f4f97c76a4a46f808 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 16:02:33 +1000 Subject: [PATCH 30/34] Fixes for tests --- app/DataMapper/Tax/BaseRule.php | 7 +++- app/DataMapper/Tax/US/Rule.php | 2 +- tests/Unit/Tax/SumTaxTest.php | 65 +++++++++++++++++++-------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 5674a4d57717..0f61cd8212e8 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -215,9 +215,12 @@ class BaseRule implements RuleInterface } /** Applies the tax data to the invoice */ - if($this->invoice instanceof Invoice && \DB::transactionLevel() == 0) { + if($this->invoice instanceof Invoice) { + $this->invoice->tax_data = $tax_data; - $this->invoice->saveQuietly(); + + if(\DB::transactionLevel() == 0) + $this->invoice->saveQuietly(); } return $this; diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 2a4e46b1dc33..9538a4f3085e 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -156,7 +156,7 @@ class Rule extends BaseRule implements RuleInterface */ public function default($item): self { - + if($this->tax_data?->stateSalesTax == 0) { $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; diff --git a/tests/Unit/Tax/SumTaxTest.php b/tests/Unit/Tax/SumTaxTest.php index ec3b3047237f..c57add9afac4 100644 --- a/tests/Unit/Tax/SumTaxTest.php +++ b/tests/Unit/Tax/SumTaxTest.php @@ -13,6 +13,7 @@ namespace Tests\Unit\Tax; use Tests\TestCase; use App\Models\Client; +use App\Models\Company; use App\Models\Invoice; use App\Models\Product; use Tests\MockAccountData; @@ -20,6 +21,7 @@ use App\DataMapper\InvoiceItem; use App\DataMapper\Tax\TaxData; use App\Factory\InvoiceFactory; use App\DataMapper\Tax\TaxModel; +use App\DataMapper\CompanySettings; use App\DataMapper\Tax\ZipTax\Response; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -92,26 +94,32 @@ class SumTaxTest extends TestCase public function testCalcInvoiceNoTax() { + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany $tax_data = new TaxModel(); $tax_data->seller_subregion = 'CA'; $tax_data->regions->US->has_sales_above_threshold = true; $tax_data->regions->US->tax_all_subregions = true; - $this->company->calculate_taxes = false; - $this->company->tax_data = $tax_data; - $this->company->save(); - - $tax_data = new TaxData($this->response); + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => false, + 'origin_tax_data' => new Response($this->resp), + ]); $client = Client::factory()->create([ 'user_id' => $this->user->id, - 'company_id' => $this->company->id, + 'company_id' => $company->id, 'country_id' => 840, - 'tax_data' => $tax_data, + 'state' => 'CA', + 'postal_code' => '90210', + 'tax_data' => new Response($this->resp), ]); - $invoice = InvoiceFactory::create($this->company->id, $this->user->id); + $invoice = InvoiceFactory::create($company->id, $this->user->id); $invoice->client_id = $client->id; $invoice->uses_inclusive_taxes = false; @@ -143,35 +151,39 @@ class SumTaxTest extends TestCase public function testCalcInvoiceTax() { - + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; + $settings->currency_id = '1'; $tax_data = new TaxModel(); $tax_data->seller_subregion = 'CA'; $tax_data->regions->US->has_sales_above_threshold = true; $tax_data->regions->US->tax_all_subregions = true; - $this->company->calculate_taxes = true; - $this->company->tax_data = $tax_data; - $this->company->save(); + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->resp), + ]); -$tax_data = new TaxData($this->response); -$client = Client::factory()->create([ - 'user_id' => $this->user->id, - 'company_id' => $this->company->id, - 'country_id' => 840, - 'tax_data' => $tax_data, -]); + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '90210', + 'state' => 'CA', + 'tax_data' => new Response($this->resp), + ]); -$invoice = InvoiceFactory::create($this->company->id, $this->user->id); -$invoice->client_id = $client->id; -$invoice->uses_inclusive_taxes = false; + $invoice = InvoiceFactory::create($company->id, $this->user->id); + $invoice->client_id = $client->id; + $invoice->uses_inclusive_taxes = false; -$line_items = []; + $line_items = []; -$invoice->tax_data = $tax_data; - - $line_item = new InvoiceItem; $line_item->quantity = 1; $line_item->cost = 10; @@ -187,7 +199,6 @@ $invoice->tax_data = $tax_data; $line_items = $invoice->line_items; - $this->assertEquals(10.88, $invoice->amount); $this->assertEquals("CA Sales Tax", $line_items[0]->tax_name1); $this->assertEquals(8.75, $line_items[0]->tax_rate1); From 79383b8811f9d54085c5f110491ff054aab7a7bd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 16:33:40 +1000 Subject: [PATCH 31/34] Fixes for tests --- app/DataMapper/Tax/BaseRule.php | 11 +++-- app/DataMapper/Tax/DE/Rule.php | 20 +++++---- tests/Unit/Tax/EuTaxTest.php | 79 ++++++++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 0f61cd8212e8..a184f2ac211b 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -147,6 +147,8 @@ class BaseRule implements RuleInterface $this->configTaxData(); + nlog($this->invoice->tax_data); + $this->tax_data = new Response($this->invoice->tax_data); return $this; @@ -176,7 +178,8 @@ class BaseRule implements RuleInterface * Destination - Client Tax Data * */ - $tax_data = new Response([]); + // $tax_data = new Response([]); + $tax_data = false; if($this->seller_region == 'US' && $this->client_region == 'US'){ @@ -215,9 +218,9 @@ class BaseRule implements RuleInterface } /** Applies the tax data to the invoice */ - if($this->invoice instanceof Invoice) { + if($this->invoice instanceof Invoice && $tax_data) { - $this->invoice->tax_data = $tax_data; + $this->invoice->tax_data = $tax_data ; if(\DB::transactionLevel() == 0) $this->invoice->saveQuietly(); @@ -273,7 +276,7 @@ class BaseRule implements RuleInterface public function defaultForeign(): self { - if($this->client_region == 'US') { + if($this->client_region == 'US' && isset($this->tax_data?->taxSales)) { $this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index 288a5aec2e6a..7cb58bc938be 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -30,10 +30,10 @@ class Rule extends BaseRule implements RuleInterface public bool $eu_business_tax_exempt = true; /** @var bool $foreign_business_tax_exempt */ - public bool $foreign_business_tax_exempt = true; + public bool $foreign_business_tax_exempt = false; /** @var bool $foreign_consumer_tax_exempt */ - public bool $foreign_consumer_tax_exempt = true; + public bool $foreign_consumer_tax_exempt = false; /** @var float $tax_rate */ public float $tax_rate = 0; @@ -223,38 +223,42 @@ class Rule extends BaseRule implements RuleInterface public function calculateRates(): self { if ($this->client->is_tax_exempt) { - // nlog("tax exempt"); + nlog("tax exempt"); $this->tax_rate = 0; $this->reduced_tax_rate = 0; } elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) { - // nlog("euro zone and tax exempt"); + nlog("euro zone and tax exempt"); $this->tax_rate = 0; $this->reduced_tax_rate = 0; } elseif(!in_array($this->client_subregion, $this->eu_country_codes) && ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt)) //foreign + tax exempt { - // nlog("foreign and tax exempt"); + nlog("foreign and tax exempt"); $this->tax_rate = 0; $this->reduced_tax_rate = 0; } + elseif(!in_array($this->client_subregion, $this->eu_country_codes)) + { + $this->defaultForeign(); + } elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->has_valid_vat_number) //eu country / no valid vat { if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) { - // nlog("eu zone with sales above threshold"); + nlog("eu zone with sales above threshold"); $this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate; $this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate; } else { - // nlog("EU with intra-community supply ie DE to DE"); + nlog("EU with intra-community supply ie DE to DE"); $this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate; $this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate; } } else { - // nlog("default tax"); + nlog("default tax"); $this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate; $this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate; } diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 2087cec9f57f..7f88675347df 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -70,6 +70,8 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'company_id' => $company->id, 'country_id' => 840, + 'state' => 'CA', + 'postal_code' => '90210', 'shipping_country_id' => 840, 'has_valid_vat_number' => false, 'is_tax_exempt' => false, @@ -113,13 +115,88 @@ class EuTaxTest extends TestCase 'taxSales' => 0.07, ]), ]); - + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); $this->assertEquals(107, $invoice->amount); } + public function testEuToAuTaxCalculation() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '276'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'DE'; + $tax_data->regions->EU->has_sales_above_threshold = false; + $tax_data->regions->EU->tax_all_subregions = true; + $tax_data->regions->AU->tax_all_subregions = true; + $tax_data->regions->AU->has_sales_above_threshold = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'shipping_country_id' => 36, + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(110, $invoice->amount); + + } + + public function testInvoiceTaxCalcDetoBeNoVat() { From 5182ff815a75d9d37a8c366f78e4d69597faa290 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 17:12:48 +1000 Subject: [PATCH 32/34] Additional tests --- app/DataMapper/Tax/BaseRule.php | 2 - app/DataMapper/Tax/US/Rule.php | 3 + app/Utils/Traits/CleanLineItems.php | 7 +- tests/Unit/Tax/UsTaxTest.php | 589 +++++++++++++++++++++++++++- 4 files changed, 596 insertions(+), 5 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index a184f2ac211b..136fbf0bfcea 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -147,8 +147,6 @@ class BaseRule implements RuleInterface $this->configTaxData(); - nlog($this->invoice->tax_data); - $this->tax_data = new Response($this->invoice->tax_data); return $this; diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 9538a4f3085e..4a4527057b9f 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -129,6 +129,7 @@ class Rule extends BaseRule implements RuleInterface */ public function taxShipping($item): self { + if($this->tax_data?->txbFreight == 'Y') { $this->default($item); } @@ -144,6 +145,8 @@ class Rule extends BaseRule implements RuleInterface */ public function taxPhysical($item): self { + nlog("tax physical"); + nlog($item); $this->default($item); return $this; diff --git a/app/Utils/Traits/CleanLineItems.php b/app/Utils/Traits/CleanLineItems.php index 921f66c32e1b..19938c0dce0b 100644 --- a/app/Utils/Traits/CleanLineItems.php +++ b/app/Utils/Traits/CleanLineItems.php @@ -66,7 +66,12 @@ trait CleanLineItems $item['tax_id'] = '1'; } elseif(array_key_exists('tax_id', $item) && $item['tax_id'] == '') { - $item['tax_id'] = '1'; + + if($item['type_id'] == '2') + $item['tax_id'] = '2'; + else + $item['tax_id'] = '1'; + } } diff --git a/tests/Unit/Tax/UsTaxTest.php b/tests/Unit/Tax/UsTaxTest.php index b1af855f06d3..15a0c10f1d5a 100644 --- a/tests/Unit/Tax/UsTaxTest.php +++ b/tests/Unit/Tax/UsTaxTest.php @@ -149,6 +149,521 @@ class UsTaxTest extends TestCase return $invoice; } + public function testTaxAuNoExemption() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + $tax_data->regions->AU->has_sales_above_threshold = true; + $tax_data->regions->AU->tax_all_subregions = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'postal_code' => '30002', + 'shipping_country_id' => 36, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'NSW' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(110, $invoice->amount); + + } + + public function testTaxAuClientExemption() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + $tax_data->regions->AU->has_sales_above_threshold = true; + $tax_data->regions->AU->tax_all_subregions = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'postal_code' => '30002', + 'shipping_country_id' => 36, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => true, + 'state' => 'NSW' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + public function testTaxAuProductExemption() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + $tax_data->regions->AU->has_sales_above_threshold = true; + $tax_data->regions->AU->tax_all_subregions = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'postal_code' => '30002', + 'shipping_country_id' => 36, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'NSW' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_EXEMPT, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + public function testTaxAuProductOverride() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + $tax_data->regions->AU->has_sales_above_threshold = true; + $tax_data->regions->AU->tax_all_subregions = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'postal_code' => '30002', + 'shipping_country_id' => 36, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'NSW' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => 'OVERRIDE', + 'tax_rate1' => 20, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_OVERRIDE_TAX, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(120, $invoice->amount); + + } + + public function testInterstateFreightNoTaxWithProductTax() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '30002', + 'shipping_country_id' => 840, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'GA' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_SHIPPING, + ], + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(208.75, $invoice->amount); + + } + + public function testInterstateFreightProductNoTax() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '30002', + 'shipping_country_id' => 840, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'GA' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_SHIPPING, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + + + public function testInterstateServiceProductNoTax() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = false; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '30002', + 'shipping_country_id' => 840, + 'shipping_postal_code' => '30002', + 'shipping_state' => '30002', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + 'state' => 'GA' + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '2', + 'tax_id' => Product::PRODUCT_TYPE_SERVICE, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + public function testInterstateWithNoTax() { @@ -219,8 +734,6 @@ class UsTaxTest extends TestCase } - - public function testSameSubregionAndExemptProduct() { @@ -293,6 +806,78 @@ class UsTaxTest extends TestCase } + public function testSameSubregionAndExemptClient() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'CA'; + $tax_data->regions->US->has_sales_above_threshold = true; + $tax_data->regions->US->tax_all_subregions = true; + $tax_data->regions->EU->has_sales_above_threshold = true; + $tax_data->regions->EU->tax_all_subregions = true; + $tax_data->regions->EU->subregions->DE->tax_rate = 21; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '90210', + 'shipping_country_id' => 840, + 'shipping_postal_code' => '90210', + 'has_valid_vat_number' => false, + 'is_tax_exempt' => true, + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + 'tax_data' => new Response($this->mock_response), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + public function testForeignTaxesEnabledWithExemptProduct() { $settings = CompanySettings::defaults(); From a6af7b7a20a2512db4655572bc7d7f406125d8e6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 17:42:22 +1000 Subject: [PATCH 33/34] Additional tests for EU Tax calculations --- tests/Unit/Tax/EuTaxTest.php | 152 +++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 7f88675347df..1e7fb438167b 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -122,6 +122,158 @@ class EuTaxTest extends TestCase } + + public function testEuToAuTaxCalculationExemptProduct() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '276'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'DE'; + $tax_data->regions->EU->has_sales_above_threshold = false; + $tax_data->regions->EU->tax_all_subregions = true; + $tax_data->regions->AU->tax_all_subregions = true; + $tax_data->regions->AU->has_sales_above_threshold = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'shipping_country_id' => 36, + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_EXEMPT, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + + public function testEuToAuTaxCalculationExemptClient() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '276'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'DE'; + $tax_data->regions->EU->has_sales_above_threshold = false; + $tax_data->regions->EU->tax_all_subregions = true; + $tax_data->regions->AU->tax_all_subregions = true; + $tax_data->regions->AU->has_sales_above_threshold = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 36, + 'shipping_country_id' => 36, + 'has_valid_vat_number' => false, + 'is_tax_exempt' => true, + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + + + public function testEuToAuTaxCalculation() { From 71719e38ab4738aa4dee241f844707bb045e0338 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 18:06:07 +1000 Subject: [PATCH 34/34] Tax Tests --- app/DataMapper/Tax/BaseRule.php | 6 ++ app/Helpers/Invoice/InvoiceItemSum.php | 4 ++ tests/Unit/Tax/EuTaxTest.php | 78 +++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 136fbf0bfcea..1cf893d5dded 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -374,4 +374,10 @@ class BaseRule implements RuleInterface { return $this; } + + public function regionWithNoTaxCoverage(string $iso_3166_2): bool + { + return ! in_array($iso_3166_2, array_merge($this->eu_country_codes, array_keys($this->region_codes))); + } + } diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index a3b1c3cee835..3be7e878ceb8 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -179,6 +179,10 @@ class InvoiceItemSum $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; $this->rule = new $class(); + + if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2)) + return $this; + $this->rule ->setEntity($this->invoice) ->init(); diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 1e7fb438167b..b497ddee4d2d 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -122,6 +122,80 @@ class EuTaxTest extends TestCase } + public function testEuToBrazilTaxCalculations() + { + + $settings = CompanySettings::defaults(); + $settings->country_id = '276'; // germany + + $tax_data = new TaxModel(); + $tax_data->seller_subregion = 'DE'; + $tax_data->regions->EU->has_sales_above_threshold = false; + $tax_data->regions->EU->tax_all_subregions = true; + $tax_data->regions->AU->tax_all_subregions = true; + $tax_data->regions->AU->has_sales_above_threshold = true; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => true, + ]); + + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 76, + 'shipping_country_id' => 76, + 'has_valid_vat_number' => false, + 'is_tax_exempt' => false, + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'client_id' => $client->id, + 'status_id' => 1, + 'user_id' => $this->user->id, + 'uses_inclusive_taxes' => false, + 'discount' => 0, + 'line_items' => [ + [ + 'product_key' => 'Test', + 'notes' => 'Test', + 'cost' => 100, + 'quantity' => 1, + 'tax_name1' => '', + 'tax_rate1' => 0, + 'tax_name2' => '', + 'tax_rate2' => 0, + 'tax_name3' => '', + 'tax_rate3' => 0, + 'type_id' => '1', + 'tax_id' => Product::PRODUCT_TYPE_PHYSICAL, + ], + ], + 'tax_rate1' => 0, + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name1' => '', + 'tax_name2' => '', + 'tax_name3' => '', + // 'tax_data' => new Response([ + // 'geoState' => 'CA', + // 'taxSales' => 0.07, + // ]), + ]); + + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); + + $this->assertEquals(100, $invoice->amount); + + } + public function testEuToAuTaxCalculationExemptProduct() { @@ -981,6 +1055,8 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'company_id' => $company->id, 'country_id' => 840, + 'state' => 'CA', + 'postal_code' => '90210', 'shipping_country_id' => 840, 'has_valid_vat_number' => true, 'is_tax_exempt' => true, @@ -992,7 +1068,7 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'status_id' => Invoice::STATUS_SENT, 'tax_data' => new Response([ - 'geoState' => 'CA', + 'geoState' => 'CA', ]), ]);