diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 30d50dd..75df32d 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -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 diff --git a/tests/Feature/CartTest.php b/tests/Feature/CartTest.php index c0e3ca8..ae83d52 100644 --- a/tests/Feature/CartTest.php +++ b/tests/Feature/CartTest.php @@ -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');