From 0124e9cf19f877714ddc2e2b59797839f0cc9a97 Mon Sep 17 00:00:00 2001 From: Jahidul Pabel Islam Date: Wed, 3 Jun 2026 22:17:43 +0100 Subject: [PATCH 1/2] Kill zone package --- composer.json | 1 - src/Model/TaxType.php | 6 +- src/Model/TaxTypeEntityInterface.php | 6 +- src/Model/TaxTypeInterface.php | 4 +- src/Repository/TaxTypeRepository.php | 6 +- src/Repository/ZoneRepository.php | 124 ++++++++++++++++++ .../TaxType/StoreRegistrationCheckerTrait.php | 6 +- tests/Model/TaxTypeTest.php | 3 +- tests/Repository/TaxTypeRepositoryTest.php | 6 +- .../TaxRate/DefaultTaxRateResolverTest.php | 2 + 10 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 src/Repository/ZoneRepository.php diff --git a/composer.json b/composer.json index e177c48..4f409b1 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,6 @@ "require": { "php": ">=7.1", "commerceguys/addressing": "^1.0", - "commerceguys/zone": "^0.8", "doctrine/collections": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { diff --git a/src/Model/TaxType.php b/src/Model/TaxType.php index a4b1367..3fcb8a1 100644 --- a/src/Model/TaxType.php +++ b/src/Model/TaxType.php @@ -2,8 +2,8 @@ namespace CommerceGuys\Tax\Model; +use CommerceGuys\Addressing\Zone\Zone; use CommerceGuys\Tax\Enum\GenericLabel; -use CommerceGuys\Zone\Model\ZoneEntityInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -59,7 +59,7 @@ class TaxType implements TaxTypeEntityInterface /** * The tax type zone. * - * @var ZoneEntityInterface + * @var Zone */ protected $zone; @@ -216,7 +216,7 @@ public function getZone() /** * {@inheritdoc} */ - public function setZone(ZoneEntityInterface $zone) + public function setZone(Zone $zone) { $this->zone = $zone; diff --git a/src/Model/TaxTypeEntityInterface.php b/src/Model/TaxTypeEntityInterface.php index 8274969..01d79ef 100644 --- a/src/Model/TaxTypeEntityInterface.php +++ b/src/Model/TaxTypeEntityInterface.php @@ -2,8 +2,8 @@ namespace CommerceGuys\Tax\Model; +use CommerceGuys\Addressing\Zone\Zone; use Doctrine\Common\Collections\Collection; -use CommerceGuys\Zone\Model\ZoneEntityInterface; interface TaxTypeEntityInterface extends TaxTypeInterface { @@ -62,11 +62,11 @@ public function setRoundingMode($roundingMode); /** * Sets the tax type zone. * - * @param ZoneEntityInterface $zone The tax type zone. + * @param Zone $zone The tax type zone. * * @return self */ - public function setZone(ZoneEntityInterface $zone); + public function setZone(Zone $zone); /** * Sets the tax type tag. diff --git a/src/Model/TaxTypeInterface.php b/src/Model/TaxTypeInterface.php index 5cb9b32..4841511 100644 --- a/src/Model/TaxTypeInterface.php +++ b/src/Model/TaxTypeInterface.php @@ -2,7 +2,7 @@ namespace CommerceGuys\Tax\Model; -use CommerceGuys\Zone\Model\ZoneInterface; +use CommerceGuys\Addressing\Zone\Zone; interface TaxTypeInterface { @@ -71,7 +71,7 @@ public function getRoundingMode(); /** * Gets the tax type zone. * - * @return ZoneInterface The tax type zone. + * @return Zone The tax type zone. */ public function getZone(); diff --git a/src/Repository/TaxTypeRepository.php b/src/Repository/TaxTypeRepository.php index 4f774e3..1acb432 100644 --- a/src/Repository/TaxTypeRepository.php +++ b/src/Repository/TaxTypeRepository.php @@ -6,8 +6,6 @@ use CommerceGuys\Tax\Model\TaxType; use CommerceGuys\Tax\Model\TaxRate; use CommerceGuys\Tax\Model\TaxRateAmount; -use CommerceGuys\Zone\Repository\ZoneRepository; -use CommerceGuys\Zone\Repository\ZoneRepositoryInterface; /** * Manages tax types based on JSON definitions. @@ -24,7 +22,7 @@ class TaxTypeRepository implements TaxTypeRepositoryInterface /** * The zone repository. * - * @var ZoneRepositoryInterface + * @var ZoneRepository */ protected $zoneRepository; @@ -48,7 +46,7 @@ class TaxTypeRepository implements TaxTypeRepositoryInterface * @param string $definitionPath The path to the tax type and zone * definitions. Defaults to 'resources/'. */ - public function __construct($definitionPath = null, ?ZoneRepositoryInterface $zoneRepository = null) + public function __construct($definitionPath = null, ?ZoneRepository $zoneRepository = null) { $definitionPath = $definitionPath ?: __DIR__ . '/../../resources/'; $this->definitionPath = $definitionPath . 'tax_type/'; diff --git a/src/Repository/ZoneRepository.php b/src/Repository/ZoneRepository.php new file mode 100644 index 0000000..43db471 --- /dev/null +++ b/src/Repository/ZoneRepository.php @@ -0,0 +1,124 @@ +definitionPath = $definitionPath; + } + + public function get(string $id): Zone + { + if (!isset($this->zones[$id])) { + $definition = $this->loadDefinition($id); + $this->zones[$id] = $this->createZoneFromDefinition($definition); + } + + return $this->zones[$id]; + } + + public function getAll(?string $scope = null): array + { + // Build the list of all available zones. + if (empty($this->zoneIndex)) { + if ($handle = opendir($this->definitionPath)) { + while (false !== ($entry = readdir($handle))) { + if (substr($entry, 0, 1) != '.') { + $id = strtok($entry, '.'); + $this->zoneIndex[] = $id; + } + } + closedir($handle); + } + } + + // Load each zone, filter by scope if needed. + $zones = []; + foreach ($this->zoneIndex as $id) { + $zone = $this->get($id); + if (is_null($scope) || ($zone->getScope() == $scope)) { + $zones[$id] = $this->get($id); + } + } + + return $zones; + } + + /** + * Loads the zone definition for the provided id. + * + * @param string $id The zone id. + * @return array The zone definition. + */ + protected function loadDefinition(string $id): array + { + $filename = $this->definitionPath . $id . '.json'; + $definition = @file_get_contents($filename); + if (empty($definition)) { + throw new UnknownZoneException($id); + } + $definition = json_decode($definition, true); + $definition['id'] = $id; + + return $definition; + } + + /** + * Creates a Zone instance from the provided definition. + * + * @param array $definition The zone definition. + * @return Zone + */ + protected function createZoneFromDefinition(array $definition): Zone + { + $definition['label'] = $definition['name']; + + $territories = []; + foreach ($definition['members'] as $member) { + if ($member['type'] === 'zone') { + foreach ($this->loadDefinition($member['zone'])['members'] as $otherTerritory) { + $territories[] = $otherTerritory; + } + } else { + $territories[] = $member; + } + } + $definition['territories'] = $territories; + return new Zone($definition); + } +} diff --git a/src/Resolver/TaxType/StoreRegistrationCheckerTrait.php b/src/Resolver/TaxType/StoreRegistrationCheckerTrait.php index cab6da1..e8c73cd 100644 --- a/src/Resolver/TaxType/StoreRegistrationCheckerTrait.php +++ b/src/Resolver/TaxType/StoreRegistrationCheckerTrait.php @@ -3,9 +3,9 @@ namespace CommerceGuys\Tax\Resolver\TaxType; use CommerceGuys\Addressing\Address; +use CommerceGuys\Addressing\Zone\Zone; use CommerceGuys\Tax\Model\TaxTypeInterface; use CommerceGuys\Tax\Resolver\Context; -use CommerceGuys\Zone\Model\ZoneInterface; trait StoreRegistrationCheckerTrait { @@ -19,13 +19,13 @@ trait StoreRegistrationCheckerTrait /** * Checks whether the store is registered to collect taxes in the given zone. * - * @param ZoneInterface $zone The zone. + * @param Zone $zone The zone. * @param Context $context The context containing store information. * * @return bool True if the store is registered to collect taxes in the * given zone, false otherwise. */ - protected function checkStoreRegistration(ZoneInterface $zone, Context $context) + protected function checkStoreRegistration(Zone $zone, Context $context) { $storeRegistrations = $context->getStoreRegistrations(); foreach ($storeRegistrations as $country) { diff --git a/tests/Model/TaxTypeTest.php b/tests/Model/TaxTypeTest.php index 8a16547..45b987f 100755 --- a/tests/Model/TaxTypeTest.php +++ b/tests/Model/TaxTypeTest.php @@ -105,7 +105,8 @@ public function testRoundingMode() public function testZone() { $zone = $this - ->getMockBuilder('CommerceGuys\Zone\Model\Zone') + ->getMockBuilder('CommerceGuys\Addressing\Zone\Zone') + ->setConstructorArgs([['id' => 'test', 'label' => 'test label', 'territories' => [['country_code' => 'test']]]]) ->getMock(); $this->taxType->setZone($zone); diff --git a/tests/Repository/TaxTypeRepositoryTest.php b/tests/Repository/TaxTypeRepositoryTest.php index 209389a..537a8fd 100644 --- a/tests/Repository/TaxTypeRepositoryTest.php +++ b/tests/Repository/TaxTypeRepositoryTest.php @@ -109,7 +109,7 @@ public function testGet() { $taxType = $this->taxTypeRepository->get('fr_vat'); $this->assertInstanceOf('CommerceGuys\Tax\Model\TaxType', $taxType); - $this->assertInstanceOf('CommerceGuys\Zone\Model\Zone', $taxType->getZone()); + $this->assertInstanceOf('CommerceGuys\Addressing\Zone\Zone', $taxType->getZone()); $this->assertEquals('fr_vat', $taxType->getId()); $this->assertEquals('French VAT', $taxType->getName()); $this->assertEquals(GenericLabel::VAT, $taxType->getGenericLabel()); @@ -182,9 +182,9 @@ public function testGetAll() */ protected function getZoneRepository() { - $zone = $this->createMock('CommerceGuys\Zone\Model\Zone'); + $zone = $this->createMock('CommerceGuys\Addressing\Zone\Zone'); $zoneRepository = $this - ->getMockBuilder('CommerceGuys\Zone\Repository\ZoneRepository') + ->getMockBuilder('CommerceGuys\Tax\Repository\ZoneRepository') ->disableOriginalConstructor() ->getMock(); $zoneRepository diff --git a/tests/Resolver/TaxRate/DefaultTaxRateResolverTest.php b/tests/Resolver/TaxRate/DefaultTaxRateResolverTest.php index 9413375..97e443b 100644 --- a/tests/Resolver/TaxRate/DefaultTaxRateResolverTest.php +++ b/tests/Resolver/TaxRate/DefaultTaxRateResolverTest.php @@ -10,6 +10,8 @@ */ class DefaultTaxRateResolverTest extends TestCase { + private ?DefaultTaxRateResolver $resolver = null; + /** * {@inheritdoc} */ From 0ad9d8fe3876f69258b0084ba094da5917b10080 Mon Sep 17 00:00:00 2001 From: Jahidul Pabel Islam Date: Wed, 3 Jun 2026 23:12:21 +0100 Subject: [PATCH 2/2] Support commerceguys/addressing 2.x --- .github/workflows/phpci.yml | 5 +- composer.json | 6 +- .../TaxType/EuTaxTypeResolverTest.php | 60 ++++++++++--------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/.github/workflows/phpci.yml b/.github/workflows/phpci.yml index 9b1961d..0e3bdc3 100644 --- a/.github/workflows/phpci.yml +++ b/.github/workflows/phpci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] fail-fast: false name: CI PHP ${{ matrix.php-versions }} @@ -24,7 +24,6 @@ jobs: php_version: ${{ matrix.php-versions }} - name: PHPUnit Tests - uses: php-actions/phpunit@v3 + uses: php-actions/phpunit@v4 with: - version: "8" php_version: ${{ matrix.php-versions }} diff --git a/composer.json b/composer.json index 4f409b1..b1e7121 100644 --- a/composer.json +++ b/composer.json @@ -5,12 +5,12 @@ "keywords": ["tax", "vat"], "license": "MIT", "require": { - "php": ">=7.1", - "commerceguys/addressing": "^1.0", + "php": ">=7.4", + "commerceguys/addressing": "^1.0 || ^2.0", "doctrine/collections": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8.0", + "phpunit/phpunit": "^9.0", "mikey179/vfsstream": "^1.0" }, "autoload": { diff --git a/tests/Resolver/TaxType/EuTaxTypeResolverTest.php b/tests/Resolver/TaxType/EuTaxTypeResolverTest.php index 3f7c90a..88bcfff 100644 --- a/tests/Resolver/TaxType/EuTaxTypeResolverTest.php +++ b/tests/Resolver/TaxType/EuTaxTypeResolverTest.php @@ -7,6 +7,7 @@ use CommerceGuys\Tax\Resolver\TaxType\EuTaxTypeResolver; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; /** * @coversDefaultClass \CommerceGuys\Tax\Resolver\TaxType\EuTaxTypeResolver @@ -178,9 +179,10 @@ protected function createResolver() * @uses \CommerceGuys\Tax\Model\TaxType * @uses \CommerceGuys\Tax\Model\TaxRate * @uses \CommerceGuys\Tax\Model\TaxRateAmount - * @dataProvider dataProvider + * @dataProvider resolverProvider */ - public function testResolver($taxable, $context, $expected) + #[DataProvider("resolverProvider")] + public function testResolver($taxable, $context, $expected): void { $resolver = $this->createResolver(); @@ -197,16 +199,19 @@ public function testResolver($taxable, $context, $expected) /** * Provides data for the resolver test. */ - public function dataProvider() + public static function resolverProvider(): array { - $mockTaxableBuilder = $this->getMockBuilder('CommerceGuys\Tax\TaxableInterface'); + $self = new self(self::class); + + $mockTaxableBuilder = $self->getMockBuilder('CommerceGuys\Tax\TaxableInterface'); $physicalTaxable = $mockTaxableBuilder->getMock(); - $physicalTaxable->expects($this->any()) + + $physicalTaxable->expects($self->atLeastOnce()) ->method('isPhysical') - ->will($this->returnValue(true)); + ->will($self->returnValue(true)); $digitalTaxable = $mockTaxableBuilder->getMock(); - $serbianAddress = $this->createStub('CommerceGuys\Addressing\Address'); + $serbianAddress = $self->createStub('CommerceGuys\Addressing\Address'); $serbianAddress ->method('getCountryCode') ->willReturn('RS'); @@ -214,29 +219,30 @@ public function dataProvider() ->method('getPostalCode') ->willReturn('') ; - $frenchAddress = $this->createStub('CommerceGuys\Addressing\Address'); + $frenchAddress = $self->createStub('CommerceGuys\Addressing\Address'); $frenchAddress ->method('getCountryCode') ->willReturn('FR'); $frenchAddress ->method('getPostalCode') - ->willreturn('') + ->willReturn('') ; - $germanAddress = $this->createStub('CommerceGuys\Addressing\Address'); + + $germanAddress = $self->createStub('CommerceGuys\Addressing\Address'); $germanAddress ->method('getCountryCode') ->willReturn('DE'); $germanAddress ->method('getPostalCode') - ->willreturn('') + ->willReturn('') ; - $usAddress = $this->createStub('CommerceGuys\Addressing\Address'); + $usAddress = $self->createStub('CommerceGuys\Addressing\Address'); $usAddress ->method('getCountryCode') ->willReturn('US'); $usAddress ->method('getPostalCode') - ->willreturn('') + ->willReturn('') ; $date1 = new \DateTime('2014-02-24'); @@ -246,33 +252,33 @@ public function dataProvider() return [ // German customer, French store, VAT number provided. - [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '123'), 'eu_ic_vat'], + [$physicalTaxable, $self->getContext($germanAddress, $frenchAddress, '123'), 'eu_ic_vat'], // French customer, French store, VAT number provided. - [$physicalTaxable, $this->getContext($frenchAddress, $frenchAddress, '123'), 'fr_vat'], + [$physicalTaxable, $self->getContext($frenchAddress, $frenchAddress, '123'), 'fr_vat'], // German customer, French store, physical product. - [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date2), 'fr_vat'], + [$physicalTaxable, $self->getContext($germanAddress, $frenchAddress, '', [], $date2), 'fr_vat'], // German customer, French store registered for German VAT, physical product. - [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', ['DE'], $date2), 'de_vat'], + [$physicalTaxable, $self->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'], + [$digitalTaxable, $self->getContext($germanAddress, $frenchAddress, '', [], $date1), 'fr_vat'], // German customer, French store, digital product. - [$digitalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date2), 'de_vat'], + [$digitalTaxable, $self->getContext($germanAddress, $frenchAddress, '', [], $date2), 'de_vat'], // German customer, US store, digital product - [$digitalTaxable, $this->getContext($germanAddress, $usAddress, '', [], $date2), []], + [$digitalTaxable, $self->getContext($germanAddress, $usAddress, '', [], $date2), []], // German customer, US store registered in FR, digital product. - [$digitalTaxable, $this->getContext($germanAddress, $usAddress, '', ['FR'], $date2), 'de_vat'], + [$digitalTaxable, $self->getContext($germanAddress, $usAddress, '', ['FR'], $date2), 'de_vat'], // German customer with VAT number, US store registered in FR, digital product. - [$digitalTaxable, $this->getContext($germanAddress, $usAddress, '123', ['FR'], $date2), $notApplicable], + [$digitalTaxable, $self->getContext($germanAddress, $usAddress, '123', ['FR'], $date2), $notApplicable], // Serbian customer, French store, physical product. - [$physicalTaxable, $this->getContext($serbianAddress, $frenchAddress), []], + [$physicalTaxable, $self->getContext($serbianAddress, $frenchAddress), []], // French customer, Serbian store, physical product. - [$physicalTaxable, $this->getContext($frenchAddress, $serbianAddress), []], + [$physicalTaxable, $self->getContext($frenchAddress, $serbianAddress), []], // German customer, French store, digital product after July 1st 2021. - [$digitalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date3), 'de_vat'], + [$digitalTaxable, $self->getContext($germanAddress, $frenchAddress, '', [], $date3), 'de_vat'], // German customer, French store, physical product after July 1st 2021. - [$physicalTaxable, $this->getContext($germanAddress, $frenchAddress, '', [], $date3), 'de_vat'], + [$physicalTaxable, $self->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'], + [$physicalTaxable, $self->getContext($germanAddress, $usAddress, '', ['FR'], $date3), 'de_vat'], ]; }