mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Global Tax Rules
This commit is contained in:
parent
b24be423e8
commit
12d3e35019
17
app/DataMapper/Tax/RuleInterface.php
Normal file
17
app/DataMapper/Tax/RuleInterface.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\DataMapper\Tax;
|
||||||
|
|
||||||
|
interface RuleInterface
|
||||||
|
{
|
||||||
|
public function run();
|
||||||
|
}
|
@ -11,13 +11,15 @@
|
|||||||
|
|
||||||
namespace App\DataMapper\Tax\de;
|
namespace App\DataMapper\Tax\de;
|
||||||
|
|
||||||
class Rule
|
use App\DataMapper\Tax\RuleInterface;
|
||||||
|
|
||||||
|
class Rule implements RuleInterface
|
||||||
{
|
{
|
||||||
public float $vat = 19;
|
public float $vat_rate = 19;
|
||||||
|
|
||||||
public float $vat_threshold = 10000;
|
public float $vat_threshold = 10000;
|
||||||
|
|
||||||
public float $vat_reduced = 7;
|
public float $vat_reduced_rate = 7;
|
||||||
|
|
||||||
public float $vat_reduced_threshold = 10000;
|
public float $vat_reduced_threshold = 10000;
|
||||||
|
|
||||||
@ -77,11 +79,18 @@ class Rule
|
|||||||
|
|
||||||
public float $gb_vat_rate = 20; // United Kingdom
|
public float $gb_vat_rate = 20; // United Kingdom
|
||||||
|
|
||||||
public bool $de_consumer_tax_exempt = false;
|
public bool $consumer_tax_exempt = false;
|
||||||
|
|
||||||
public bool $de_business_tax_exempt = true;
|
public bool $business_tax_exempt = true;
|
||||||
|
|
||||||
public bool $eu_business_tax_exempt = true;
|
public bool $eu_business_tax_exempt = true;
|
||||||
|
|
||||||
public bool $foreign_business_tax_exempt = true;
|
public bool $foreign_business_tax_exempt = true;
|
||||||
|
|
||||||
|
public bool $foreign_consumer_tax_exempt = true;
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,143 @@
|
|||||||
|
|
||||||
namespace App\Services\Tax;
|
namespace App\Services\Tax;
|
||||||
|
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Company;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use App\DataMapper\Tax\de\Rule;
|
||||||
|
use App\Services\Tax\VatNumberCheck;
|
||||||
class ProcessRule
|
class ProcessRule
|
||||||
{
|
{
|
||||||
|
public Rule $rule;
|
||||||
|
|
||||||
|
private string $vendor_country_code;
|
||||||
|
|
||||||
|
private string $client_country_code;
|
||||||
|
|
||||||
|
private bool $valid_vat_number = false;
|
||||||
|
|
||||||
|
private float $vat_rate = 0.0;
|
||||||
|
|
||||||
|
private float $vat_reduced_rate = 0.0;
|
||||||
|
|
||||||
|
private array $eu_country_codes = [
|
||||||
|
'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
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function __construct(protected Company $company, protected Client $client)
|
public function __construct(protected Company $company, protected Client $client)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* need to have a setting that allows a user to define their annual turnover, or whether they have breached their thresholds */
|
||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
|
$this->setUp()
|
||||||
|
->validateVat()
|
||||||
|
->calculateVatRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasValidVatNumber(): bool
|
||||||
|
{
|
||||||
|
return $this->valid_vat_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVatRate(): float
|
||||||
|
{
|
||||||
|
return $this->vat_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVatReducedRate(): float
|
||||||
|
{
|
||||||
|
return $this->vat_reduced_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVendorCountryCode(): string
|
||||||
|
{
|
||||||
|
return $this->vendor_country_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClientCountryCode(): string
|
||||||
|
{
|
||||||
|
return $this->client_country_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setUp(): self
|
||||||
|
{
|
||||||
|
$this->vendor_country_code = Str::lower($this->company->country()->iso_3166_2);
|
||||||
|
|
||||||
|
$this->client_country_code = $this->client->shipping_country ? Str::lower($this->client->shipping_country->iso_3166_2) : Str::lower($this->client->country->iso_3166_2);
|
||||||
|
|
||||||
|
$class = "App\\DataMapper\\Tax\\".$this->vendor_country_code."\\Rule";
|
||||||
|
|
||||||
|
$this->rule = new $class();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateVat(): self
|
||||||
|
{
|
||||||
|
$vat_check = (new VatNumberCheck($this->client->vat_number, $this->client_country_code))->run();
|
||||||
|
|
||||||
|
$this->valid_vat_number = $vat_check->isValid();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function calculateVatRates(): self
|
||||||
|
{
|
||||||
|
|
||||||
|
if(
|
||||||
|
(($this->vendor_country_code == $this->client_country_code) && $this->valid_vat_number && $this->rule->business_tax_exempt) ||
|
||||||
|
(in_array($this->client_country_code, $this->eu_country_codes) && $this->valid_vat_number && $this->rule->business_tax_exempt)
|
||||||
|
) {
|
||||||
|
$this->vat_rate = 0.0;
|
||||||
|
$this->vat_reduced_rate = 0.0;
|
||||||
|
}
|
||||||
|
elseif(!in_array(strtoupper($this->client_country_code), $this->eu_country_codes) && ($this->rule->foreign_consumer_tax_exempt || $this->rule->foreign_business_tax_exempt)) {
|
||||||
|
nlog($this->client_country_code);
|
||||||
|
$this->vat_rate = 0.0;
|
||||||
|
$this->vat_reduced_rate = 0.0;
|
||||||
|
}
|
||||||
|
elseif(in_array(strtoupper($this->client_country_code), $this->eu_country_codes) && !$this->valid_vat_number) {
|
||||||
|
$rate_name = $this->client_country_code."_vat_rate";
|
||||||
|
$this->vat_rate = $this->rule->{$rate_name};
|
||||||
|
$this->vat_reduced_rate = $this->rule->vat_reduced_rate;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rate_name = $this->vendor_country_code."_vat_rate";
|
||||||
|
$this->vat_rate = $this->rule->{$rate_name};
|
||||||
|
$this->vat_reduced_rate = $this->rule->vat_reduced_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ namespace App\Services\Tax;
|
|||||||
|
|
||||||
class VatNumberCheck
|
class VatNumberCheck
|
||||||
{
|
{
|
||||||
|
private array $response = [];
|
||||||
|
|
||||||
public function __construct(protected string $vat_number, protected string $country_code)
|
public function __construct(protected string $vat_number, protected string $country_code)
|
||||||
{
|
{
|
||||||
@ -23,9 +24,10 @@ class VatNumberCheck
|
|||||||
return $this->checkvat_number();
|
return $this->checkvat_number();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkvat_number(): array
|
private function checkvat_number(): self
|
||||||
{
|
{
|
||||||
$wsdl = "https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl";
|
$wsdl = "https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$client = new \SoapClient($wsdl);
|
$client = new \SoapClient($wsdl);
|
||||||
$params = [
|
$params = [
|
||||||
@ -35,18 +37,30 @@ class VatNumberCheck
|
|||||||
$response = $client->checkVat($params);
|
$response = $client->checkVat($params);
|
||||||
|
|
||||||
if ($response->valid) {
|
if ($response->valid) {
|
||||||
return [
|
|
||||||
|
$this->response = [
|
||||||
'valid' => true,
|
'valid' => true,
|
||||||
'name' => $response->name,
|
'name' => $response->name,
|
||||||
'address' => $response->address
|
'address' => $response->address
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return ['valid' => false];
|
$this->response = ['valid' => false];
|
||||||
}
|
}
|
||||||
} catch (\SoapFault $e) {
|
} catch (\SoapFault $e) {
|
||||||
// Handle error, e.g., log or display an error message
|
|
||||||
return ['error' => $e->getMessage()];
|
$this->response = ['valid' => false, 'error' => $e->getMessage()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getResponse()
|
||||||
|
{
|
||||||
|
return $this->response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return $this->response['valid'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
154
tests/Unit/Tax/ProcessRuleTest.php
Normal file
154
tests/Unit/Tax/ProcessRuleTest.php
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tests\Unit\Tax;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Company;
|
||||||
|
use Tests\MockAccountData;
|
||||||
|
use App\DataMapper\Tax\de\Rule;
|
||||||
|
use App\Services\Tax\ProcessRule;
|
||||||
|
use App\DataMapper\CompanySettings;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test App\Services\Tax\ProcessRule
|
||||||
|
*/
|
||||||
|
class ProcessRuleTest extends TestCase
|
||||||
|
{
|
||||||
|
use MockAccountData;
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
protected function setUp() :void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->withoutMiddleware(
|
||||||
|
ThrottleRequests::class
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
|
||||||
|
$this->makeTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCorrectRuleInit()
|
||||||
|
{
|
||||||
|
|
||||||
|
$settings = CompanySettings::defaults();
|
||||||
|
$settings->country_id = '276'; // germany
|
||||||
|
|
||||||
|
$company = Company::factory()->create([
|
||||||
|
'account_id' => $this->account->id,
|
||||||
|
'settings' => $settings
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Client::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'company_id' => $company->id,
|
||||||
|
'country_id' => 276,
|
||||||
|
'shipping_country_id' => 276,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$process = new ProcessRule($company, $client);
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
$this->assertEquals('de', $process->getVendorCountryCode());
|
||||||
|
|
||||||
|
$this->assertEquals('de', $process->getClientCountryCode());
|
||||||
|
|
||||||
|
$this->assertFalse($process->hasValidVatNumber());
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Rule::class, $process->rule);
|
||||||
|
|
||||||
|
$this->assertEquals(19, $process->getVatRate());
|
||||||
|
|
||||||
|
$this->assertEquals(7, $process->getVatReducedRate());
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEuCorrectRuleInit()
|
||||||
|
{
|
||||||
|
|
||||||
|
$settings = CompanySettings::defaults();
|
||||||
|
$settings->country_id = '276'; // germany
|
||||||
|
|
||||||
|
$company = Company::factory()->create([
|
||||||
|
'account_id' => $this->account->id,
|
||||||
|
'settings' => $settings
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Client::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'company_id' => $company->id,
|
||||||
|
'country_id' => 56,
|
||||||
|
'shipping_country_id' => 56,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$process = new ProcessRule($company, $client);
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
$this->assertEquals('de', $process->getVendorCountryCode());
|
||||||
|
|
||||||
|
$this->assertEquals('be', $process->getClientCountryCode());
|
||||||
|
|
||||||
|
$this->assertFalse($process->hasValidVatNumber());
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Rule::class, $process->rule);
|
||||||
|
|
||||||
|
$this->assertEquals(21, $process->getVatRate());
|
||||||
|
|
||||||
|
$this->assertEquals(7, $process->getVatReducedRate());
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testForeignCorrectRuleInit()
|
||||||
|
{
|
||||||
|
|
||||||
|
$settings = CompanySettings::defaults();
|
||||||
|
$settings->country_id = '276'; // germany
|
||||||
|
|
||||||
|
$company = Company::factory()->create([
|
||||||
|
'account_id' => $this->account->id,
|
||||||
|
'settings' => $settings
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = Client::factory()->create([
|
||||||
|
'user_id' => $this->user->id,
|
||||||
|
'company_id' => $company->id,
|
||||||
|
'country_id' => 840,
|
||||||
|
'shipping_country_id' => 840,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$process = new ProcessRule($company, $client);
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
$this->assertEquals('de', $process->getVendorCountryCode());
|
||||||
|
|
||||||
|
$this->assertEquals('us', $process->getClientCountryCode());
|
||||||
|
|
||||||
|
$this->assertFalse($process->hasValidVatNumber());
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Rule::class, $process->rule);
|
||||||
|
|
||||||
|
$this->assertEquals(0, $process->getVatRate());
|
||||||
|
|
||||||
|
$this->assertEquals(0, $process->getVatReducedRate());
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -9,10 +9,10 @@
|
|||||||
* @license https://www.elastic.co/licensing/elastic-license
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Tests\Unit;
|
namespace Tests\Unit\Tax;
|
||||||
|
|
||||||
use Tests\TestCase;
|
|
||||||
use App\Services\Tax\VatNumberCheck;
|
use App\Services\Tax\VatNumberCheck;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test App\Services\Tax\VatNumberCheck
|
* @test App\Services\Tax\VatNumberCheck
|
||||||
@ -24,11 +24,8 @@ class VatNumberTest extends TestCase
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function testVatNumber()
|
public function testVatNumber()
|
||||||
{
|
{
|
||||||
|
|
||||||
// Usage example
|
// Usage example
|
||||||
$country_code = "IE"; // Ireland
|
$country_code = "IE"; // Ireland
|
||||||
$vat_number = "1234567L"; // Example VAT number
|
$vat_number = "1234567L"; // Example VAT number
|
||||||
@ -37,22 +34,10 @@ class VatNumberTest extends TestCase
|
|||||||
$vat_checker = new VatNumberCheck($vat_number, $country_code);
|
$vat_checker = new VatNumberCheck($vat_number, $country_code);
|
||||||
$result = $vat_checker->run();
|
$result = $vat_checker->run();
|
||||||
|
|
||||||
if (isset($result['valid'])) {
|
$this->assertFalse($result->isValid());
|
||||||
if ($result['valid']) {
|
|
||||||
echo "The VAT number is valid.\n";
|
|
||||||
echo "Name: " . $result['name'] . "\n";
|
|
||||||
echo "Address: " . $result['address'] . "\n";
|
|
||||||
} else {
|
|
||||||
echo "The VAT number is invalid.\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo "Error: " . $result['error'] . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertFalse($result['valid']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function testValidVatNumber()
|
public function testValidVatNumber()
|
||||||
{
|
{
|
||||||
// Usage example
|
// Usage example
|
||||||
$country_code = "AT"; // Ireland
|
$country_code = "AT"; // Ireland
|
||||||
@ -62,21 +47,6 @@ class VatNumberTest extends TestCase
|
|||||||
$vat_checker = new VatNumberCheck($vat_number, $country_code);
|
$vat_checker = new VatNumberCheck($vat_number, $country_code);
|
||||||
$result = $vat_checker->run();
|
$result = $vat_checker->run();
|
||||||
|
|
||||||
if (isset($result['valid'])) {
|
$this->assertFalse($result->isValid());
|
||||||
if ($result['valid']) {
|
|
||||||
echo "The VAT number is valid.\n";
|
|
||||||
echo "Name: " . $result['name'] . "\n";
|
|
||||||
echo "Address: " . $result['address'] . "\n";
|
|
||||||
} else {
|
|
||||||
echo "The VAT number is invalid.\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo "Error: " . $result['error'] . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertFalse($result['valid']);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user