From 8908bc318cf2890afbc4741e52312108f9af4a5d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 17 May 2023 14:07:48 +1000 Subject: [PATCH] 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(); }); }