Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Require the package via Composer:

The Validation library is built to work with the Laravel Framework (>=10). It
comes with a service provider, which will be discovered automatically and
registers the validation rules into your installation. The package provides 37
registers the validation rules into your installation. The package provides 38
additional validation rules including multi language error messages, which can
be used like Laravel's own validation rules.

Expand Down Expand Up @@ -112,6 +112,18 @@ Checks for a valid [European Article Number](https://en.wikipedia.org/wiki/Inter

Optional integer length (8 or 13) to check only for EAN-8 or EAN-13.

### European VAT Number

Checks for a valid [European VAT identification number](https://en.wikipedia.org/wiki/VAT_identification_number).

public Intervention\Validation\Rules\EuropeanVatNumber::__construct(bool $withApi = false)

#### Parameters

**withApi**

Set to `true` to validate the VAT number against the official [VIES API](http://ec.europa.eu/taxation_customs/vies/). Defaults to `false` for format validation only.

### Global Release Identifier (GRid)

The field under validation must be a [Global Release Identifier](https://en.wikipedia.org/wiki/Global_Release_Identifier).
Expand Down
163 changes: 163 additions & 0 deletions src/Rules/EuropeanVatNumber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);

namespace Intervention\Validation\Rules;

use Intervention\Validation\AbstractRule;

class EuropeanVatNumber extends AbstractRule
{
/**
* @link http://ec.europa.eu/taxation_customs/vies/faq.html?locale=en#item_11
*
* @var array<string, string>
*/
private array $patternExpressions = [
'AT' => 'U[A-Z\d]{8}',
'BE' => '(0\d{9}|\d{10})',
'BG' => '\d{9,10}',
'CH' => '^\d{6}$|^[E]\d{9}\s?(TVA|MWST|IVA)$',
'CY' => '\d{8}[A-Z]',
'CZ' => '\d{8,10}',
'DE' => '\d{9}',
'DK' => '(\d{2} ?){3}\d{2}',
'EE' => '\d{9}',
'EL' => '\d{9}',
'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}',
'FI' => '\d{8}',
'FR' => '([A-Z]{2}|\d{2})\d{9}',
'GB' => '\d{9}|\d{12}|(GD|HA)\d{3}',
'HR' => '\d{11}',
'HU' => '\d{8}',
'IE' => '[A-Z\d]{8}|[A-Z\d]{9}',
'IT' => '\d{11}',
'LT' => '(\d{9}|\d{12})',
'LU' => '\d{8}',
'LV' => '\d{11}',
'MT' => '\d{8}',
'NL' => '\d{9}B\d{2}',
'PL' => '\d{10}',
'PT' => '\d{9}',
'RO' => '\d{2,10}',
'SE' => '\d{12}',
'SI' => '\d{8}',
'SK' => '\d{10}',
];

public function __construct(private readonly bool $withApi = false)
{
}

/**
* @throws \SoapFault
*/
public function isValid(mixed $value): bool
{
$vatNumber = $this->vatCleaner((string) $value);
[$country, $number] = $this->splitVat($vatNumber);


$isValidFormat = $this->isValidFormat($country, $number);

if (!$this->withApi) {
return $isValidFormat;
}

return $this->checkVatViaApi($country, $number);
}

private function isValidFormat(string $country, string $number): bool
{
if (!isset($this->patternExpressions[$country])) {
return false;
}

$validateRule = preg_match('/^' . $this->patternExpressions[$country] . '$/', (string) $number) > 0;

if ($validateRule && $country === 'IT') {
$result = self::luhnCheck($number);

return $result % 10 == 0;
}

if ($validateRule && $country === 'HU') {
return $this->validateHuVat($number);
}

return $validateRule;
}

/** @link https://en.wikipedia.org/wiki/Luhn_algorithm */
private function luhnCheck(string $vat): int
{
$sum = 0;
$vat_array = str_split($vat);
$counter = count($vat_array);
for ($index = 0; $index < $counter; ++$index) {
$value = intval($vat_array[$index]);
if ($index % 2 !== 0) {
$value *= 2;
if ($value > 9) {
$value = 1 + ($value % 10);
}
}

$sum += $value;
}

return $sum;
}
Comment thread
bambamboole marked this conversation as resolved.
Outdated

private function validateHuVat(string $vatNumber): bool
{
$checksum = (int) $vatNumber[7];
$weights = [9, 7, 3, 1, 9, 7, 3];
$sum = 0;

foreach ($weights as $i => $weight) {
$sum += (int) $vatNumber[$i] * $weight;
}

$calculatedChecksum = (10 - ($sum % 10)) % 10;

return $calculatedChecksum === $checksum;
}

private function vatCleaner(string $vatNumber): string
{
$vatNumber_no_spaces = trim($vatNumber);

return strtoupper($vatNumber_no_spaces);
}

/**
* @return array{0: string, 1: string}
*/
private function splitVat(string $vatNumber): array
{
return [
substr($vatNumber, 0, 2),
substr($vatNumber, 2),
];
}

/**
* @throws \SoapFault
*/
private function checkVatViaApi(string $country, string $number): bool
{
$client = new \SoapClient(
'https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl',
['connection_timeout' => 10],
);
$response = $client->checkVat(
[
'countryCode' => $country,
'vatNumber' => $number,
]
);

return $response->valid;
}
Comment thread
bambamboole marked this conversation as resolved.
Outdated
}
1 change: 1 addition & 0 deletions src/lang/de/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
'datauri' => ':attribute ist keine gültige Data-URL.',
'ulid' => ':attribute ist keine gültige ULID.',
'ean' => 'Der Wert :attribute ist keine gültige European Article Number (EAN).',
'europeanvatnumber' => 'Der Wert :attribute ist keine gültige europäische Umsatzsteuer-Identifikationsnummer.',
'gtin' => 'Der Wert :attribute ist keine gültige Global Trade Item Number (GTIN).',
'postalcode' => 'Der Wert :attribute muss eine gültige Postleitzahl sein.',
'mimetype' => 'Der Wert :attribute einhält keinen gültigen Internet Media Type (MIME-Type).',
Expand Down
1 change: 1 addition & 0 deletions src/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
'datauri' => 'The :attribute must be a valid data url.',
'ulid' => 'The :attribute is not a valid ULID.',
'ean' => 'The :attribute is not a valid European Article Number (EAN).',
'europeanvatnumber' => 'The :attribute is not a valid European VAT number.',
'gtin' => 'The :attribute is not a valid Global Trade Item Number (GTIN).',
'postalcode' => 'The value :attribute must be a valid postal code.',
'mimetype' => 'The value :attribute does not contain a valid Internet Media Type (MIME-Type).',
Expand Down
1 change: 1 addition & 0 deletions src/lang/es/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'datauri' => ':attribute debe ser un url de datos válido .',
'ulid' => ':attribute no es un ULID válido.',
'ean' => ':attribute no es un EAN válido.',
'europeanvatnumber' => ':attribute no es un número de IVA europeo válido.',
'gtin' => ':attribute no es un número GTIN válido.',
'postalcode' => 'El valor de :attribute debe ser un código postal válido.',
'mimetype' => 'El valor de :attribute no contiene un tipo de media de internet (MIME-Type) válido.',
Expand Down
1 change: 1 addition & 0 deletions src/lang/fr/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'datauri' => 'Le :attribute doit être un Data URL valide.',
'ulid' => 'Le :attribute doit être un ULID valide.',
'ean' => 'Le :attribute doit être un EAN valide.',
'europeanvatnumber' => 'Le :attribute doit être un numéro de TVA européen valide.',
'gtin' => 'Le :attribute doit être un GTIN valide.',
'postalcode' => 'La valeur :attribute doit être un code postal valide.',
'mimetype' => 'La valeur :attribute ne contient pas de type de média Internet valide (type MIME).',
Expand Down
1 change: 1 addition & 0 deletions src/lang/nl/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'datauri' => ':attribute moet een geldige Data-URL zijn.',
'ulid' => ':attribute is geen geldig ULID.',
'ean' => ':attribute is geen geldig European Article Number (EAN).',
'europeanvatnumber' => ':attribute is geen geldig Europees btw-nummer.',
'gtin' => ':attribute is geen geldig Global Trade Item Number (GTIN).',
'postalcode' => ':attribute moet een geldige postcode zijn.',
'mimetype' => ':attribute bevat geen geldig internetmediatype (MIME-type).',
Expand Down
81 changes: 81 additions & 0 deletions tests/Rules/EuropeanVatNumberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Intervention\Validation\Tests\Rules;

use Generator;
use Intervention\Validation\Rules\EuropeanVatNumber;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class EuropeanVatNumberTest extends TestCase
{
#[DataProvider('dataProvider')]
public function testValidation(bool $result, string $value): void
{
$valid = (new EuropeanVatNumber())->isValid($value);
$this->assertEquals($result, $valid);
}

public static function dataProvider(): Generator
{
yield [true, 'ATU12345678'];
yield [true, 'BE0123456789'];
yield [true, 'BE1234567890'];
yield [true, 'BG123456789'];
yield [true, 'BG1234567890'];
yield [true, 'CY12345678A'];
yield [true, 'CZ12345678'];
yield [true, 'CZ123456789'];
yield [true, 'CZ1234567890'];
yield [true, 'DE123456789'];
yield [true, 'DK12345678'];
yield [true, 'DK12 34 56 78'];
yield [true, 'EE123456789'];
yield [true, 'EL123456789'];
yield [true, 'ESA12345678'];
yield [true, 'ES12345678A'];
yield [true, 'ESA1234567B'];
yield [true, 'FI12345678'];
yield [true, 'FR12123456789'];
yield [true, 'FRAA123456789'];
yield [true, 'GB123456789'];
yield [true, 'GB123456789012'];
yield [true, 'GBGD123'];
yield [true, 'GBHA123'];
yield [true, 'HR12345678901'];
yield [true, 'HU12345676'];
yield [true, 'IE1234567A'];
yield [true, 'IE1A234567B'];
yield [true, 'IT02182030391'];
yield [true, 'LT123456789'];
yield [true, 'LT123456789012'];
yield [true, 'LU12345678'];
yield [true, 'LV12345678901'];
yield [true, 'MT12345678'];
yield [true, 'NL123456789B01'];
yield [true, 'PL1234567890'];
yield [true, 'PT123456789'];
yield [true, 'RO12'];
yield [true, 'RO1234567890'];
yield [true, 'SE123456789012'];
yield [true, 'SI12345678'];
yield [true, 'SK1234567890'];
yield [true, 'CHE123456789MWST'];
yield [true, 'CHE123456789TVA'];
yield [true, 'CHE123456789IVA'];
yield [true, 'CH123456'];
yield [true, ' de123456789 '];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
yield [true, ' de123456789 '];
yield [false, ' de123456789 '];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches the behaviour of the other rules.

yield [false, 'foobar'];
yield [false, 'XX123456789'];
yield [false, 'DE12345678'];
yield [false, 'DE1234567890'];
yield [false, 'AT12345678'];
yield [false, 'ATU1234567'];
yield [false, 'NL123456789B1'];
yield [false, 'IT12345678901'];
yield [false, 'HU12345678'];
yield [false, ''];
}
}