Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 7 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,12 @@ 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()

### Global Release Identifier (GRid)

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

declare(strict_types=1);

namespace Intervention\Validation\Rules;

class EuropeanVatNumber extends Luhn
{
/**
* @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 isValid(mixed $value): bool
{
$vatNumber = strtoupper(trim((string) $value));
[$country, $number] = $this->splitVat($vatNumber);

if (!isset($this->patternExpressions[$country])) {
return false;
}

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

if ($hasMatch && $country === 'IT') {
return parent::isValid($number);
}

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

return $hasMatch;
}

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;
}

/**
* @return array{0: string, 1: string}
*/
private function splitVat(string $vatNumber): array
{
return [
substr($vatNumber, 0, 2),
substr($vatNumber, 2),
];
}
}
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
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
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, ''];
}
}