Add company-name cross-check against VIES response

This commit is contained in:
2026-09-11 16:55:32 +02:00
parent 8b54b42991
commit 6f7362c42a
7 changed files with 125 additions and 9 deletions
+30 -2
View File
@@ -7,6 +7,7 @@ use PH7\Eu\Vat\Provider\Europa;
use PH7\Eu\Vat\Exception; use PH7\Eu\Vat\Exception;
use Websmid\VatCheck\Config\Config; use Websmid\VatCheck\Config\Config;
use Websmid\VatCheck\Model\VatCache; use Websmid\VatCheck\Model\VatCache;
use Websmid\VatCheck\Model\CompanyNameMatcher;
use PrestaShopLogger; use PrestaShopLogger;
use Configuration; use Configuration;
@@ -16,12 +17,15 @@ class EuValidatorAdapter
protected $isoCode = ''; protected $isoCode = '';
protected $company = '';
private $checkStatus; private $checkStatus;
public function __construct(string $vatNr, string $isoCode) public function __construct(string $vatNr, string $isoCode, string $company = '')
{ {
$this->vatNr = str_replace($isoCode, '', $vatNr); $this->vatNr = str_replace($isoCode, '', $vatNr);
$this->isoCode = $isoCode; $this->isoCode = $isoCode;
$this->company = $company;
} }
public function checkAndStore() public function checkAndStore()
@@ -46,7 +50,7 @@ class EuValidatorAdapter
$fullVat = $this->isoCode.$this->vatNr; $fullVat = $this->isoCode.$this->vatNr;
$vatCache = VatCache::getOrCreateByVat($fullVat); $vatCache = VatCache::getOrCreateByVat($fullVat);
$vatCache->vat_nr = $fullVat; $vatCache->vat_nr = $fullVat;
$vatCache->valid = $euVatValidator->check(); $vatCache->valid = $euVatValidator->check() && $this->isCompanyNameAcceptable($euVatValidator);
$vatCache->save(); $vatCache->save();
} }
@@ -54,6 +58,30 @@ class EuValidatorAdapter
return true; return true;
} }
/**
* Only rejects on a confirmed mismatch. Skips the check entirely when it's
* disabled, when the address has no company on file, or when VIES didn't
* return a usable name for this member state (not every country discloses
* one) — there's nothing meaningful to compare against in those cases.
*/
private function isCompanyNameAcceptable(Validator $validator): bool
{
if (!Configuration::get(Config::VALIDATE_COMPANY_CONFIG_KEY)) {
return true;
}
if ('' === trim($this->company)) {
return true;
}
$viesName = $validator->getName();
if (!CompanyNameMatcher::hasUsableName($viesName)) {
return true;
}
return CompanyNameMatcher::matches($this->company, $viesName);
}
/** /**
* Decide what to do when VIES itself can't be reached, per the configured * Decide what to do when VIES itself can't be reached, per the configured
* Config::OFFLINE_MODE_CONFIG_KEY policy. Returning true here tells * Config::OFFLINE_MODE_CONFIG_KEY policy. Returning true here tells
+4
View File
@@ -32,6 +32,10 @@ class Config
self::OFFLINE_MODE_LAST_KNOWN_OR_VALID => 'Reuse the last known result, otherwise mark valid', self::OFFLINE_MODE_LAST_KNOWN_OR_VALID => 'Reuse the last known result, otherwise mark valid',
]; ];
// Configuration key toggling the company-name cross-check against VIES.
CONST VALIDATE_COMPANY_CONFIG_KEY = 'WS_VATCHECK_VALIDATE_COMPANY';
CONST VALIDATE_COMPANY_DEFAULT = false;
// Article 39bis W.BTW intra-community exemption notice, per invoice language. // Article 39bis W.BTW intra-community exemption notice, per invoice language.
CONST INTRACOMMUNITY_EXEMPTION_TEXTS = [ CONST INTRACOMMUNITY_EXEMPTION_TEXTS = [
'nl' => 'Vrijgestelde intracommunautaire levering – artikel 39bis W.BTW', 'nl' => 'Vrijgestelde intracommunautaire levering – artikel 39bis W.BTW',
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Websmid\VatCheck\Model;
class CompanyNameMatcher
{
// Common legal-entity suffixes to ignore across the EU/UK/US so e.g.
// "Acme BV" matches VIES's "ACME B.V." or "Acme".
private const LEGAL_SUFFIXES = [
'inc', 'bv', 'gmbh', 'ltd', 'llc', 'corp', 'nv', 'sa', 'ag', 'oy', 'as', 'spa', 'srl',
];
/**
* VIES sometimes returns a placeholder like "---" instead of a real name
* for member states that don't disclose company details. There is nothing
* to compare against in that case.
*/
public static function hasUsableName(string $viesName): bool
{
return '' !== preg_replace('/[^a-zA-Z0-9]+/', '', $viesName);
}
public static function matches(string $addressCompany, string $viesName): bool
{
return self::normalize($addressCompany) === self::normalize($viesName);
}
private static function normalize(string $name): string
{
$name = strtolower($name);
$name = preg_replace('/[.,\/#!$%\^&*;:{}=\-_`~()]/', '', $name);
foreach (self::LEGAL_SUFFIXES as $suffix) {
$name = preg_replace('/\b'.$suffix.'\b/', '', $name);
}
return trim(preg_replace('/\s+/', ' ', $name));
}
}
+36 -3
View File
@@ -23,11 +23,14 @@ trait SettingsForm
{ {
$mode = Tools::getValue(Config::OFFLINE_MODE_CONFIG_KEY); $mode = Tools::getValue(Config::OFFLINE_MODE_CONFIG_KEY);
if (!array_key_exists($mode, Config::OFFLINE_MODE_LABELS)) { if (array_key_exists($mode, Config::OFFLINE_MODE_LABELS)) {
return; Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, $mode);
} }
Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, $mode); Configuration::updateValue(
Config::VALIDATE_COMPANY_CONFIG_KEY,
(bool) Tools::getValue(Config::VALIDATE_COMPANY_CONFIG_KEY)
);
} }
private function renderSettingsForm() private function renderSettingsForm()
@@ -67,6 +70,13 @@ trait SettingsForm
null, null,
Config::OFFLINE_MODE_DEFAULT Config::OFFLINE_MODE_DEFAULT
), ),
Config::VALIDATE_COMPANY_CONFIG_KEY => (bool) Configuration::get(
Config::VALIDATE_COMPANY_CONFIG_KEY,
null,
null,
null,
Config::VALIDATE_COMPANY_DEFAULT
),
]; ];
} }
@@ -99,6 +109,29 @@ trait SettingsForm
), ),
'values' => $choices, 'values' => $choices,
], ],
[
'type' => 'switch',
'label' => $this->trans('Also check the company name', [], 'Modules.Ws_VatCheck.Admin'),
'name' => Config::VALIDATE_COMPANY_CONFIG_KEY,
'is_bool' => true,
'desc' => $this->trans(
'Compare the address\'s company name against the name VIES returns for the VAT number, when available. Not every member state discloses one.',
[],
'Modules.Ws_VatCheck.Admin'
),
'values' => [
[
'id' => 'validate_company_on',
'value' => true,
'label' => $this->trans('Enabled', [], 'Admin.Global'),
],
[
'id' => 'validate_company_off',
'value' => false,
'label' => $this->trans('Disabled', [], 'Admin.Global'),
],
],
],
], ],
'submit' => [ 'submit' => [
'title' => $this->trans('Save', [], 'Admin.Actions'), 'title' => $this->trans('Save', [], 'Admin.Actions'),
+9 -1
View File
@@ -31,7 +31,15 @@ trait Install
private function registerConfiguration() private function registerConfiguration()
{ {
return Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, Config::OFFLINE_MODE_DEFAULT); if (!Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, Config::OFFLINE_MODE_DEFAULT)) {
return false;
}
if (!Configuration::updateValue(Config::VALIDATE_COMPANY_CONFIG_KEY, Config::VALIDATE_COMPANY_DEFAULT)) {
return false;
}
return true;
} }
private function registerHooks() private function registerHooks()
+1 -1
View File
@@ -36,7 +36,7 @@ class CacheTaxManager implements TaxManagerInterface
} }
$vatPrefix = VatNumber::$euIso[$isoCode]; $vatPrefix = VatNumber::$euIso[$isoCode];
$euValidator = new EuValidatorAdapter($address->vat_number, $vatPrefix); $euValidator = new EuValidatorAdapter($address->vat_number, $vatPrefix, (string) $address->company);
if( !$euValidator->checkAndStore() ){ if( !$euValidator->checkAndStore() ){
return false; return false;
} }
+5 -1
View File
@@ -8,5 +8,9 @@ if (!defined('_PS_VERSION_')) {
function upgrade_module_1_0_3($module) function upgrade_module_1_0_3($module)
{ {
return Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, Config::OFFLINE_MODE_DEFAULT); if (!Configuration::updateValue(Config::OFFLINE_MODE_CONFIG_KEY, Config::OFFLINE_MODE_DEFAULT)) {
return false;
}
return Configuration::updateValue(Config::VALIDATE_COMPANY_CONFIG_KEY, Config::VALIDATE_COMPANY_DEFAULT);
} }