Initial commit: ws_vatcheck at v1.0.3

Includes the fix for the GR/EL VAT prefix bug that broke exemption for
every Greek VAT number (in_array-on-values vs array_key_exists in
CacheTaxManager/VatNumber, plus the missing use import in
EuValidatorAdapter). Adds IMPROVEMENTS.txt with a checklist of features
adopted from comparing against the vatchecker module (offline-fallback
policy, failure logging, company-name check, configurable EU country
list, per-product exclusion, GB/XI mapping, AJAX live validation,
address-priority strategy).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 16:01:41 +02:00
co-authored by Claude Sonnet 5
parent 4c181ea0a7
commit 451817f2e1
47 changed files with 3432 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace Websmid\VatCheck\Model;
use Websmid\VatCheck\Config\Config;
use Context;
class VatNumber
{
private $vatNumber;
private $isoCode;
private $errorMessage = '';
public static $euIso = [
'AT' => 'AT',
'BE' => 'BE',
'DK' => 'DK',
'FI' => 'FI',
'FR' => 'FR',
'FX' => 'FR',
'DE' => 'DE',
'GR' => 'EL',
'IE' => 'IE',
'IT' => 'IT',
'LU' => 'LU',
'NL' => 'NL',
'PT' => 'PT',
'ES' => 'ES',
'SE' => 'SE',
'CY' => 'CY',
'EE' => 'EE',
'HU' => 'HU',
'LV' => 'LV',
'LT' => 'LT',
'MT' => 'MT',
'PL' => 'PL',
'SK' => 'SK',
'CZ' => 'CZ',
'SI' => 'SI',
'RO' => 'RO',
'BG' => 'BG',
'HR' => 'HR',
];
private $isoPattern = [
'AT' => 'U[A-Z\d]{8}',
'BE' => '[0|1]{1}\d{9}',
'BG' => '\d{9,10}',
'CY' => '\d{8}[A-Z]',
'CZ' => '\d{8,10}',
'DE' => '\d{9}',
'DK' => '(\d{2} ?){3}\d{2}',
'EE' => '\d{9}',
'GR' => '\d{9}',
'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}',
'FI' => '\d{8}',
'FR' => '([A-Z0-9]{2})\d{9}',
'FX' => '([A-Z0-9]{2})\d{9}',
'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(
string $rawVatNumber,
string $rawIsoCode
) {
$this->vatNumber = $this->commonFormatVatNumber($rawVatNumber);
$this->isoCode = strtoupper($rawIsoCode);
$this->validateVatNr();
}
protected function validateVatNr()
{
$vatPrefix = self::$euIso[$this->isoCode] ?? $this->isoCode;
if( substr($this->vatNumber, 0, 2) != $vatPrefix ){
$this->errorMessage = Context::getContext()->getTranslator()->trans('ISO in VAT number doesn\'t seem to match selected country', [], 'Modules.Ws_VatCheck.Front');
}
$trimmedVat = str_replace($vatPrefix, '', $this->vatNumber);
$isoPattern = $this->isoPattern[$this->isoCode];
if( !preg_match('/^(?:'.$isoPattern.')$/', $trimmedVat) ){
$this->errorMessage = Context::getContext()->getTranslator()->trans('VAT number doesn\'t seem to be valid', [], 'Modules.Ws_VatCheck.Front');
}
}
public function getFormattedVat()
{
return $this->vatNumber;
}
public function hasErrors()
{
return !empty($this->errorMessage);
}
public function isNotEuIso()
{
return !array_key_exists($this->isoCode, self::$euIso);
}
private function commonFormatVatNumber(string $rawVatNumber)
{
return strtoupper(preg_replace('/[ .]/', '', $rawVatNumber));
}
public function getErrorMessage()
{
return $this->errorMessage;
}
}