diff --git a/README.md b/README.md index e076429..e2dda36 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# EU VAT Number Validator +# EU VAT and TIN Number Validator -A simple and clean PHP class that validates EU VAT numbers against the central ec.europa.eu database (using the official europa API). +A simple and clean PHP class that validates EU VAT and TIN numbers against the central ec.europa.eu database (using the official europa API). ![EU VATIN validator; EU Flag](eu-flag.svg) ## The Problem -Validate VAT numbers might be difficult and if you use a validation pattern to check if the format is valid, you are never sure if the VAT registration number is still valid. +Validate VAT and TIN numbers might be difficult and if you use a validation pattern to check if the format is valid, you are never sure if the VAT registration number is still valid. ## The Solution -This [PHP VAT validator library](https://github.com/pH-7/eu-vat-validator) uses real-time data feeds from individual EU member states' VAT systems so you are sure of the validity of the number and avoid fraud with expired or wrong VAT numbers. +This [PHP VAT validator library](https://github.com/pH-7/eu-vat-validator) uses real-time data feeds from individual EU member states' VAT and TIN systems so you are sure of the validity of the number and avoid fraud with expired or wrong VAT numbers. For example, this kind of validation can be very useful on online payment forms. @@ -50,9 +50,9 @@ require 'src/autoloader.php'; ```php use PH7\Eu\Vat\Validator; -use PH7\Eu\Vat\Provider\Europa; +use PH7\Eu\Vat\Provider\EuropaVAT; -$oVatValidator = new Validator(new Europa, '0472429986', 'BE'); +$oVatValidator = new Validator(new EuropaVAT, '0472429986', 'BE'); if ($oVatValidator->check()) { $sRequestDate = $oVatValidator->getRequestDate(); @@ -72,8 +72,19 @@ if ($oVatValidator->check()) { ## Optimization (Suggestion) -Depending of the use of this library, it could be handy to cache the result specifically for each specified VAT number. +Depending of the use of this library, it could be handy to cache the result specifically for each specified VAT or TIN number. +## Strict mode + +By default this librery clean VAT or TIN numbers before checking them (it cleans by deleting from VAT or TIN numbers: country Code and these special characters: '-', '_', '.', ',', ' '). + +If you don't want, you can check numbers in strict mode, just by calling check function with option value TRUE (default value is FALSE). In the above example, you only need to change this line: +```php +if ($oVatValidator->check(true)) { +``` +```php +if ($oVatValidator->check(strict: true)) { +``` ## Requirements diff --git a/example.php b/example.php index b745e49..81aa14e 100644 --- a/example.php +++ b/example.php @@ -1,24 +1,26 @@ - * @copyright (c) 2017-2023, Pierre-Henry Soria. All Rights Reserved. + * @author Pierre-Henry Soria + * @copyright (c) 2017-2019, Pierre-Henry Soria. All Rights Reserved. * @license GNU General Public License; */ require 'src/autoloader.php'; -use PH7\Eu\Vat\Provider\Europa; +use PH7\Eu\Vat\Provider\EuropaVAT; use PH7\Eu\Vat\Validator; -$sEuVatNumber = '0472429986'; // EU VAT number -$sEuCountryCode = 'BE'; // EU two-letter country code +use PH7\Eu\Tin\Provider\EuropaTIN; +use PH7\Eu\Tin\ValidatorTIN; -$oVatValidator = new Validator(new Europa, $sEuVatNumber, $sEuCountryCode); +$oVatValidator = new Validator(new EuropaVAT, '0472429986', 'BE'); + +echo $oVatValidator->all() . '
'; +echo 'Check: ' . ($oVatValidator->check() ? 'true' : 'false') . '
'; if ($oVatValidator->check()) { $sRequestDate = $oVatValidator->getRequestDate(); - - // Optional - explicitly format the date to d-m-Y format + // Optional, format the date $sFormattedRequestDate = (new DateTime)->format('d-m-Y'); echo 'Business Name: ' . $oVatValidator->getName() . '
'; @@ -27,5 +29,83 @@ echo 'Member State: ' . $oVatValidator->getCountryCode() . '
'; echo 'VAT Number: ' . $oVatValidator->getVatNumber() . '
'; } else { - echo 'Invalid VAT number'; + echo 'Invalid VAT number' . '
'; } + +echo '
'; + +$oVatValidatorInvalid = new Validator(new EuropaVAT, '047242998', 'BE'); + +echo $oVatValidatorInvalid->all() . '
'; +echo 'Check: ' . ($oVatValidatorInvalid->check() ? 'true' : 'false') . '
'; + +if ($oVatValidatorInvalid->check()) { + $sRequestDate = $oVatValidatorInvalid->getRequestDate(); + // Optional, format the date + $sFormattedRequestDate = (new DateTime)->format('d-m-Y'); + + echo 'Business Name: ' . $oVatValidatorInvalid->getName() . '
'; + echo 'Address: ' . $oVatValidatorInvalid->getAddress() . '
'; + echo 'Request Date: ' . $sFormattedRequestDate . '
'; + echo 'Member State: ' . $oVatValidatorInvalid->getCountryCode() . '
'; + echo 'VAT Number: ' . $oVatValidatorInvalid->getVatNumber() . '
'; +} else { + echo 'Invalid VAT number' . '
'; +} + +echo '
'; + +$oTinValidator = new ValidatorTIN(new EuropaTIN, '78888888S', 'ES'); + +echo $oTinValidator->all() . '
'; + +if ($oTinValidator->check()) { + $sRequestDate = $oTinValidator->getRequestDate(); + // Optional, format the date + $sFormattedRequestDate = (new DateTime)->format('d-m-Y'); + echo 'Request Date: ' . $sFormattedRequestDate . '
'; + echo 'TIN Number: ' . $oTinValidator->getTinNumber() . '
'; + +} else { + echo 'Invalid TIN number' . '
'; + echo 'Structure: ' . ($oTinValidator->checkStructure() ? 'true' : 'false'). '
'; + echo 'Syntax: ' . ($oTinValidator->checkSyntax() ? 'true' : 'false') . '
'; +} + +echo '
'; + +$oTinValidatorInvalid = new ValidatorTIN(new EuropaTIN, '78888888R', 'ES'); + +echo $oTinValidatorInvalid->all() . '
'; + +if ($oTinValidatorInvalid->check()) { + $sRequestDate = $oTinValidatorInvalid->getRequestDate(); + // Optional, format the date + $sFormattedRequestDate = (new DateTime)->format('d-m-Y'); + echo 'Request Date: ' . $sFormattedRequestDate . '
'; + echo 'TIN Number: ' . $oTinValidatorInvalid->getTinNumber() . '
'; + +} else { + echo 'Invalid TIN number' . '
'; + echo 'Structure: ' . ($oTinValidatorInvalid->checkStructure() ? 'true' : 'false') . '
'; + echo 'Syntax: ' . ($oTinValidatorInvalid->checkSyntax() ? 'true' : 'false') . '
'; +} + +echo '
'; + +$oTinValidatorInvalid2 = new ValidatorTIN(new EuropaTIN, '7888S8888', 'ES'); + +echo $oTinValidatorInvalid2->all() . '
'; + +if ($oTinValidatorInvalid2->check()) { + $sRequestDate = $oTinValidatorInvalid2->getRequestDate(); + // Optional, format the date + $sFormattedRequestDate = (new DateTime)->format('d-m-Y'); + echo 'Request Date: ' . $sFormattedRequestDate . '
'; + echo 'TIN Number: ' . $oTinValidatorInvalid2->getTinNumber() . '
'; + +} else { + echo 'Invalid TIN number' . '
'; + echo 'Structure: ' . ($oTinValidatorInvalid2->checkStructure() ? 'true' : 'false') . '
'; + echo 'Syntax: ' . ($oTinValidatorInvalid2->checkSyntax() ? 'true' : 'false') . '
'; +} \ No newline at end of file diff --git a/src/Tin/Exception.php b/src/Tin/Exception.php new file mode 100644 index 0000000..abb65c7 --- /dev/null +++ b/src/Tin/Exception.php @@ -0,0 +1,13 @@ + + * @copyright (c) 2017-2019, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +namespace PH7\Eu\Tin; + +class Exception extends \Exception +{ + +} diff --git a/src/Tin/Provider/EuropaTIN.php b/src/Tin/Provider/EuropaTIN.php new file mode 100644 index 0000000..06a4a15 --- /dev/null +++ b/src/Tin/Provider/EuropaTIN.php @@ -0,0 +1,83 @@ + + * @copyright (c) 2017-2022, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +declare(strict_types=1); + +namespace PH7\Eu\Tin\Provider; + +use PH7\Eu\Tin\Exception; +use SoapClient; +use SoapFault; +use stdClass; + +class EuropaTIN implements Providable +{ + protected const TIN_EU_COUNTRY_LIST = ['AT','BE','BG','CY','CZ','DE','DK','EE','EL','ES','FI','FR','HR','HU','IE','IT','LU','LV','LT','MT','NL','PL','PT','RO','SE','SI','SK']; + public const COUNTRY_NOT_VALID = 'Country not valid in Europa TIN Service: %s'; + + public const EU_TIN_API = 'https://ec.europa.eu'; + public const EU_TIN_WSDL_ENDPOINT = '/taxation_customs/tin/services/checkTinService.wsdl'; + + private const IMPOSSIBLE_CONNECT_API_MESSAGE = 'Impossible to connect to the Europa TIN SOAP: %s'; + private const IMPOSSIBLE_RETRIEVE_DATA_MESSAGE = 'Impossible to retrieve the TIN details: %s'; + + /** @var SoapClient */ + private $oClient; + + /** + * EuropaTIN Provider constructor + * + * @throws Exception + */ + public function __construct() + { + try { + $this->oClient = new SoapClient($this->getApiUrl()); + } catch (SoapFault $oExcept) { + throw new Exception( + sprintf(self::IMPOSSIBLE_CONNECT_API_MESSAGE, $oExcept->faultstring), + 0, + $oExcept + ); + } + } + + public function getApiUrl(): string + { + return static::EU_TIN_API . static::EU_TIN_WSDL_ENDPOINT; + } + + /** + * Send the TIN number and country code to europa.eu API and get the data. + * + * @param int|string $sTinNumber The TIN number + * @param string $sCountryCode The country code + * + * @return stdClass The TIN number's details. + * + * @throws Exception + */ + public function getResource($sTinNumber, string $sCountryCode): stdClass + { + if (!in_array(strtoupper($sCountryCode), self::TIN_EU_COUNTRY_LIST)) { + throw new Exception( + sprintf(self::COUNTRY_NOT_VALID, strtoupper($sCountryCode)) + ); + } + try { + $aDetails = [ + 'countryCode' => strtoupper($sCountryCode), + 'tinNumber' => $sTinNumber + ]; + return $this->oClient->checkTin($aDetails); + } catch (SoapFault $oExcept) { + throw new Exception( + sprintf(self::IMPOSSIBLE_RETRIEVE_DATA_MESSAGE, $oExcept->faultstring) + ); + } + } +} diff --git a/src/Tin/Provider/Providable.php b/src/Tin/Provider/Providable.php new file mode 100644 index 0000000..86766a6 --- /dev/null +++ b/src/Tin/Provider/Providable.php @@ -0,0 +1,19 @@ + + * @copyright (c) 2017-2019, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +declare(strict_types=1); + +namespace PH7\Eu\Tin\Provider; + +use stdClass; + +interface Providable +{ + public function getApiUrl(): string; + + public function getResource($sTinNumber, string $sCountryCode): stdClass; +} diff --git a/src/Tin/Validatable.php b/src/Tin/Validatable.php new file mode 100644 index 0000000..e30bbc9 --- /dev/null +++ b/src/Tin/Validatable.php @@ -0,0 +1,15 @@ + + * @copyright (c) 2017-2019, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +namespace PH7\Eu\Tin; + +interface Validatable +{ + public function check(): bool; + + public function sanitize(bool $strict): void +} diff --git a/src/Tin/ValidatorTIN.php b/src/Tin/ValidatorTIN.php new file mode 100644 index 0000000..117fe5e --- /dev/null +++ b/src/Tin/ValidatorTIN.php @@ -0,0 +1,91 @@ + + * @copyright (c) 2017-2019, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +declare(strict_types=1); + +namespace PH7\Eu\Tin; + +use PH7\Eu\Tin\Provider\Providable; +use stdClass; + +class ValidatorTIN implements Validatable +{ + /** @var int|string */ + private $sTinNumber; + + /** @var string */ + private $sCountryCode; + + /** @var stdClass */ + private $oResponse; + + /** + * @param Providable $oProvider The API that checks the TIN no. + * @param int|string $sTinNumber The TIN number. + * @param string $sCountryCode The country code. + * @param boolean $strict Strict sanitize version, not change anything in TIN number + */ + public function __construct(Providable $oProvider, $sTinNumber, string $sCountryCode, bool $strict = false) + { + $this->sTinNumber = $sTinNumber; + $this->sCountryCode = $sCountryCode; + + $this->sanitize($strict); + $this->oResponse = $oProvider->getResource($this->sTinNumber, $this->sCountryCode); + } + + + public function all(): string + { + return json_encode($this->oResponse); + } + + /** + * Check if the TIN number is valid or not + * + * @return bool + */ + public function check(): bool + { + return ($this->checkStructure() and $this->checkSyntax()); + } + + public function checkStructure(): bool + { + return (bool)$this->oResponse->validStructure; + } + + public function checkSyntax(): bool + { + return (bool)$this->oResponse->validSyntax; + } + + public function getRequestDate(): string + { + return $this->oResponse->requestDate ?? ''; + } + + public function getCountryCode(): string + { + return $this->oResponse->countryCode ?? ''; + } + + public function getTinNumber(): string + { + return $this->oResponse->tinNumber ?? ''; + } + + public function sanitize(bool $strict): void + { + if (!$strict) { + $aSearch = [$this->sCountryCode, '-', '_', '.', ',', ' ']; + $this->sTinNumber = trim(str_replace($aSearch, '', $this->sTinNumber)); + } + $this->sCountryCode = strtoupper($this->sCountryCode); + } + +} diff --git a/src/Vat/Provider/EuropaVAT.php b/src/Vat/Provider/EuropaVAT.php new file mode 100644 index 0000000..3dadbfc --- /dev/null +++ b/src/Vat/Provider/EuropaVAT.php @@ -0,0 +1,83 @@ + + * @copyright (c) 2017-2022, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +declare(strict_types=1); + +namespace PH7\Eu\Vat\Provider; + +use PH7\Eu\Vat\Exception; +use SoapClient; +use SoapFault; +use stdClass; + +class EuropaVAT implements Providable +{ + protected const VAT_EU_COUNTRY_LIST = ['AT','BE','BG','CY','CZ','DE','DK','EE','EL','ES','FI','FR','HR','HU','IE','IT','LU','LV','LT','MT','NL','PL','PT','RO','SE','SI','SK','XI']; + private const COUNTRY_NOT_VALID = 'Country not valid in Europa VAT Service: %s'; + + public const EU_VAT_API = 'https://ec.europa.eu'; + public const EU_VAT_WSDL_ENDPOINT = '/taxation_customs/vies/checkVatService.wsdl'; + + private const IMPOSSIBLE_CONNECT_API_MESSAGE = 'Impossible to connect to the Europa VAT SOAP: %s'; + private const IMPOSSIBLE_RETRIEVE_DATA_MESSAGE = 'Impossible to retrieve the VAT details: %s'; + + /** @var SoapClient */ + private $oClient; + + /** + * EuropaVAT Provider constructor + * + * @throws Exception + */ + public function __construct() + { + try { + $this->oClient = new SoapClient($this->getApiUrl()); + } catch (SoapFault $oExcept) { + throw new Exception( + sprintf(self::IMPOSSIBLE_CONNECT_API_MESSAGE, $oExcept->faultstring), + 0, + $oExcept + ); + } + } + + public function getApiUrl(): string + { + return static::EU_VAT_API . static::EU_VAT_WSDL_ENDPOINT; + } + + /** + * Send the VAT number and country code to europa.eu API and get the data. + * + * @param int|string $sVatNumber The VAT number + * @param string $sCountryCode The country code + * + * @return stdClass The VAT number's details. + * + * @throws Exception + */ + public function getResource($sVatNumber, string $sCountryCode): stdClass + { + if (!in_array(strtoupper($sCountryCode), self::VAT_EU_COUNTRY_LIST)) { + throw new Exception( + sprintf(self::COUNTRY_NOT_VALID, strtoupper($sCountryCode)) + ); + } + try { + $aDetails = [ + 'countryCode' => strtoupper($sCountryCode), + 'vatNumber' => $sVatNumber + ]; + return $this->oClient->checkVat($aDetails); + } catch (SoapFault $oExcept) { + throw new Exception( + sprintf(self::IMPOSSIBLE_RETRIEVE_DATA_MESSAGE, $oExcept->faultstring) + ); + } + } +} diff --git a/src/Vat/Validator.php b/src/Vat/Validator.php index 8e94faf..e1cfffd 100644 --- a/src/Vat/Validator.php +++ b/src/Vat/Validator.php @@ -27,16 +27,22 @@ class Validator implements Validatable * @param Providable $oProvider The API that checks the VAT no. and retrieve the VAT registration's details. * @param int|string $sVatNumber The VAT number. * @param string $sCountryCode The country code. + * @param boolean $strict Strict sanitize version, not change anything in VAT number */ - public function __construct(Providable $oProvider, $sVatNumber, string $sCountryCode) + public function __construct(Providable $oProvider, $sVatNumber, string $sCountryCode, bool $strict = false) { $this->sVatNumber = $sVatNumber; $this->sCountryCode = $sCountryCode; - $this->sanitize(); + $this->sanitize($strict); $this->oResponse = $oProvider->getResource($this->sVatNumber, $this->sCountryCode); } + public function all(): string + { + return json_encode($this->oResponse); + } + /** * Check if the VAT number is valid or not * @@ -72,10 +78,12 @@ public function getVatNumber(): string return $this->oResponse->vatNumber ?? ''; } - public function sanitize(): void + public function sanitize(bool $strict): void { - $aSearch = [$this->sCountryCode, '-', '_', '.', ',', ' ']; - $this->sVatNumber = trim(str_replace($aSearch, '', $this->sVatNumber)); + if (!$strict) { + $aSearch = [$this->sCountryCode, '-', '_', '.', ',', ' ']; + $this->sVatNumber = trim(str_replace($aSearch, '', $this->sVatNumber)); + } $this->sCountryCode = strtoupper($this->sCountryCode); } diff --git a/tests/Tin/Provider/EuropaTINTest.php b/tests/Tin/Provider/EuropaTINTest.php new file mode 100644 index 0000000..1293971 --- /dev/null +++ b/tests/Tin/Provider/EuropaTINTest.php @@ -0,0 +1,29 @@ + + * @copyright (c) 2017-2021, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +declare(strict_types=1); + +namespace PH7\Eu\Tests\Tin\Provider; + +use PH7\Eu\Tin\Provider\EuropaTIN; +use PHPUnit\Framework\TestCase; + +class ProviderTestTIN extends TestCase +{ + /** @var Europa */ + private $oEuropa; + + protected function setUp(): void + { + $this->oEuropa = new EuropaTIN; + } + + public function testApiUrl(): void + { + $this->assertEquals(EuropaTIN::EU_TIN_API_URL . EuropaTIN::EU_TIN_WSDL_ENDPOINT, $this->oEuropa->getApiUrl()); + } +} diff --git a/tests/Tin/ValidatorTestTin.php b/tests/Tin/ValidatorTestTin.php new file mode 100644 index 0000000..be4c58d --- /dev/null +++ b/tests/Tin/ValidatorTestTin.php @@ -0,0 +1,165 @@ + + * @copyright (c) 2017-2021, Pierre-Henry Soria. All Rights Reserved. + * @license GNU General Public License; + */ + +declare(strict_types=1); + +namespace PH7\Eu\Tests\Tin; + +use PH7\Eu\Tin\Exception; +use PH7\Eu\Tin\Provider\EuropaTin; +use PH7\Eu\Tin\Provider\Providable; +use PH7\Eu\Tin\ValidatorTin; +use Phake; +use Phake_IMock; +use PHPUnit\Framework\TestCase; +use stdClass; + +class ValidatorTestTin extends TestCase +{ + /** + * @dataProvider validTinNumbers + * + * @param int|string $sTinNumber The TIN number + * @param string $sCountryCode The country code + */ + public function testValidTinNumbers($sTinNumber, string $sCountryCode): void + { + try { + $oTinValidator = new ValidatorTIN(new EuropaTIN, $sTinNumber, $sCountryCode); + $this->assertTrue($oTinValidator->check()); + } catch (Exception $oExcept) { + $this->assertIsResponseFailure($oExcept); + } + } + + /** + * @dataProvider invalidTinNumbers + * + * @param int|string $sTinNumber The TIN number + * @param string $sCountryCode The country code + */ + public function testInvalidTinNumbers($sTinNumber, string $sCountryCode): void + { + try { + $oTinValidator = new ValidatorTIN(new EuropaTIN, $sTinNumber, $sCountryCode); + $this->assertFalse($oTinValidator->check()); + } catch (Exception $oExcept) { + $this->assertIsResponseFailure($oExcept); + } + } + + /** + * @dataProvider validTinNumberDetails + */ + public function testValidTinNumberStatus(stdClass $oTinDetails): void + { + $oValidator = $this->setUpAndMock($oTinDetails); + $this->assertTrue($oValidator->check()); + } + + /** + * @dataProvider invalidTinNumberDetails + */ + public function testInvalidTinNumberStatus(stdClass $oTinDetails): void + { + $oValidator = $this->setUpAndMock($oTinDetails); + $this->assertFalse($oValidator->check()); + } + + /** + * @dataProvider validTinNumberDetails + */ + public function testCountryCode(stdClass $oTinDetails): void + { + $oValidator = $this->setUpAndMock($oTinDetails); + $this->assertEquals('BE', $oValidator->getCountryCode()); + } + + /** + * @dataProvider validTinNumberDetails + */ + public function testTinNumber(stdClass $oTinDetails): void + { + $oValidator = $this->setUpAndMock($oTinDetails); + $this->assertEquals('0472429986', $oValidator->getTinNumber()); + } + + /** + * @dataProvider validTinNumberDetails + */ + public function testRequestDate(stdClass $oTinDetails): void + { + $oValidator = $this->setUpAndMock($oTinDetails); + $this->assertEquals('2017-01-22+01:00', $oValidator->getRequestDate()); + } + + public function testResource(): void + { + try { + $oEuropaTINProvider = new EuropaTIN; + $this->assertInstanceOf(stdClass::class, $oEuropaTINProvider->getResource('0472429986', 'BE')); + } catch (Exception $oExcept) { + $this->assertIsResponseFailure($oExcept); + } + } + + public function validTinNumberDetails(): array + { + $oData = new stdClass; + $oData->valid = true; + $oData->countryCode = 'BE'; + $oData->TinNumber = '0472429986'; + $oData->requestDate = '2017-01-22+01:00'; + return [ + [$oData] + ]; + } + + public function invalidTinNumberDetails(): array + { + $oData = new stdClass; + $oData->valid = false; + + return [ + [$oData] + ]; + } + + public function validTinNumbers(): array + { + return [ + ['78888888S', 'ES'], + ['9763375H', 'IE'], + ['RSSMRA85T10A562S', 'IT'] + ]; + } + + public function invalidTinNumbers(): array + { + return [ + [243852752, 'UK'], // Has to be 'GB' + [29672050085, 'FRANCE'], + ['blablabla', 'DE'] + ]; + } + + private function setUpAndMock(stdClass $oTinDetails): Phake_IMock + { + $oProvider = Phake::mock(Providable::class); + Phake::when($oProvider)->getResource(Phake::anyParameters())->thenReturn($oTinDetails); + $oValidator = Phake::partialMock(ValidatorTIN::class, $oProvider, '78888888S', 'ES'); + Phake::verify($oValidator)->sanitize(); + Phake::verify($oProvider)->getResource('78888888S', 'ES'); + + return $oValidator; + } + + private function assertIsResponseFailure(Exception $oExcept): void + { + $this->assertRegexp('/^Impossible to retrieve the TIN details/', $oExcept->getMessage()); + } +}