fix(billing): normalise both sides of the VAT comparison; cast the timestamp
A verifier that returns the number in display form ("DE 811 907 980") would
have failed the comparison against the normalised current value and silently
switched a genuine reverse-charge customer back to domestic VAT. Both sides are
normalised now, and vat_id_verified_at is a real datetime rather than a string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
parent
5aef5e693c
commit
c36ea17b39
|
|
@ -15,9 +15,14 @@ use RuntimeException;
|
|||
class Customer extends Model
|
||||
{
|
||||
/** Comparable form: spaces and punctuation are cosmetic in a VAT number. */
|
||||
public static function normaliseVatId(?string $value): string
|
||||
{
|
||||
return strtoupper(preg_replace('/[\s.-]+/', '', (string) $value) ?? '');
|
||||
}
|
||||
|
||||
public function normalisedVatId(): string
|
||||
{
|
||||
return strtoupper(preg_replace('/[\s.-]+/', '', (string) $this->vat_id) ?? '');
|
||||
return self::normaliseVatId($this->vat_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,9 +32,11 @@ class Customer extends Model
|
|||
*/
|
||||
public function hasVerifiedVatId(): bool
|
||||
{
|
||||
// Both sides normalised: a verifier that stores the number in display
|
||||
// form ("DE 811 907 980") must not invalidate a genuine confirmation.
|
||||
return $this->vat_id_verified_at !== null
|
||||
&& $this->normalisedVatId() !== ''
|
||||
&& $this->normalisedVatId() === strtoupper((string) $this->vat_id_verified_value);
|
||||
&& $this->normalisedVatId() === self::normaliseVatId($this->vat_id_verified_value);
|
||||
}
|
||||
|
||||
/** @use HasFactory<\Database\Factories\CustomerFactory> */
|
||||
|
|
@ -43,7 +50,7 @@ class Customer extends Model
|
|||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['closed_at' => 'datetime'];
|
||||
return ['closed_at' => 'datetime', 'vat_id_verified_at' => 'datetime'];
|
||||
}
|
||||
|
||||
public function seats(): HasMany
|
||||
|
|
|
|||
|
|
@ -184,6 +184,26 @@ it('charges domestic VAT to a domestic business and to private customers', funct
|
|||
->assertSee(__('billing.cart.vat', ['percent' => '20']));
|
||||
});
|
||||
|
||||
it('accepts a verified value stored in display form', function () {
|
||||
config()->set('provisioning.tax.rate_percent', 20);
|
||||
config()->set('provisioning.tax.seller_country', 'AT');
|
||||
|
||||
[$customer, ] = cartCustomer();
|
||||
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 1000, 'status' => 'pending']);
|
||||
|
||||
// A verifier may hand back the number the way it prints it; spaces are
|
||||
// cosmetic and must not invalidate a genuine confirmation.
|
||||
$customer->update([
|
||||
'vat_id' => 'DE811907980',
|
||||
'vat_id_verified_at' => now(),
|
||||
'vat_id_verified_value' => 'DE 811 907 980',
|
||||
]);
|
||||
|
||||
expect($customer->fresh()->hasVerifiedVatId())->toBeTrue()
|
||||
->and($customer->fresh()->vat_id_verified_at)->toBeInstanceOf(Illuminate\Support\Carbon::class)
|
||||
->and(Order::query()->first()->fresh()->grossCents())->toBe(1000);
|
||||
});
|
||||
|
||||
it('will not let a typed-in VAT ID zero the tax', function () {
|
||||
config()->set('provisioning.tax.rate_percent', 20);
|
||||
config()->set('provisioning.tax.seller_country', 'AT');
|
||||
|
|
|
|||
Loading…
Reference in New Issue