55 lines
1.9 KiB
PHP
55 lines
1.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Tax;
|
|
|
|
use App\Models\Customer;
|
|
use App\Services\Billing\TaxTreatment;
|
|
|
|
/**
|
|
* Whether a string is even worth asking VIES about.
|
|
*
|
|
* Shared by the real verifier and the fake, deliberately. A fake that answers
|
|
* where the real one would not have asked is not a simplification, it is a
|
|
* different product — and every test written against it proves something about
|
|
* that product instead of about this one.
|
|
*
|
|
* Two conditions, and the second is less obvious than it looks. Free text
|
|
* normalises into a well-formed VAT number more easily than one would think:
|
|
* "not a number" loses its spaces and becomes NOTANUMBER, which the shape rule
|
|
* alone reads as a perfectly good Norwegian number. VIES is the EU's register and
|
|
* holds nothing else, and reverse charge is an intra-EU rule in any case, so a
|
|
* prefix outside the union is answered here rather than spent on a request.
|
|
*/
|
|
final class VatIdShape
|
|
{
|
|
/** @return bool false when no member state could hold a number like this */
|
|
public static function verifiable(string $vatId): bool
|
|
{
|
|
$normalised = Customer::normaliseVatId($vatId);
|
|
|
|
if (preg_match('/^([A-Z]{2})([0-9A-Z]{8,12})$/', $normalised, $parts) !== 1) {
|
|
return false;
|
|
}
|
|
|
|
// Against TaxTreatment's list rather than a second copy of it: two lists
|
|
// of member states is how one of them ends up a year out of date.
|
|
return in_array($parts[1], TaxTreatment::EU_MEMBER_STATES, true);
|
|
}
|
|
|
|
/**
|
|
* The two halves VIES wants, or null when there is nothing to ask.
|
|
*
|
|
* @return array{0: string, 1: string}|null
|
|
*/
|
|
public static function split(string $vatId): ?array
|
|
{
|
|
$normalised = Customer::normaliseVatId($vatId);
|
|
|
|
if (! self::verifiable($normalised)) {
|
|
return null;
|
|
}
|
|
|
|
return [substr($normalised, 0, 2), substr($normalised, 2)];
|
|
}
|
|
}
|