├── .gitignore ├── src ├── Exception │ ├── ExceptionInterface.php │ └── UnknownZoneException.php ├── Repository │ ├── ZoneRepositoryInterface.php │ └── ZoneRepository.php ├── Model │ ├── ZoneMemberEntityInterface.php │ ├── ZoneMemberInterface.php │ ├── ZoneMemberEu.php │ ├── ZoneMemberZone.php │ ├── ZoneInterface.php │ ├── ZoneMember.php │ ├── ZoneEntityInterface.php │ ├── Zone.php │ └── ZoneMemberCountry.php └── Matcher │ ├── ZoneMatcherInterface.php │ └── ZoneMatcher.php ├── phpcs.xml ├── .editorconfig ├── .travis.yml ├── phpunit.xml ├── composer.json ├── LICENSE ├── tests ├── Model │ ├── ZoneMemberTest.php │ ├── ZoneMemberEuTest.php │ ├── ZoneMemberZoneTest.php │ ├── ZoneTest.php │ └── ZoneMemberCountryTest.php ├── Matcher │ └── ZoneMatcherTest.php └── Repository │ └── ZoneRepositoryTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PSR2 excluding line length 5 | 6 | 7 | 8 | 9 | 10 | 0 11 | 12 | 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | charset = utf-8 7 | end_of_line = LF 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{php,html,twig}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.md] 16 | max_line_length = 80 17 | 18 | [COMMIT_EDITMSG] 19 | max_line_length = 0 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | 4 | php: 5 | - 7.1 6 | - 7.0 7 | - 5.6 8 | - 5.5 9 | 10 | install: 11 | - composer self-update 12 | - composer install 13 | 14 | script: 15 | - ./vendor/bin/phpunit -c ./phpunit.xml --coverage-text --strict 16 | - ./vendor/bin/phpcs --standard=phpcs.xml src -s 17 | - ./vendor/bin/phpcs --standard=phpcs.xml tests -s 18 | 19 | matrix: 20 | fast_finish: true 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | 20 | ./src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Repository/ZoneRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | =5.5.0", 8 | "commerceguys/addressing": "~1.0" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "~4.0", 12 | "mikey179/vfsstream": "1.*", 13 | "squizlabs/php_codesniffer": "2.*" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "CommerceGuys\\Zone\\": "src" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "CommerceGuys\\Zone\\Tests\\": "tests" 23 | } 24 | }, 25 | "authors": [ 26 | { 27 | "name": "Bojan Zivanovic" 28 | } 29 | ], 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "1.x-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Model/ZoneMemberInterface.php: -------------------------------------------------------------------------------- 1 | name = 'EU'; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function match(AddressInterface $address) 27 | { 28 | $euCountries = [ 29 | 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 30 | 'FR', 'GB', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 31 | 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK', 32 | ]; 33 | $countryCode = $address->getCountryCode(); 34 | 35 | return in_array($countryCode, $euCountries); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Commerce Guys 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Model/ZoneMemberZone.php: -------------------------------------------------------------------------------- 1 | zone->getName(); 25 | } 26 | 27 | /** 28 | * Gets the zone. 29 | * 30 | * @return ZoneInterface The zone matched by the zone member. 31 | */ 32 | public function getZone() 33 | { 34 | return $this->zone; 35 | } 36 | 37 | /** 38 | * Sets the zone. 39 | * 40 | * @param ZoneEntityInterface $zone The zone matched by the zone member. 41 | * 42 | * @return self 43 | */ 44 | public function setZone(ZoneEntityInterface $zone) 45 | { 46 | $this->zone = $zone; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function match(AddressInterface $address) 55 | { 56 | return $this->zone->match($address); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Model/ZoneMemberTest.php: -------------------------------------------------------------------------------- 1 | zoneMember = $this->getMockForAbstractClass('\CommerceGuys\Zone\Model\ZoneMember'); 21 | } 22 | 23 | /** 24 | * @covers ::getId 25 | * @covers ::setId 26 | */ 27 | public function testId() 28 | { 29 | $this->zoneMember->setId('fr_tax'); 30 | $this->assertEquals('fr_tax', $this->zoneMember->getId()); 31 | } 32 | 33 | /** 34 | * @covers ::getName 35 | * @covers ::setName 36 | */ 37 | public function testName() 38 | { 39 | $this->zoneMember->setName('France'); 40 | $this->assertEquals('France', $this->zoneMember->getName()); 41 | } 42 | 43 | /** 44 | * @covers ::getParentZone 45 | * @covers ::setParentZone 46 | */ 47 | public function testParentZone() 48 | { 49 | $zone = $this 50 | ->getMockBuilder('CommerceGuys\Zone\Model\Zone') 51 | ->disableOriginalConstructor() 52 | ->getMock(); 53 | 54 | $this->zoneMember->setParentZone($zone); 55 | $this->assertEquals($zone, $this->zoneMember->getParentZone()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Model/ZoneInterface.php: -------------------------------------------------------------------------------- 1 | id; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function setId($id) 45 | { 46 | $this->id = $id; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getName() 55 | { 56 | return $this->name; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function setName($name) 63 | { 64 | $this->name = $name; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getParentZone() 73 | { 74 | return $this->parentZone; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function setParentZone(ZoneEntityInterface $parentZone = null) 81 | { 82 | $this->parentZone = $parentZone; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Model/ZoneMemberEuTest.php: -------------------------------------------------------------------------------- 1 | zoneMember = new ZoneMemberEu(); 23 | } 24 | 25 | /** 26 | * @covers ::__construct 27 | * 28 | * @uses \CommerceGuys\Zone\Model\ZoneMemberEu::getName 29 | */ 30 | public function testConstructor() 31 | { 32 | $this->assertEquals('EU', $this->zoneMember->getName()); 33 | } 34 | 35 | /** 36 | * @covers ::match 37 | * 38 | * @uses \CommerceGuys\Zone\Model\ZoneMemberEu::__construct 39 | */ 40 | public function testMatch() 41 | { 42 | $mockBuilder = $this 43 | ->getMockBuilder('CommerceGuys\Addressing\Address') 44 | ->disableOriginalConstructor(); 45 | 46 | $frenchAddress = $mockBuilder->getMock(); 47 | $frenchAddress 48 | ->expects($this->any()) 49 | ->method('getCountryCode') 50 | ->will($this->returnValue('FR')); 51 | $serbianAddress = $mockBuilder->getMock(); 52 | $serbianAddress 53 | ->expects($this->any()) 54 | ->method('getCountryCode') 55 | ->will($this->returnValue('RS')); 56 | 57 | $this->assertEquals(true, $this->zoneMember->match($frenchAddress)); 58 | $this->assertEquals(false, $this->zoneMember->match($serbianAddress)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Matcher/ZoneMatcher.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function match(AddressInterface $address, $scope = null) 31 | { 32 | $zones = $this->matchAll($address, $scope); 33 | 34 | return count($zones) ? $zones[0] : null; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function matchAll(AddressInterface $address, $scope = null) 41 | { 42 | // Find all matching zones. 43 | $results = []; 44 | foreach ($this->repository->getAll($scope) as $zone) { 45 | if ($zone->match($address)) { 46 | $results[] = [ 47 | 'priority' => (int) $zone->getPriority(), 48 | 'zone' => $zone, 49 | ]; 50 | } 51 | } 52 | // Sort the matched zones by priority. 53 | usort($results, function ($a, $b) { 54 | if ($a['priority'] == $b['priority']) { 55 | return 0; 56 | } 57 | 58 | return ($a['priority'] > $b['priority']) ? -1 : 1; 59 | }); 60 | // Create the final zone array from the results. 61 | $zones = []; 62 | foreach ($results as $result) { 63 | $zones[] = $result['zone']; 64 | } 65 | 66 | return $zones; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Model/ZoneEntityInterface.php: -------------------------------------------------------------------------------- 1 | zoneMember = new ZoneMemberZone(); 23 | } 24 | 25 | /** 26 | * @covers ::getName 27 | * 28 | * @uses \CommerceGuys\Zone\Model\ZoneMemberZone::setZone 29 | */ 30 | public function testName() 31 | { 32 | $zone = $this 33 | ->getMockBuilder('CommerceGuys\Zone\Model\Zone') 34 | ->disableOriginalConstructor() 35 | ->getMock(); 36 | $zone 37 | ->expects($this->any()) 38 | ->method('getName') 39 | ->will($this->returnValue('Test')); 40 | 41 | $this->zoneMember->setZone($zone); 42 | $this->assertEquals('Test', $this->zoneMember->getName()); 43 | } 44 | 45 | /** 46 | * @covers ::getZone 47 | * @covers ::setZone 48 | */ 49 | public function testZone() 50 | { 51 | $zone = $this 52 | ->getMockBuilder('CommerceGuys\Zone\Model\Zone') 53 | ->disableOriginalConstructor() 54 | ->getMock(); 55 | 56 | $this->zoneMember->setZone($zone); 57 | $this->assertEquals($zone, $this->zoneMember->getZone()); 58 | } 59 | 60 | /** 61 | * @covers ::match 62 | * 63 | * @uses \CommerceGuys\Zone\Model\ZoneMemberZone::setZone 64 | */ 65 | public function testMatch() 66 | { 67 | $address = $this 68 | ->getMockBuilder('CommerceGuys\Addressing\Address') 69 | ->disableOriginalConstructor() 70 | ->getMock(); 71 | $matchingZone = $this 72 | ->getMockBuilder('CommerceGuys\Zone\Model\Zone') 73 | ->disableOriginalConstructor() 74 | ->getMock(); 75 | $matchingZone 76 | ->expects($this->any()) 77 | ->method('match') 78 | ->with($address) 79 | ->will($this->returnValue(true)); 80 | $nonMatchingZone = $this 81 | ->getMockBuilder('CommerceGuys\Zone\Model\Zone') 82 | ->disableOriginalConstructor() 83 | ->getMock(); 84 | $nonMatchingZone 85 | ->expects($this->any()) 86 | ->method('match') 87 | ->with($address) 88 | ->will($this->returnValue(false)); 89 | 90 | $this->zoneMember->setZone($matchingZone); 91 | $this->assertEquals(true, $this->zoneMember->match($address)); 92 | 93 | $this->zoneMember->setZone($nonMatchingZone); 94 | $this->assertEquals(false, $this->zoneMember->match($address)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zone 2 | ===== 3 | 4 | *Important:* A newer version of the zone functionality is now included directly in the [commerceguys/addressing](https://github.com/commerceguys/addressing) library. This library is deprecated. 5 | 6 | [![Build Status](https://travis-ci.org/commerceguys/zone.svg?branch=master)](https://travis-ci.org/commerceguys/zone) 7 | 8 | A PHP 5.5+ zone management library. Requires [commerceguys/addressing](https://github.com/commerceguys/addressing). 9 | 10 | Zones are territorial groupings mostly used for shipping or tax purposes. 11 | For example, a set of shipping rates associated with a zone where the rates 12 | become available only if the customer's address matches the zone. 13 | 14 | A zone can match other zones, countries, subdivisions (states/provinces/municipalities), postal codes. 15 | Postal codes can also be expressed using ranges or regular expressions. 16 | 17 | Examples of zones: 18 | - California and Nevada 19 | - Belgium, Netherlands, Luxemburg 20 | - European Union 21 | - Germany and a set of Austrian postal codes (6691, 6991, 6992, 6993) 22 | - Austria without specific postal codes (6691, 6991, 6992, 6993) 23 | 24 | # Data model 25 | 26 | Each [zone](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneInterface.php) has [zone members](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneMemberInterface.php). 27 | A zone matches the provided address if one of its zone members matches the provided address. 28 | 29 | The base interfaces don't impose setters, since they aren't needed by the service classes. 30 | Extended interfaces ([ZoneEntityInterface](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneEntityInterface.php), [ZoneMemberEntityInterface](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneMemberEntityInterface.php)) are provided for that purpose, 31 | as well as matching [Zone](https://github.com/commerceguys/zone/blob/master/src/Model/Zone.php) and [ZoneMember](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneMember.php) classes that can be used as examples or mapped by Doctrine. 32 | 33 | The library contains two types of zone members: 34 | - [country](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneMemberCountry.php) (matches a country, its subdivisions, included/excluded postal codes) 35 | - [zone](https://github.com/commerceguys/zone/blob/master/src/Model/ZoneMemberZone.php) (matches a zone) 36 | 37 | ```php 38 | use CommerceGuys\Addressing\Address; 39 | use CommerceGuys\Zone\Model\Zone; 40 | use CommerceGuys\Zone\Model\ZoneMemberCountry; 41 | 42 | $zone = new Zone(); 43 | $zone->setId('german_vat'); 44 | $zone->setName('German VAT'); 45 | $zone->setScope('tax'); 46 | 47 | // Create the German VAT zone (Germany and 4 Austrian postal codes). 48 | $germanyZoneMember = new ZoneMemberCountry(); 49 | $germanyZoneMember->setCountryCode('DE'); 50 | $zone->addMember($germanyZoneMember); 51 | 52 | $austriaZoneMember = new ZoneMemberCountry(); 53 | $austriaZoneMember->setCountryCode('AT'); 54 | $austriaZoneMember->setIncludedPostalCodes('6691, 6991:6993'); 55 | $zone->addMember($austriaZoneMember); 56 | 57 | // Check if the provided austrian address matches the German VAT zone. 58 | $austrianAddress = new Address(); 59 | $austrianAddress = $austrianAddress 60 | ->withCountryCode('AT') 61 | ->withPostalCode('6692'); 62 | echo $zone->match($austrianAddress); // true 63 | ``` 64 | 65 | # Matcher 66 | 67 | A matcher class is provided for the use case where an address should be matched 68 | against all zones in the system, with the matched zones ordered by priority. 69 | 70 | ```php 71 | use CommerceGuys\Addressing\Address; 72 | use CommerceGuys\Zone\Matcher\ZoneMatcher; 73 | use CommerceGuys\Zone\Repository\ZoneRepository; 74 | 75 | // Initialize the default repository which loads zones from json files stored in 76 | // resources/zone. A different repository might load them from the database, etc. 77 | $repository = new ZoneRepository('resources/zone'); 78 | $matcher = new ZoneMatcher($repository); 79 | 80 | $austrianAddress = new Address(); 81 | $austrianAddress = $austrianAddress 82 | ->withCountryCode('AT') 83 | ->withPostalCode('6692'); 84 | 85 | // Get all matching zones. 86 | $zones = $matcher->matchAll($austrianAddress); 87 | // Get all matching zones for the 'tax' scope. 88 | $zones = $matcher->matchAll($austrianAddress, 'tax'); 89 | 90 | // Get the best matching zone. 91 | $zone = $matcher->match($austrianAddress); 92 | // Get the best matching zone for the 'shipping' scope. 93 | $zone = $matcher->match($austrianAddress, 'shipping'); 94 | ``` 95 | -------------------------------------------------------------------------------- /src/Model/Zone.php: -------------------------------------------------------------------------------- 1 | members = new ArrayCollection(); 57 | } 58 | 59 | /** 60 | * Returns the string representation of the zone. 61 | * 62 | * @return string 63 | */ 64 | public function __toString() 65 | { 66 | return $this->name; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getId() 73 | { 74 | return $this->id; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function setId($id) 81 | { 82 | $this->id = $id; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function getName() 91 | { 92 | return $this->name; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function setName($name) 99 | { 100 | $this->name = $name; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function getScope() 109 | { 110 | return $this->scope; 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | public function setScope($scope) 117 | { 118 | $this->scope = $scope; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function getPriority() 127 | { 128 | return $this->priority; 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | public function setPriority($priority) 135 | { 136 | $this->priority = $priority; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function getMembers() 145 | { 146 | return $this->members; 147 | } 148 | 149 | /** 150 | * {@inheritdoc} 151 | */ 152 | public function setMembers(Collection $members) 153 | { 154 | $this->members = $members; 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * {@inheritdoc} 161 | */ 162 | public function hasMembers() 163 | { 164 | return !$this->members->isEmpty(); 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function addMember(ZoneMemberEntityInterface $member) 171 | { 172 | if (!$this->hasMember($member)) { 173 | $member->setParentZone($this); 174 | $this->members->add($member); 175 | } 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function removeMember(ZoneMemberEntityInterface $member) 184 | { 185 | if ($this->hasMember($member)) { 186 | $member->setParentZone(null); 187 | $this->members->removeElement($member); 188 | } 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * {@inheritdoc} 195 | */ 196 | public function hasMember(ZoneMemberEntityInterface $member) 197 | { 198 | return $this->members->contains($member); 199 | } 200 | 201 | /** 202 | * {@inheritdoc} 203 | */ 204 | public function match(AddressInterface $address) 205 | { 206 | foreach ($this->members as $member) { 207 | if ($member->match($address)) { 208 | return true; 209 | } 210 | } 211 | 212 | return false; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/Model/ZoneTest.php: -------------------------------------------------------------------------------- 1 | zone = new Zone(); 24 | } 25 | 26 | /** 27 | * @covers ::getId 28 | * @covers ::setId 29 | * 30 | * @uses \CommerceGuys\Zone\Model\Zone::__construct 31 | */ 32 | public function testId() 33 | { 34 | $this->zone->setId('north_america'); 35 | $this->assertEquals('north_america', $this->zone->getId()); 36 | } 37 | 38 | /** 39 | * @covers ::getName 40 | * @covers ::setName 41 | * @covers ::__toString 42 | * 43 | * @uses \CommerceGuys\Zone\Model\Zone::__construct 44 | */ 45 | public function testName() 46 | { 47 | $this->zone->setName('North America'); 48 | $this->assertEquals('North America', $this->zone->getName()); 49 | $this->assertEquals('North America', (string) $this->zone); 50 | } 51 | 52 | /** 53 | * @covers ::getScope 54 | * @covers ::setScope 55 | * 56 | * @uses \CommerceGuys\Zone\Model\Zone::__construct 57 | */ 58 | public function testScope() 59 | { 60 | $this->zone->setScope('shipping'); 61 | $this->assertEquals('shipping', $this->zone->getScope()); 62 | } 63 | 64 | /** 65 | * @covers ::getPriority 66 | * @covers ::setPriority 67 | * 68 | * @uses \CommerceGuys\Zone\Model\Zone::__construct 69 | */ 70 | public function testPriority() 71 | { 72 | $this->zone->setPriority(10); 73 | $this->assertEquals(10, $this->zone->getPriority()); 74 | } 75 | 76 | /** 77 | * @covers ::__construct 78 | * @covers ::getMembers 79 | * @covers ::setMembers 80 | * @covers ::hasMembers 81 | * @covers ::addMember 82 | * @covers ::removeMember 83 | * @covers ::hasMember 84 | * 85 | * @uses \CommerceGuys\Zone\Model\Zone::__construct 86 | * @uses \CommerceGuys\Zone\Model\ZoneMember::setParentZone 87 | */ 88 | public function testMembers() 89 | { 90 | $firstZoneMember = $this 91 | ->getMockBuilder('CommerceGuys\Zone\Model\ZoneMember') 92 | ->getMock(); 93 | $secondZoneMember = $this 94 | ->getMockBuilder('CommerceGuys\Zone\Model\ZoneMember') 95 | ->getMock(); 96 | $empty = new ArrayCollection(); 97 | $members = new ArrayCollection([$firstZoneMember, $secondZoneMember]); 98 | 99 | $this->assertEquals(false, $this->zone->hasMembers()); 100 | $this->assertEquals($empty, $this->zone->getMembers()); 101 | $members = new ArrayCollection([$firstZoneMember, $secondZoneMember]); 102 | $this->zone->setMembers($members); 103 | $this->assertEquals($members, $this->zone->getMembers()); 104 | $this->assertEquals(true, $this->zone->hasMembers()); 105 | $this->zone->removeMember($secondZoneMember); 106 | $this->assertEquals(false, $this->zone->hasMember($secondZoneMember)); 107 | $this->assertEquals(true, $this->zone->hasMember($firstZoneMember)); 108 | $this->zone->addMember($secondZoneMember); 109 | $this->assertEquals($members, $this->zone->getMembers()); 110 | } 111 | 112 | /** 113 | * @covers ::match 114 | * 115 | * @uses \CommerceGuys\Zone\Model\Zone::__construct 116 | * @uses \CommerceGuys\Zone\Model\Zone::setMembers 117 | */ 118 | public function testMatch() 119 | { 120 | $address = $this 121 | ->getMockBuilder('CommerceGuys\Addressing\Address') 122 | ->getMock(); 123 | $matchingZoneMember = $this 124 | ->getMockBuilder('CommerceGuys\Zone\Model\ZoneMember') 125 | ->getMock(); 126 | $matchingZoneMember 127 | ->expects($this->any()) 128 | ->method('match') 129 | ->with($address) 130 | ->will($this->returnValue(true)); 131 | $nonMatchingZoneMember = $this 132 | ->getMockBuilder('CommerceGuys\Zone\Model\ZoneMember') 133 | ->getMock(); 134 | $nonMatchingZoneMember 135 | ->expects($this->any()) 136 | ->method('match') 137 | ->with($address) 138 | ->will($this->returnValue(false)); 139 | 140 | $members = new ArrayCollection([$matchingZoneMember, $nonMatchingZoneMember]); 141 | $this->zone->setMembers($members); 142 | $this->assertEquals(true, $this->zone->match($address)); 143 | 144 | $members = new ArrayCollection([$nonMatchingZoneMember]); 145 | $this->zone->setMembers($members); 146 | $this->assertEquals(false, $this->zone->match($address)); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /tests/Matcher/ZoneMatcherTest.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'id' => 'fr', 23 | 'priority' => 0, 24 | 'match' => false, 25 | ], 26 | 'de' => [ 27 | 'id' => 'de', 28 | 'priority' => 0, 29 | 'match' => true, 30 | ], 31 | 'de2' => [ 32 | 'id' => 'de2', 33 | 'priority' => 0, 34 | 'match' => true, 35 | ], 36 | 'de3' => [ 37 | 'id' => 'de3', 38 | 'priority' => 2, 39 | 'match' => true, 40 | ], 41 | ]; 42 | 43 | /** 44 | * The zone matcher. 45 | * 46 | * @var ZoneMatcher 47 | */ 48 | protected $matcher; 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function setUp() 54 | { 55 | $zones = []; 56 | foreach ($this->zones as $definition) { 57 | $zones[] = $this->getZone($definition['id'], $definition['priority'], $definition['match']); 58 | } 59 | $repository = $this 60 | ->getMockBuilder('CommerceGuys\Zone\Repository\ZoneRepository') 61 | ->disableOriginalConstructor() 62 | ->getMock(); 63 | $repository 64 | ->expects($this->any()) 65 | ->method('getAll') 66 | ->will($this->returnValue($zones)); 67 | $this->matcher = new ZoneMatcher($repository); 68 | } 69 | 70 | /** 71 | * @covers ::__construct 72 | */ 73 | public function testConstructor() 74 | { 75 | // Note: other tests use $this->matcher instead of depending on 76 | // testConstructor because of a phpunit bug with dependencies and mocks: 77 | // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/127 78 | $repository = $this 79 | ->getMockBuilder('CommerceGuys\Zone\Repository\ZoneRepository') 80 | ->disableOriginalConstructor() 81 | ->getMock(); 82 | $matcher = new ZoneMatcher($repository); 83 | // Confirm that the repository was properly set. 84 | $this->assertSame($repository, $this->getObjectAttribute($matcher, 'repository')); 85 | } 86 | 87 | /** 88 | * @covers ::match 89 | * 90 | * @uses \CommerceGuys\Zone\Matcher\ZoneMatcher::__construct 91 | * @uses \CommerceGuys\Zone\Matcher\ZoneMatcher::matchAll 92 | */ 93 | public function testMatch() 94 | { 95 | $address = $this 96 | ->getMockBuilder('CommerceGuys\Addressing\Address') 97 | ->disableOriginalConstructor() 98 | ->getMock(); 99 | $zone = $this->matcher->match($address); 100 | $this->assertInstanceOf('CommerceGuys\Zone\Model\Zone', $zone); 101 | $this->assertEquals('de3', $zone->getId()); 102 | } 103 | 104 | /** 105 | * @covers ::matchAll 106 | * 107 | * @uses \CommerceGuys\Zone\Matcher\ZoneMatcher::__construct 108 | */ 109 | public function testMatchAll() 110 | { 111 | $address = $this 112 | ->getMockBuilder('CommerceGuys\Addressing\Address') 113 | ->disableOriginalConstructor() 114 | ->getMock(); 115 | $zones = $this->matcher->matchAll($address); 116 | $this->assertCount(3, $zones); 117 | // de3 must come first because it has the highest priority. 118 | $this->assertEquals('de3', $zones[0]->getId()); 119 | // The other two zones have the same priority, so their order is 120 | // undefined and different between PHP and HHVM. 121 | $otherIds = []; 122 | $otherIds[] = $zones[1]->getId(); 123 | $otherIds[] = $zones[2]->getId(); 124 | $this->assertContains('de2', $otherIds); 125 | $this->assertContains('de', $otherIds); 126 | } 127 | 128 | /** 129 | * Returns a mock zone based on the provided data. 130 | * 131 | * @param string $id The zone id. 132 | * @param int $priority The zone priority. 133 | * @param bool $match Whether the zone should match. 134 | * 135 | * @return \CommerceGuys\Zone\Model\Zone 136 | */ 137 | protected function getZone($id, $priority, $match) 138 | { 139 | $zone = $this 140 | ->getMockBuilder('CommerceGuys\Zone\Model\Zone') 141 | ->disableOriginalConstructor() 142 | ->getMock(); 143 | $zone 144 | ->expects($this->any()) 145 | ->method('getId') 146 | ->will($this->returnValue($id)); 147 | $zone 148 | ->expects($this->any()) 149 | ->method('getPriority') 150 | ->will($this->returnValue($priority)); 151 | $zone 152 | ->expects($this->any()) 153 | ->method('match') 154 | ->with($this->anything()) 155 | ->will($this->returnValue($match)); 156 | 157 | return $zone; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Model/ZoneMemberCountry.php: -------------------------------------------------------------------------------- 1 | countryCode; 69 | } 70 | 71 | /** 72 | * Sets the country code. 73 | * 74 | * @param string $countryCode The country code. 75 | * 76 | * @return self 77 | */ 78 | public function setCountryCode($countryCode) 79 | { 80 | $this->countryCode = $countryCode; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Gets the administrative area. 87 | * 88 | * @return string|null The administrative area, or null if all should match. 89 | */ 90 | public function getAdministrativeArea() 91 | { 92 | return $this->administrativeArea; 93 | } 94 | 95 | /** 96 | * Sets the administrative area. 97 | * 98 | * @param string|null $administrativeArea The administrative area. 99 | * 100 | * @return self 101 | */ 102 | public function setAdministrativeArea($administrativeArea = null) 103 | { 104 | $this->administrativeArea = $administrativeArea; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Gets the locality. 111 | * 112 | * @return string|null The locality, or null if all should match. 113 | */ 114 | public function getLocality() 115 | { 116 | return $this->locality; 117 | } 118 | 119 | /** 120 | * Sets the locality. 121 | * 122 | * @param string|null $locality The locality. 123 | * 124 | * @return self 125 | */ 126 | public function setLocality($locality = null) 127 | { 128 | $this->locality = $locality; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Gets the dependent locality. 135 | * 136 | * @return string|null The dependent locality, or null if all should match. 137 | */ 138 | public function getDependentLocality() 139 | { 140 | return $this->dependentLocality; 141 | } 142 | 143 | /** 144 | * Sets the dependent locality. 145 | * 146 | * @param string|null $dependentLocality The dependent locality. 147 | * 148 | * @return self 149 | */ 150 | public function setDependentLocality($dependentLocality = null) 151 | { 152 | $this->dependentLocality = $dependentLocality; 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Gets the included postal codes. 159 | * 160 | * @return string The included postal codes. 161 | */ 162 | public function getIncludedPostalCodes() 163 | { 164 | return $this->includedPostalCodes; 165 | } 166 | 167 | /** 168 | * Sets the included postal codes. 169 | * 170 | * @param string $includedPostalCodes The included postal codes. 171 | * 172 | * @return self 173 | */ 174 | public function setIncludedPostalCodes($includedPostalCodes) 175 | { 176 | $this->includedPostalCodes = $includedPostalCodes; 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Gets the excluded postal codes. 183 | * 184 | * @return string The excluded postal codes. 185 | */ 186 | public function getExcludedPostalCodes() 187 | { 188 | return $this->excludedPostalCodes; 189 | } 190 | 191 | /** 192 | * Sets the excluded postal codes. 193 | * 194 | * @param string $excludedPostalCodes The excluded postal codes. 195 | * 196 | * @return self 197 | */ 198 | public function setExcludedPostalCodes($excludedPostalCodes) 199 | { 200 | $this->excludedPostalCodes = $excludedPostalCodes; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function match(AddressInterface $address) 209 | { 210 | if ($address->getCountryCode() != $this->countryCode) { 211 | return false; 212 | } 213 | if ($this->administrativeArea && $this->administrativeArea != $address->getAdministrativeArea()) { 214 | return false; 215 | } 216 | if ($this->locality && $this->locality != $address->getLocality()) { 217 | return false; 218 | } 219 | if ($this->dependentLocality && $this->dependentLocality != $address->getDependentLocality()) { 220 | return false; 221 | } 222 | if (!PostalCodeHelper::match($address->getPostalCode(), $this->includedPostalCodes, $this->excludedPostalCodes)) { 223 | return false; 224 | } 225 | 226 | return true; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Repository/ZoneRepository.php: -------------------------------------------------------------------------------- 1 | definitionPath = $definitionPath; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function get($id) 50 | { 51 | if (!isset($this->zones[$id])) { 52 | $definition = $this->loadDefinition($id); 53 | $this->zones[$id] = $this->createZoneFromDefinition($definition); 54 | } 55 | 56 | return $this->zones[$id]; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function getAll($scope = null) 63 | { 64 | // Build the list of all available zones. 65 | if (empty($this->zoneIndex)) { 66 | if ($handle = opendir($this->definitionPath)) { 67 | while (false !== ($entry = readdir($handle))) { 68 | if (substr($entry, 0, 1) != '.') { 69 | $id = strtok($entry, '.'); 70 | $this->zoneIndex[] = $id; 71 | } 72 | } 73 | closedir($handle); 74 | } 75 | } 76 | 77 | // Load each zone, filter by scope if needed. 78 | $zones = []; 79 | foreach ($this->zoneIndex as $id) { 80 | $zone = $this->get($id); 81 | if (is_null($scope) || ($zone->getScope() == $scope)) { 82 | $zones[$id] = $this->get($id); 83 | } 84 | } 85 | 86 | return $zones; 87 | } 88 | 89 | /** 90 | * Loads the zone definition for the provided id. 91 | * 92 | * @param string $id The zone id. 93 | * 94 | * @return array The zone definition. 95 | */ 96 | protected function loadDefinition($id) 97 | { 98 | $filename = $this->definitionPath . $id . '.json'; 99 | $definition = @file_get_contents($filename); 100 | if (empty($definition)) { 101 | throw new UnknownZoneException($id); 102 | } 103 | $definition = json_decode($definition, true); 104 | $definition['id'] = $id; 105 | 106 | return $definition; 107 | } 108 | 109 | /** 110 | * Creates a Zone instance from the provided definition. 111 | * 112 | * @param array $definition The zone definition. 113 | * 114 | * @return Zone 115 | */ 116 | protected function createZoneFromDefinition(array $definition) 117 | { 118 | $zone = new Zone(); 119 | // Bind the closure to the Zone object, giving it access to 120 | // its protected properties. Faster than both setters and reflection. 121 | $setValues = \Closure::bind(function ($definition) { 122 | $this->id = $definition['id']; 123 | $this->name = $definition['name']; 124 | if (isset($definition['scope'])) { 125 | $this->scope = $definition['scope']; 126 | } 127 | if (isset($definition['priority'])) { 128 | $this->priority = $definition['priority']; 129 | } 130 | }, $zone, '\CommerceGuys\Zone\Model\Zone'); 131 | $setValues($definition); 132 | 133 | // Add the zone members. 134 | foreach ($definition['members'] as $memberDefinition) { 135 | if ($memberDefinition['type'] == 'country') { 136 | $zoneMember = $this->createZoneMemberCountryFromDefinition($memberDefinition); 137 | $zone->addMember($zoneMember); 138 | } elseif ($memberDefinition['type'] == 'zone') { 139 | $zoneMember = $this->createZoneMemberZoneFromDefinition($memberDefinition); 140 | $zone->addMember($zoneMember); 141 | } 142 | } 143 | 144 | return $zone; 145 | } 146 | 147 | /** 148 | * Creates a ZoneMemberCountry instance from the provided definition. 149 | * 150 | * @param array $definition The zone member definition. 151 | * 152 | * @return ZoneMemberCountry 153 | */ 154 | protected function createZoneMemberCountryFromDefinition(array $definition) 155 | { 156 | $zoneMember = new ZoneMemberCountry(); 157 | $setValues = \Closure::bind(function ($definition) { 158 | $this->id = $definition['id']; 159 | $this->name = $definition['name']; 160 | $this->countryCode = $definition['country_code']; 161 | if (isset($definition['administrative_area'])) { 162 | $this->administrativeArea = $definition['administrative_area']; 163 | } 164 | if (isset($definition['locality'])) { 165 | $this->locality = $definition['locality']; 166 | } 167 | if (isset($definition['dependent_locality'])) { 168 | $this->dependentLocality = $definition['dependent_locality']; 169 | } 170 | if (isset($definition['included_postal_codes'])) { 171 | $this->includedPostalCodes = $definition['included_postal_codes']; 172 | } 173 | if (isset($definition['excluded_postal_codes'])) { 174 | $this->excludedPostalCodes = $definition['excluded_postal_codes']; 175 | } 176 | }, $zoneMember, '\CommerceGuys\Zone\Model\ZoneMemberCountry'); 177 | $setValues($definition); 178 | 179 | return $zoneMember; 180 | } 181 | 182 | /** 183 | * Creates a ZoneMemberZone instance from the provided definition. 184 | * 185 | * @param array $definition The zone member definition. 186 | * 187 | * @return ZoneMemberZone 188 | */ 189 | protected function createZoneMemberZoneFromDefinition(array $definition) 190 | { 191 | $zone = $this->get($definition['zone']); 192 | $zoneMember = new ZoneMemberZone(); 193 | $zoneMember->setZone($zone); 194 | $setValues = \Closure::bind(function ($definition) { 195 | $this->id = $definition['id']; 196 | }, $zoneMember, '\CommerceGuys\Zone\Model\ZoneMemberZone'); 197 | $setValues($definition); 198 | 199 | return $zoneMember; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/Repository/ZoneRepositoryTest.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'name' => 'Germany', 23 | 'scope' => 'shipping', 24 | 'members' => [ 25 | [ 26 | 'type' => 'country', 27 | 'id' => '1', 28 | 'name' => 'Germany', 29 | 'country_code' => 'DE', 30 | ], 31 | ], 32 | ], 33 | 'de_vat' => [ 34 | 'name' => 'Germany', 35 | 'scope' => 'tax', 36 | 'priority' => 1, 37 | // A real zone wouldn't reference a zone of a different 38 | // scope (like here with de_vat -> de), but it decreases the 39 | // amount of data in this test. 40 | 'members' => [ 41 | [ 42 | 'type' => 'zone', 43 | 'id' => '2', 44 | 'zone' => 'de', 45 | ], 46 | [ 47 | 'type' => 'country', 48 | 'id' => '3', 49 | 'name' => 'Austria', 50 | 'country_code' => 'AT', 51 | 'included_postal_codes' => '6691, 6991:6993', 52 | // Dummy data to ensure all fields get tested. 53 | 'administrative_area' => 'dummy', 54 | 'locality' => 'dummy', 55 | 'dependent_locality' => 'dummy', 56 | 'excluded_postal_codes' => '123456', 57 | ], 58 | ], 59 | ], 60 | ]; 61 | 62 | /** 63 | * @covers ::__construct 64 | */ 65 | public function testConstructor() 66 | { 67 | // Mock the existence of JSON definitions on the filesystem. 68 | $root = vfsStream::setup('resources'); 69 | $directory = vfsStream::newDirectory('zone')->at($root); 70 | foreach ($this->zones as $id => $definition) { 71 | $filename = $id . '.json'; 72 | vfsStream::newFile($filename)->at($directory)->setContent(json_encode($definition)); 73 | } 74 | 75 | // Instantiate the zone repository and confirm that the 76 | // definition path was properly set. 77 | $zoneRepository = new ZoneRepository('vfs://resources/zone/'); 78 | $definitionPath = $this->getObjectAttribute($zoneRepository, 'definitionPath'); 79 | $this->assertEquals('vfs://resources/zone/', $definitionPath); 80 | 81 | return $zoneRepository; 82 | } 83 | 84 | /** 85 | * @covers ::get 86 | * @covers ::loadDefinition 87 | * @covers ::createZoneFromDefinition 88 | * @covers ::createZoneMemberCountryFromDefinition 89 | * @covers ::createZoneMemberZoneFromDefinition 90 | * 91 | * @uses \CommerceGuys\Zone\Model\Zone 92 | * @uses \CommerceGuys\Zone\Model\ZoneMember 93 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry 94 | * @uses \CommerceGuys\Zone\Model\ZoneMemberZone 95 | * @uses \CommerceGuys\Addressing\PostalCodeHelper 96 | * @depends testConstructor 97 | */ 98 | public function testGet($zoneRepository) 99 | { 100 | $zone = $zoneRepository->get('de_vat'); 101 | $this->assertInstanceOf('CommerceGuys\Zone\Model\Zone', $zone); 102 | $this->assertEquals('de_vat', $zone->getId()); 103 | $this->assertEquals('Germany', $zone->getName()); 104 | $this->assertEquals('tax', $zone->getScope()); 105 | $this->assertEquals('1', $zone->getPriority()); 106 | $members = $zone->getMembers(); 107 | $this->assertCount(2, $members); 108 | 109 | $germanyMember = $members[0]; 110 | $this->assertInstanceOf('CommerceGuys\Zone\Model\ZoneMemberZone', $germanyMember); 111 | $this->assertEquals('2', $germanyMember->getId()); 112 | $this->assertEquals($zone, $germanyMember->getParentZone()); 113 | $this->assertEquals($zoneRepository->get('de'), $germanyMember->getZone()); 114 | 115 | $austriaMember = $members[1]; 116 | $this->assertInstanceOf('CommerceGuys\Zone\Model\ZoneMemberCountry', $austriaMember); 117 | $this->assertEquals('3', $austriaMember->getId()); 118 | $this->assertEquals('Austria', $austriaMember->getName()); 119 | $this->assertEquals($zone, $austriaMember->getParentZone()); 120 | $this->assertEquals('AT', $austriaMember->getCountryCode()); 121 | $this->assertEquals('6691, 6991:6993', $austriaMember->getIncludedPostalCodes()); 122 | $this->assertEquals('123456', $austriaMember->getExcludedPostalCodes()); 123 | $this->assertEquals('dummy', $austriaMember->getAdministrativeArea()); 124 | $this->assertEquals('dummy', $austriaMember->getLocality()); 125 | $this->assertEquals('dummy', $austriaMember->getDependentLocality()); 126 | 127 | // Test the static cache. 128 | $sameZone = $zoneRepository->get('de_vat'); 129 | $this->assertSame($zone, $sameZone); 130 | } 131 | 132 | /** 133 | * @covers ::get 134 | * @covers ::loadDefinition 135 | * @covers ::createZoneFromDefinition 136 | * @expectedException \CommerceGuys\Zone\Exception\UnknownZoneException 137 | * @depends testConstructor 138 | */ 139 | public function testGetNonExistingZone($zoneRepository) 140 | { 141 | $zone = $zoneRepository->get('rs'); 142 | } 143 | 144 | /** 145 | * @covers ::getAll 146 | * @covers ::loadDefinition 147 | * @covers ::createZoneFromDefinition 148 | * @covers ::createZoneMemberCountryFromDefinition 149 | * @covers ::createZoneMemberZoneFromDefinition 150 | * 151 | * @uses \CommerceGuys\Zone\Repository\ZoneRepository::get 152 | * @uses \CommerceGuys\Zone\Model\Zone 153 | * @uses \CommerceGuys\Zone\Model\ZoneMember 154 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry 155 | * @uses \CommerceGuys\Zone\Model\ZoneMemberZone 156 | * @uses \CommerceGuys\Addressing\PostalCodeHelper 157 | * @depends testConstructor 158 | */ 159 | public function testGetAll($zoneRepository) 160 | { 161 | $zones = $zoneRepository->getAll(); 162 | $this->assertCount(2, $zones); 163 | $this->assertArrayHasKey('de', $zones); 164 | $this->assertArrayHasKey('de_vat', $zones); 165 | $this->assertEquals($zones['de']->getId(), 'de'); 166 | $this->assertEquals($zones['de_vat']->getId(), 'de_vat'); 167 | 168 | $zones = $zoneRepository->getAll('tax'); 169 | $this->assertCount(1, $zones); 170 | $this->assertArrayHasKey('de_vat', $zones); 171 | $this->assertEquals($zones['de_vat']->getId(), 'de_vat'); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/Model/ZoneMemberCountryTest.php: -------------------------------------------------------------------------------- 1 | zoneMember = new ZoneMemberCountry(); 23 | } 24 | 25 | /** 26 | * @covers ::getCountryCode 27 | * @covers ::setCountryCode 28 | */ 29 | public function testCountryCode() 30 | { 31 | $this->zoneMember->setCountryCode('CN'); 32 | $this->assertEquals('CN', $this->zoneMember->getCountryCode()); 33 | } 34 | 35 | /** 36 | * @covers ::getAdministrativeArea 37 | * @covers ::setAdministrativeArea 38 | */ 39 | public function testAdministrativeArea() 40 | { 41 | $administrativeArea = $this 42 | ->getMockBuilder('CommerceGuys\Addressing\Subdivision\Subdivision') 43 | ->disableOriginalConstructor() 44 | ->getMock(); 45 | 46 | $this->zoneMember->setAdministrativeArea($administrativeArea); 47 | $this->assertSame($administrativeArea, $this->zoneMember->getAdministrativeArea()); 48 | } 49 | 50 | /** 51 | * @covers ::getLocality 52 | * @covers ::setLocality 53 | */ 54 | public function testLocality() 55 | { 56 | $locality = $this 57 | ->getMockBuilder('CommerceGuys\Addressing\Subdivision\Subdivision') 58 | ->disableOriginalConstructor() 59 | ->getMock(); 60 | 61 | $this->zoneMember->setLocality($locality); 62 | $this->assertSame($locality, $this->zoneMember->getLocality()); 63 | } 64 | 65 | /** 66 | * @covers ::getDependentLocality 67 | * @covers ::setDependentLocality 68 | */ 69 | public function testDependentLocality() 70 | { 71 | $dependentLocality = $this 72 | ->getMockBuilder('CommerceGuys\Addressing\Subdivision\Subdivision') 73 | ->disableOriginalConstructor() 74 | ->getMock(); 75 | 76 | $this->zoneMember->setDependentLocality($dependentLocality); 77 | $this->assertSame($dependentLocality, $this->zoneMember->getDependentLocality()); 78 | } 79 | 80 | /** 81 | * @covers ::getIncludedPostalCodes 82 | * @covers ::setIncludedPostalCodes 83 | */ 84 | public function testIncludedPostalCodes() 85 | { 86 | $this->zoneMember->setIncludedPostalCodes('123, 456, 789'); 87 | $this->assertEquals('123, 456, 789', $this->zoneMember->getIncludedPostalCodes()); 88 | } 89 | 90 | /** 91 | * @covers ::getExcludedPostalCodes 92 | * @covers ::setExcludedPostalCodes 93 | */ 94 | public function testExcludedPostalCodes() 95 | { 96 | $this->zoneMember->setExcludedPostalCodes('123, 456, 789'); 97 | $this->assertEquals('123, 456, 789', $this->zoneMember->getExcludedPostalCodes()); 98 | } 99 | 100 | /** 101 | * @covers ::match 102 | * 103 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry::setCountryCode 104 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry::setAdministrativeArea 105 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry::setLocality 106 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry::setDependentLocality 107 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry::setIncludedPostalCodes 108 | * @uses \CommerceGuys\Zone\Model\ZoneMemberCountry::setExcludedPostalCodes 109 | * @uses \CommerceGuys\Addressing\PostalCodeHelper 110 | * @dataProvider addressProvider 111 | */ 112 | public function testMatch($address, $expectedResult) 113 | { 114 | $this->zoneMember->setCountryCode('CN'); 115 | $this->zoneMember->setAdministrativeArea('Hebei Sheng'); 116 | $this->zoneMember->setLocality('Handan Shi'); 117 | $this->zoneMember->setDependentLocality('Ci Xian'); 118 | $this->zoneMember->setIncludedPostalCodes('123456'); 119 | 120 | $this->assertEquals($expectedResult, $this->zoneMember->match($address)); 121 | } 122 | 123 | /** 124 | * Provides addresses and the expected match results. 125 | */ 126 | public function addressProvider() 127 | { 128 | $emptyAddress = $this->getAddress(); 129 | $countryAddress = $this->getAddress('CN'); 130 | $administrativeAreaAddress = $this->getAddress('CN', 'Hebei Sheng'); 131 | $localityAddress = $this->getAddress('CN', 'Hebei Sheng', 'Handan Shi'); 132 | $dependentLocalityAddress = $this->getAddress('CN', 'Hebei Sheng', 'Handan Shi', 'Ci Xian'); 133 | $fullAddress = $this->getAddress('CN', 'Hebei Sheng', 'Handan Shi', 'Ci Xian', '123456'); 134 | 135 | return [ 136 | [$emptyAddress, false], 137 | [$countryAddress, false], 138 | [$administrativeAreaAddress, false], 139 | [$localityAddress, false], 140 | [$dependentLocalityAddress, false], 141 | [$fullAddress, true], 142 | ]; 143 | } 144 | 145 | /** 146 | * Returns a mock address. 147 | * 148 | * @param string $countryCode The country code. 149 | * @param string $administrativeArea The administrative area id. 150 | * @param string $locality The locality id. 151 | * @param string $dependentLocality The dependent locality id. 152 | * @param string $postalCode The postal code. 153 | * 154 | * @return \CommerceGuys\Addressing\Address 155 | */ 156 | protected function getAddress( 157 | $countryCode = null, 158 | $administrativeArea = null, 159 | $locality = null, 160 | $dependentLocality = null, 161 | $postalCode = null 162 | ) { 163 | $address = $this 164 | ->getMockBuilder('CommerceGuys\Addressing\Address') 165 | ->disableOriginalConstructor() 166 | ->getMock(); 167 | if ($countryCode) { 168 | $address 169 | ->expects($this->any()) 170 | ->method('getCountryCode') 171 | ->will($this->returnValue($countryCode)); 172 | } 173 | if ($administrativeArea) { 174 | $address 175 | ->expects($this->any()) 176 | ->method('getAdministrativeArea') 177 | ->will($this->returnValue($administrativeArea)); 178 | } 179 | if ($locality) { 180 | $address 181 | ->expects($this->any()) 182 | ->method('getLocality') 183 | ->will($this->returnValue($locality)); 184 | } 185 | if ($dependentLocality) { 186 | $address 187 | ->expects($this->any()) 188 | ->method('getDependentLocality') 189 | ->will($this->returnValue($dependentLocality)); 190 | } 191 | if ($postalCode) { 192 | $address 193 | ->expects($this->any()) 194 | ->method('getPostalCode') 195 | ->will($this->returnValue($postalCode)); 196 | } 197 | 198 | return $address; 199 | } 200 | } 201 | --------------------------------------------------------------------------------