From b3d9dc3a518de490680e7b4fd5bc5473fd30693b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 12 Apr 2023 13:59:38 +1000 Subject: [PATCH] Refactors for taxes --- app/DataMapper/Tax/BaseRule.php | 66 ++++++++++++++--------- app/DataMapper/Tax/DE/Rule.php | 42 ++++----------- app/DataMapper/Tax/US/Rule.php | 26 +-------- tests/Unit/Tax/EuTaxTest.php | 95 ++++++++++++++++++++++++++++++--- 4 files changed, 141 insertions(+), 88 deletions(-) diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 22c457e917d3..492aeca29ab1 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -11,8 +11,9 @@ namespace App\DataMapper\Tax; -use App\DataMapper\Tax\ZipTax\Response; use App\Models\Client; +use App\Models\Product; +use App\DataMapper\Tax\ZipTax\Response; class BaseRule implements RuleInterface { @@ -27,7 +28,7 @@ class BaseRule implements RuleInterface public bool $foreign_consumer_tax_exempt = true; - public string $vendor_iso_3166_2 = ''; + public string $seller_region = ''; public string $client_region = ''; @@ -93,6 +94,8 @@ class BaseRule implements RuleInterface 'SK' => 'EU', // Slovakia 'US' => 'US', // United States + + 'AU' => 'AU', // Australia ]; /** EU TAXES */ @@ -132,19 +135,26 @@ class BaseRule implements RuleInterface return $this; } + public function setTaxData(Response $tax_data): self + { + $this->tax_data = $tax_data; + + return $this; + } + // Refactor to support switching between shipping / billing country / region / subregion private function resolveRegions(): self { if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) - throw new \Exception('Automatic tax calculates not supported for this country'); + throw new \Exception('Automatic tax calculations not supported for this country'); $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; match($this->client_region){ 'US' => $this->client_subregion = $this->tax_data->geoState, 'EU' => $this->client_subregion = $this->client->country->iso_3166_2, - default => '', + default => $this->client->country->iso_3166_2, }; return $this; @@ -155,26 +165,18 @@ class BaseRule implements RuleInterface return $this->client->company->tax_data->regions->{$this->client_region}->tax_all_subregions || $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->apply_tax; } - public function taxForeignEntity(mixed $item): self + public function defaultForeign(): self { + if($this->client_region == 'US') { - + $this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; - } - else { - - $this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; - $this->tax_name1 = "Tax"; + return $this; } - return $this; - } - - public function defaultForeign(): self - { $this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; $this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; @@ -182,18 +184,32 @@ class BaseRule implements RuleInterface return $this; } - public function setTaxData(Response $tax_data): self - { - $this->tax_data = $tax_data; - - return $this; - } - public function tax($item = null): self { - return $this; - } + nlog($this->client_region); + nlog($this->seller_region); + if ($this->client->is_tax_exempt) { + return $this->taxExempt(); + } elseif($this->client_region == $this->seller_region && $this->isTaxableRegion()) { + + $this->taxByType($item->tax_id); + + return $this; + } elseif($this->isTaxableRegion()) { //other regions outside of US + + match($item->tax_id) { + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), + default => $this->defaultForeign(), + }; + + } + return $this; + + } + public function taxByType(mixed $type): self { return $this; diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index af6dbc2b0a9c..0ec3fa5fd0b2 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -17,7 +17,7 @@ use App\DataMapper\Tax\RuleInterface; class Rule extends BaseRule implements RuleInterface { - public string $vendor_iso_3166_2 = 'DE'; + public string $seller_region = 'EU'; public bool $consumer_tax_exempt = false; @@ -40,30 +40,6 @@ class Rule extends BaseRule implements RuleInterface return $this; } - public function tax($item = null): self - { - - if ($this->client->is_tax_exempt) { - - return $this->taxExempt(); - - } - elseif ($this->client->company->tax_data->regions->EU->tax_all_subregions || $this->client->company->tax_data->regions->EU->subregions->{$this->client_subregion}->apply_tax) { - - $this->taxByType($item->tax_id); - - return $this; - } - elseif ($this->client_region != 'EU' && $this->isTaxableRegion()) { //foreign entity with tax obligations - - $this->taxForeignEntity($item); - - return $this; - } - return $this; - - } - public function taxByType($product_tax_type): self { @@ -148,38 +124,38 @@ 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->vendor_iso_3166_2 && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) + 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->client->has_valid_vat_number) //eu country / no valid vat { - if(($this->vendor_iso_3166_2 != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) + 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/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index c62f6f1864fb..efeb4448c623 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -17,6 +17,8 @@ use App\Models\Product; class Rule extends BaseRule implements RuleInterface { + public string $seller_region = 'US'; + /** * The rules apply US => US taxes using the tax calculator. * @@ -34,30 +36,6 @@ class Rule extends BaseRule implements RuleInterface return $this; } - public function tax($item = null): self - { - - if ($this->client->is_tax_exempt) { - return $this->taxExempt(); - } elseif($this->client_region == 'US' && $this->isTaxableRegion()) { - - $this->taxByType($item->tax_id); - - return $this; - } elseif($this->isTaxableRegion()) { //other regions outside of US - - match($item->tax_id) { - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), - default => $this->defaultForeign(), - }; - - } - return $this; - - } - public function taxByType($product_tax_type): self { diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 7e1c1be1a2b1..3368ab935add 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -15,6 +15,7 @@ use Tests\TestCase; use App\Models\Client; use App\Models\Company; use App\Models\Invoice; +use App\Models\Product; use Tests\MockAccountData; use App\DataMapper\Tax\DE\Rule; use App\DataMapper\Tax\TaxModel; @@ -44,6 +45,82 @@ class EuTaxTest extends TestCase $this->makeTestData(); } + + public function testEuToUsTaxCalculation() + { + + $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->US->tax_all_subregions = true; + $tax_data->regions->US->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' => 840, + 'shipping_country_id' => 840, + '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(107, $invoice->amount); + + } + + public function testInvoiceTaxCalcDetoBeNoVat() { $settings = CompanySettings::defaults(); @@ -265,8 +342,7 @@ class EuTaxTest extends TestCase $process->setClient($client); $process->init(); - $this->assertEquals('DE', $process->vendor_iso_3166_2); - + $this->assertEquals('EU', $process->seller_region); $this->assertEquals('DE', $process->client_subregion); $this->assertFalse($client->has_valid_vat_number); @@ -311,7 +387,7 @@ class EuTaxTest extends TestCase $process->init(); - $this->assertEquals('DE', $process->vendor_iso_3166_2); +$this->assertEquals('EU', $process->seller_region); $this->assertEquals('BE', $process->client_subregion); @@ -332,11 +408,18 @@ class EuTaxTest extends TestCase $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 = true; + $tax_data->regions->EU->tax_all_subregions = true; + $company = Company::factory()->create([ 'account_id' => $this->account->id, - 'settings' => $settings + 'settings' => $settings, + 'tax_data' => $tax_data, ]); + $client = Client::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $company->id, @@ -352,7 +435,7 @@ class EuTaxTest extends TestCase $process->setClient($client); $process->init(); - $this->assertEquals('DE', $process->vendor_iso_3166_2); + $this->assertEquals('EU', $process->seller_region); $this->assertEquals('CA', $process->client_subregion); @@ -364,9 +447,9 @@ class EuTaxTest extends TestCase $this->assertEquals(0, $process->reduced_tax_rate); - } + public function testSubThresholdCorrectRate() {