diff --git a/.travis.yml b/.travis.yml index e125295..3723726 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 5.5 - 5.6 - 7.0 - 7.1 diff --git a/README.md b/README.md index 8a6818d..705f79c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The `http://www.controleerbtwnummer.nl/` API relies on the [VIES/EU](http://ec.e ## Installation -`composer install evertharmeling/vat-client` +`composer require evertharmeling/vat-client` ## Usage @@ -26,3 +26,7 @@ catch (TaxableObjectNotFoundException $e) { // VAT number not found } ``` + +## Roadmap + +- Formatter, add formatter who according to the regexes defined in the validator, formats the VAT number diff --git a/src/VIESApi/Exception/InvalidVATNumberException.php b/src/VIESApi/Exception/InvalidVATNumberException.php new file mode 100644 index 0000000..21bc814 --- /dev/null +++ b/src/VIESApi/Exception/InvalidVATNumberException.php @@ -0,0 +1,10 @@ + + */ +class InvalidVATNumberException extends \Exception implements VIESApiExceptionInterface +{ +} diff --git a/src/VIESApi/Model/Country.php b/src/VIESApi/Model/Country.php new file mode 100644 index 0000000..9444d2c --- /dev/null +++ b/src/VIESApi/Model/Country.php @@ -0,0 +1,38 @@ + + */ +class Country +{ + const CODE_AUSTRIA = 'AT'; + const CODE_BELGIUM = 'BE'; + const CODE_BULGARY = 'BG'; + const CODE_CYPRUS = 'CY'; + const CODE_CZECH_REPUBLIC = 'CZ'; + const CODE_GERMANY = 'DE'; + const CODE_DENMARK = 'DK'; + const CODE_ESTONIA = 'EE'; + const CODE_GREECE = 'EL'; + const CODE_SPAIN = 'ES'; + const CODE_FINLAND = 'FI'; + const CODE_FRANCE = 'FR'; + const CODE_GREAT_BRITAIN = 'GB'; + const CODE_CROATIA = 'HR'; + const CODE_HUNGARY = 'HU'; + const CODE_IRELAND = 'IE'; + const CODE_ITALY = 'IT'; + const CODE_LITHUANIA = 'LT'; + const CODE_LUXEMBOURG = 'LU'; + const CODE_LATVIA = 'LV'; + const CODE_MALTA = 'MT'; + const CODE_NETHERLANDS = 'NL'; + const CODE_POLAND = 'PL'; + const CODE_PORTUGAL = 'PT'; + const CODE_ROMANIA = 'RO'; + const CODE_SWEDEN = 'SE'; + const CODE_SLOVENIA = 'SL'; + const CODE_SLOVAKIA = 'SK'; +} diff --git a/src/VIESApi/Validator/VATNumberValidator.php b/src/VIESApi/Validator/VATNumberValidator.php new file mode 100644 index 0000000..9a0b9a3 --- /dev/null +++ b/src/VIESApi/Validator/VATNumberValidator.php @@ -0,0 +1,173 @@ + + */ +class VATNumberValidator +{ + /** + * @var Client + */ + private $client; + + /** + * @param Client $client + */ + public function __construct(Client $client) + { + $this->client = $client; + } + + public function validate($value) + { + self::checkFormat($value); + + try { + $this->client->getInfo($value); + + return true; + } catch (VIESApiExceptionInterface $e) { } + } + + /** + * Checks if the $value is the right format according to the country specifications + * + * @param string $value + * @return string + * @throws InvalidVATNumberException + */ + private static function checkFormat($value) + { + $countryCode = substr($value, 0, 2); + + if (isset(self::getFiscalNumberFormatsPerCountry()[$countryCode])) { + $regexes = self::getFiscalNumberFormatsPerCountry()[$countryCode]; + + foreach ($regexes as $regex) { + preg_match($regex, $value, $matches); + if (count($matches)) { + return true; + } + } + + throw new InvalidVATNumberException(sprintf("'%s' isn't a valid VATNumber according to the country (%s) specifications. It should match the regex: '%s'", $value, $countryCode, implode(', ', $regexes))); + } + + throw new InvalidVATNumberException(sprintf("'%s' isn't a valid VATNumber according to the country (%s) specifications", $value, $countryCode)); + } + + /** + * @link https://www.btw-nummer-controle.nl/Userfiles/images/Format%20btw-nummers%20EU(4).pdf + * + * @return array + */ + private static function getFiscalNumberFormatsPerCountry() + { + return [ + Country::CODE_AUSTRIA => [ + self::regexify('U\d{9}') + ], + Country::CODE_BELGIUM => [ + self::regexify('0\d{9}') + ], + Country::CODE_BULGARY => [ + self::regexify('\d{9,10}') + ], + Country::CODE_CYPRUS => [ + self::regexify('\d{8}[A-Z]{1}') + ], + Country::CODE_CZECH_REPUBLIC => [ + self::regexify('\d{8,10}') + ], + Country::CODE_GERMANY => [ + self::regexify('\d{9}') + ], + Country::CODE_DENMARK => [ + self::regexify('\d{2}\s{1}\d{2}\s{1}\d{2}\s{1}\d{2}') + ], + Country::CODE_ESTONIA => [ + self::regexify('\d{9}') + ], + Country::CODE_GREECE => [ + self::regexify('\d{9}') + ], + Country::CODE_SPAIN => [ + self::regexify('[0-9A-Z]{1}\d{7}[0-9A-Z]{1}') + ], + Country::CODE_FINLAND => [ + self::regexify('\d{8}') + ], + Country::CODE_FRANCE => [ + self::regexify('[0-9A-Z]{2}\s{1}\d{9}') + ], + Country::CODE_GREAT_BRITAIN => [ + self::regexify('\d{3}\s{1}\d{4}\s{1}\d{2}'), + self::regexify('\d{3}\s{1}\d{4}\s{1}\d{2}\s{1}\d{3}'), + self::regexify('GD|HA\d{3}') + ], + Country::CODE_CROATIA => [ + self::regexify('\d{11}') + ], + Country::CODE_HUNGARY => [ + self::regexify('\d{8}') + ], + Country::CODE_IRELAND => [ + self::regexify('\d{1}[0-9A-Z+*]{1}\d{5}[A-Z]{1}') + ], + Country::CODE_ITALY => [ + self::regexify('\d{11}') + ], + Country::CODE_LITHUANIA => [ + self::regexify('\d{9}'), + self::regexify('\d{12}') + ], + Country::CODE_LUXEMBOURG => [ + self::regexify('\d{8}') + ], + Country::CODE_LATVIA => [ + self::regexify('\d{11}') + ], + Country::CODE_MALTA => [ + self::regexify('\d{8}') + ], + Country::CODE_NETHERLANDS => [ + self::regexify('\d{9}B\d{2}') + ], + Country::CODE_POLAND => [ + self::regexify('\d{10}') + ], + Country::CODE_PORTUGAL => [ + self::regexify('\d{9}') + ], + Country::CODE_ROMANIA => [ + self::regexify('\d{2,10}') + ], + Country::CODE_SWEDEN => [ + self::regexify('\d{12}') + ], + Country::CODE_SLOVENIA => [ + self::regexify('\d{8}') + ], + Country::CODE_SLOVAKIA => [ + self::regexify('\d{10}') + ] + ]; + } + + /** + * @param string $value + * @return string + */ + private static function regexify($value) + { + // always add 2-letter country code to the regex + return sprintf('/^[A-Z]{2}%s$/', $value); + } +} diff --git a/tests/VIESApi/Client/ClientTest.php b/tests/VIESApi/Client/ClientTest.php index 7509267..229591b 100644 --- a/tests/VIESApi/Client/ClientTest.php +++ b/tests/VIESApi/Client/ClientTest.php @@ -13,7 +13,7 @@ class ClientTest extends \PHPUnit_Framework_TestCase { const VAT_NUMBER = 'NL818918172B01'; - const VAT_NUMBER_INVALID = 'NL818918172B02'; + const VAT_NUMBER_INVALID = 'NL818918172B99'; public function testGetInfo() { diff --git a/tests/VIESApi/Validator/VATNumberValidatorTest.php b/tests/VIESApi/Validator/VATNumberValidatorTest.php new file mode 100644 index 0000000..36dd61b --- /dev/null +++ b/tests/VIESApi/Validator/VATNumberValidatorTest.php @@ -0,0 +1,160 @@ + + */ +class VATNumberValidatorTest extends \PHPUnit_Framework_TestCase +{ + const VALID_AT_VAT_NUMBER = 'ATU123456789'; + const VALID_BE_VAT_NUMBER = 'BE0123456789'; + const VALID_BG_VAT_NUMBER = [ 'BG123456789', 'BG1234567890' ]; + const VALID_CY_VAT_NUMBER = 'CY12345678A'; + const VALID_CZ_VAT_NUMBER = [ 'CZ12345678', 'CZ123456789', 'CZ1234567890' ]; + const VALID_DE_VAT_NUMBER = 'DE123456789'; + const VALID_DK_VAT_NUMBER = 'DK12 34 56 78'; + const VALID_EE_VAT_NUMBER = 'EE123456789'; + const VALID_EL_VAT_NUMBER = 'EL123456789'; + const VALID_ES_VAT_NUMBER = [ 'ES123456789', 'ESA23456789', 'ESA2345678A', 'ES12345678A' ]; + const VALID_FI_VAT_NUMBER = 'FI12345678'; + const VALID_FR_VAT_NUMBER = [ 'FR12 123456789', 'FRA1 123456789', 'FR1A 123456789', 'FRAA 123456789' ]; + const VALID_GB_VAT_NUMBER = [ 'GB123 4567 89', 'GB123 4567 89 012', 'GBGD123', 'GBHA123' ]; + const VALID_HR_VAT_NUMBER = 'HR12345678901'; + const VALID_HU_VAT_NUMBER = 'HU12345678'; + const VALID_IE_VAT_NUMBER = [ 'IE1234567A', 'IE1A34567A', 'IE1+34567A', 'IE1*34567A' ]; + const VALID_IT_VAT_NUMBER = 'IT12345678901'; + const VALID_LT_VAT_NUMBER = [ 'LT123456789', 'LT123456789012' ]; + const VALID_LU_VAT_NUMBER = 'LU12345678'; + const VALID_LV_VAT_NUMBER = 'LV12345678901'; + const VALID_MT_VAT_NUMBER = 'MT12345678'; + const VALID_NL_VAT_NUMBER = 'NL123456789B01'; + const VALID_PL_VAT_NUMBER = 'PL1234567890'; + const VALID_PT_VAT_NUMBER = 'PT123456789'; + const VALID_RO_VAT_NUMBER = [ 'RO12', 'RO123', 'RO1234', 'RO12345', 'RO123456', 'RO1234567', 'RO12345678', 'RO123456789', 'RO1234567890' ]; + const VALID_SE_VAT_NUMBER = 'SE123456789012'; + const VALID_SL_VAT_NUMBER = 'SL12345678'; + const VALID_SK_VAT_NUMBER = 'SK1234567890'; + + + /** + * @dataProvider validVATNumberProvider + */ + public function testValidVATNumbers($values) + { + if (!is_array($values)) { + $values = [ $values ]; + } + + foreach ($values as $value) { + static::assertTrue($this->getValidator()->validate($value)); + } + } + + /** + * @dataProvider invalidVATNumberProvider + * @expectedException \VIESApi\Exception\InvalidVATNumberException + */ + public function testInvalidVATNumbers($values) + { + if (!is_array($values)) { + $values = [ $values ]; + } + + foreach ($values as $value) { + $this->getValidator()->validate($value); + } + } + + public function validVATNumberProvider() + { + return [ + Country::CODE_AUSTRIA => [ self::VALID_AT_VAT_NUMBER ], + Country::CODE_BELGIUM => [ self::VALID_BE_VAT_NUMBER ], + Country::CODE_BULGARY => [ self::VALID_BG_VAT_NUMBER ], + Country::CODE_CZECH_REPUBLIC => [ self::VALID_CZ_VAT_NUMBER ], + Country::CODE_GERMANY => [ self::VALID_DE_VAT_NUMBER ], + Country::CODE_DENMARK => [ self::VALID_DK_VAT_NUMBER ], + Country::CODE_ESTONIA => [ self::VALID_EE_VAT_NUMBER ], + Country::CODE_GREECE => [ self::VALID_EL_VAT_NUMBER ], + Country::CODE_SPAIN => [ self::VALID_ES_VAT_NUMBER ], + Country::CODE_FINLAND => [ self::VALID_FI_VAT_NUMBER ], + Country::CODE_FRANCE => [ self::VALID_FR_VAT_NUMBER ], + Country::CODE_GREAT_BRITAIN => [ self::VALID_GB_VAT_NUMBER ], + Country::CODE_CROATIA => [ self::VALID_HR_VAT_NUMBER ], + Country::CODE_HUNGARY => [ self::VALID_HU_VAT_NUMBER ], + Country::CODE_IRELAND => [ self::VALID_IE_VAT_NUMBER ], + Country::CODE_ITALY => [ self::VALID_IT_VAT_NUMBER ], + Country::CODE_LITHUANIA => [ self::VALID_LT_VAT_NUMBER ], + Country::CODE_LUXEMBOURG => [ self::VALID_LU_VAT_NUMBER ], + Country::CODE_LATVIA => [ self::VALID_LV_VAT_NUMBER ], + Country::CODE_MALTA => [ self::VALID_MT_VAT_NUMBER ], + Country::CODE_NETHERLANDS => [ self::VALID_NL_VAT_NUMBER ], + Country::CODE_POLAND => [ self::VALID_PL_VAT_NUMBER ], + Country::CODE_PORTUGAL => [ self::VALID_PT_VAT_NUMBER ], + Country::CODE_ROMANIA => [ self::VALID_RO_VAT_NUMBER ], + Country::CODE_SWEDEN => [ self::VALID_SE_VAT_NUMBER ], + Country::CODE_SLOVENIA => [ self::VALID_SL_VAT_NUMBER ], + Country::CODE_SLOVAKIA => [ self::VALID_SK_VAT_NUMBER ] + ]; + } + + public function invalidVATNumberProvider() + { + return [ + Country::CODE_AUSTRIA => [ [ 'ATU12345678', 'ATU1234567890' ] ], + Country::CODE_BELGIUM => [ [ 'BE012345678', 'BE01234567890' ] ], + Country::CODE_BULGARY => [ [ 'BG12345678', 'BG12345678901' ] ], + Country::CODE_CYPRUS => [ [ 'CY12345678', 'CY123456789' ] ], + Country::CODE_CZECH_REPUBLIC => [ [ 'CZ1234567', 'CZ12345678901' ] ], + Country::CODE_GERMANY => [ [ 'DE12345678', 'DE1234567890' ] ], + Country::CODE_DENMARK => [ [ 'DK12345678', 'DK12 34 56 78 9', 'DK12 34 56 78 90' ] ], + Country::CODE_ESTONIA => [ [ 'EE12345678', 'EE1234567890' ] ], + Country::CODE_GREECE => [ [ 'EL12345678', 'EL1234567890' ] ], + Country::CODE_SPAIN => [ [ 'ES12345678', 'ESA234567890' ] ], + Country::CODE_FINLAND => [ [ 'FI1234567', 'FI123456789' ] ], + Country::CODE_FRANCE => [ [ 'FR12123456789', 'FRA1 1234567890' ] ], + Country::CODE_GREAT_BRITAIN => [ [ 'GB123456789', 'GB123456789012', 'GBGD12', 'GBAA123', 'GBAA1234' ] ], + Country::CODE_CROATIA => [ [ 'HR1234567890', 'HR123456789012' ] ], + Country::CODE_HUNGARY => [ [ 'HU1234567', 'HU123456789' ] ], + Country::CODE_IRELAND => [ [ 'IE1234567', 'IE1234567AA', 'IE1_34567A', 'IE1*345678' ] ], + Country::CODE_ITALY => [ [ 'IT1234567890', 'IT123456789012' ] ], + Country::CODE_LITHUANIA => [ [ 'LT12345678', 'LT1234567890', 'LT12345678901' ] ], + Country::CODE_LUXEMBOURG => [ [ 'LU1234567', 'LU123456789' ] ], + Country::CODE_LATVIA => [ [ 'LV1234567890', 'LV123456789012' ] ], + Country::CODE_MALTA => [ [ 'MT1234567', 'MT123456789' ] ], + Country::CODE_NETHERLANDS => [ [ 'NL12345678B01', 'NL123456789B012', 'NL123456789A01', 'NL1234567890B01' ] ], + Country::CODE_POLAND => [ [ 'PL123456789', 'PL12345678901' ] ], + Country::CODE_PORTUGAL => [ [ 'PT12345678', 'PT1234567890' ] ], + Country::CODE_ROMANIA => [ [ 'RO1', 'RO12345678901' ] ], + Country::CODE_SWEDEN => [ [ 'SE12345678901', 'SE1234567890123' ] ], + Country::CODE_SLOVENIA => [ [ 'SL1234567', 'SL123456789' ] ], + Country::CODE_SLOVAKIA => [ [ 'SK123456789', 'SK12345678901' ] ], + ]; + } + + /** + * @return VATNumberValidator + */ + private function getValidator() + { + return new VATNumberValidator($this->createClientMock()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|Client + */ + private function createClientMock() + { + return $this->getMockBuilder(Client::class) + ->disableOriginalConstructor() + ->getMock(); + } +}