From 646977f5ef5668981195701b9155586b38292ad5 Mon Sep 17 00:00:00 2001 From: Dom Morgan Date: Tue, 18 Jun 2024 14:11:36 +0100 Subject: [PATCH] EU One-Stop-Shop Tax Return (#84) * Simpler update for EU tax * Capture new OSS behaviour * Update composer.json * No need to fork * Remove branch alias in preparation for release of 1.0 * Add PHP 8.3 to tests --- .github/workflows/phpunit83.yml | 20 ++++++++++++++++ composer.json | 7 +----- src/Resolver/TaxType/EuTaxTypeResolver.php | 24 ++++++++++++++++--- .../TaxType/EuTaxTypeResolverTest.php | 11 +++++++-- 4 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/phpunit83.yml diff --git a/.github/workflows/phpunit83.yml b/.github/workflows/phpunit83.yml new file mode 100644 index 0000000..78b5ad7 --- /dev/null +++ b/.github/workflows/phpunit83.yml @@ -0,0 +1,20 @@ +name: CI PHP 8.3 + +on: [push] + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: php-actions/composer@v6 + with: + php_version: "8.3" + + - name: PHPUnit Tests + uses: php-actions/phpunit@v3 + with: + version: "8" + php_version: "8.3" diff --git a/composer.json b/composer.json index 1e661c4..cd4e942 100644 --- a/composer.json +++ b/composer.json @@ -29,10 +29,5 @@ { "name": "David Kitchen" } - ], - "extra": { - "branch-alias": { - "dev-master": "0.x-dev" - } - } + ] } diff --git a/src/Resolver/TaxType/EuTaxTypeResolver.php b/src/Resolver/TaxType/EuTaxTypeResolver.php index c951353..65564f5 100644 --- a/src/Resolver/TaxType/EuTaxTypeResolver.php +++ b/src/Resolver/TaxType/EuTaxTypeResolver.php @@ -13,6 +13,11 @@ */ class EuTaxTypeResolver implements TaxTypeResolverInterface { + /** + * The date on which the one stop shop rules are in effect + */ + const ONE_STOP_SHOP_INTRODUCTION = '2020-07-01T00:00:00+0100'; + use StoreRegistrationCheckerTrait; /** @@ -60,19 +65,32 @@ public function resolve(TaxableInterface $taxable, Context $context) // to Germany needs to have German VAT applied. $isDigital = $context->getDate()->format('Y') >= '2015' && !$taxable->isPhysical(); + $oneStopShop = $context->getDate()->getTimeStamp() > strtotime(static::ONE_STOP_SHOP_INTRODUCTION); + $resolvedTaxTypes = []; - if (empty($storeTaxTypes) && !empty($storeRegistrationTaxTypes)) { + if ( + !$oneStopShop + && empty($storeTaxTypes) + && !empty($storeRegistrationTaxTypes) + ) { // The store is not in the EU but is registered to collect VAT. // This VAT is only charged on B2C digital services. $resolvedTaxTypes = self::NO_APPLICABLE_TAX_TYPE; if ($isDigital && !$customerTaxNumber) { $resolvedTaxTypes = $customerTaxTypes; } - } elseif ($customerTaxNumber && $customerCountry != $storeCountry) { + } elseif ( + $customerTaxNumber + && $customerCountry != $storeCountry + && !empty($storeTaxTypes) // store is in the EU + ) { // Intra-community supply (B2B). $icTaxType = $this->taxTypeRepository->get('eu_ic_vat'); $resolvedTaxTypes = [$icTaxType]; - } elseif ($isDigital) { + } elseif ( + $isDigital + || $oneStopShop + ) { $resolvedTaxTypes = $customerTaxTypes; } else { // Physical products use the origin tax types, unless the store is diff --git a/tests/Resolver/TaxType/EuTaxTypeResolverTest.php b/tests/Resolver/TaxType/EuTaxTypeResolverTest.php index 46b708c..3f7c90a 100644 --- a/tests/Resolver/TaxType/EuTaxTypeResolverTest.php +++ b/tests/Resolver/TaxType/EuTaxTypeResolverTest.php @@ -241,6 +241,7 @@ public function dataProvider() $date1 = new \DateTime('2014-02-24'); $date2 = new \DateTime('2015-02-24'); + $date3 = new \DateTime('2021-08-24'); $notApplicable = EuTaxTypeResolver::NO_APPLICABLE_TAX_TYPE; return [ @@ -249,9 +250,9 @@ public function dataProvider() // French customer, French store, VAT number provided. [$physicalTaxable, $this->getContext($frenchAddress, $frenchAddress, '123'), 'fr_vat'], // German customer, French store, physical product. - [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress), 'fr_vat'], + [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date2), 'fr_vat'], // German customer, French store registered for German VAT, physical product. - [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', ['DE']), 'de_vat'], + [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', ['DE'], $date2), 'de_vat'], // German customer, French store, digital product before Jan 1st 2015. [$digitalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date1), 'fr_vat'], // German customer, French store, digital product. @@ -266,6 +267,12 @@ public function dataProvider() [$physicalTaxable, $this->getContext($serbianAddress, $frenchAddress), []], // French customer, Serbian store, physical product. [$physicalTaxable, $this->getContext($frenchAddress, $serbianAddress), []], + // German customer, French store, digital product after July 1st 2021. + [$digitalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date3), 'de_vat'], + // German customer, French store, physical product after July 1st 2021. + [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date3), 'de_vat'], + // German customer US store registered in FR, physical product after July 1st 2021 + [$physicalTaxable, $this->getContext($germanAddress, $usAddress, '', ['FR'], $date3), 'de_vat'], ]; }