USt-ID vor jeder Buchung per eVatR gegenprüfen, Stripe-Tax-IDs abgleichen

USt-IDs laufen ab — Kunden bestellten anderswo über veraltete IDs
steuerfrei. Vor jedem Checkout wird die Profil-USt-ID jetzt einmal per
eVatR (BZSt) geprüft (Ergebnis 6 h gecacht): Eine definitiv abgelehnte
ID geht nicht an Stripe, nicht mehr passende eu_vat-Einträge am
Stripe-Customer werden entfernt — Stripe Tax rechnet dann wieder mit
Steuer statt Reverse Charge. Fail-open bei API-Ausfall und deutschen
IDs (eVatR prüft nur ausländische EU-IDs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-08 13:28:56 +00:00
parent f8e8f3eb1a
commit 6c4fd0cc17
2 changed files with 170 additions and 17 deletions

View file

@ -3,6 +3,7 @@
use App\Models\BillingAddress;
use App\Models\User;
use App\Services\Billing\StripeCheckoutService;
use Illuminate\Support\Facades\Http;
test('the stripe recipient name prefers the billing company over names', function () {
$user = User::factory()->create(['name' => 'Login Anzeigename']);
@ -87,6 +88,91 @@ test('one-time checkouts enable stripe invoice creation, subscription checkouts
expect(invokeSessionOptions($user))->not->toHaveKey('invoice_creation');
});
test('the checkout vat id is re-verified via evatr before every booking', function () {
config()->set('billing.own_vat_id', 'DE999999999');
Http::fake(['api.evatr.vies.bzst.de/*' => Http::response(['status' => 'evatr-0000'])]);
$user = User::factory()->create();
BillingAddress::factory()->create([
'user_id' => $user->id,
'country_code' => 'AT',
// Normalisierung: Großschreibung, Leerzeichen entfernt.
'vat_id' => 'atu 12345678',
]);
expect(invokeVerifiedVatId($user->fresh()))->toBe('ATU12345678');
Http::assertSent(fn ($request): bool => $request['angefragteUstid'] === 'ATU12345678');
});
test('a vat id rejected by evatr is withheld from stripe', function () {
// Kern des Befunds vom 08.07.: abgelaufene USt-IDs dürfen nicht weiter
// steuerfrei (Reverse Charge) buchen — die Buchung läuft dann mit Steuer.
config()->set('billing.own_vat_id', 'DE999999999');
Http::fake(['api.evatr.vies.bzst.de/*' => Http::response(['status' => 'evatr-1001'])]);
$user = User::factory()->create();
BillingAddress::factory()->create([
'user_id' => $user->id,
'country_code' => 'AT',
'vat_id' => 'ATU99999999',
]);
expect(invokeVerifiedVatId($user->fresh()))->toBeNull();
});
test('an unreachable evatr api keeps the vat id usable for the booking', function () {
// Fail-open: Ein Ausfall der BZSt-API darf Bestandskunden nicht
// plötzlich Steuer berechnen — nur definitive Ablehnungen halten zurück.
config()->set('billing.own_vat_id', 'DE999999999');
Http::fake(['api.evatr.vies.bzst.de/*' => Http::response(null, 503)]);
$user = User::factory()->create();
BillingAddress::factory()->create([
'user_id' => $user->id,
'country_code' => 'NL',
'vat_id' => 'NL123456789B01',
]);
expect(invokeVerifiedVatId($user->fresh()))->toBe('NL123456789B01');
});
test('german vat ids pass without an online check, non-eu and missing ids yield nothing', function () {
config()->set('billing.own_vat_id', 'DE999999999');
Http::fake();
// Deutsche ID: eVatR kann sie nicht prüfen — Formatprüfung genügt.
$german = User::factory()->create();
BillingAddress::factory()->create(['user_id' => $german->id, 'country_code' => 'DE', 'vat_id' => 'DE123456789']);
expect(invokeVerifiedVatId($german->fresh()))->toBe('DE123456789');
// Ohne USt-ID und außerhalb der EU gibt es nichts zu übergeben.
$without = User::factory()->create();
BillingAddress::factory()->create(['user_id' => $without->id, 'vat_id' => null]);
expect(invokeVerifiedVatId($without->fresh()))->toBeNull();
$swiss = User::factory()->create();
BillingAddress::factory()->create(['user_id' => $swiss->id, 'country_code' => 'CH', 'vat_id' => 'CHE123456789']);
expect(invokeVerifiedVatId($swiss->fresh()))->toBeNull();
Http::assertNothingSent();
});
test('a vat id that does not match the billing country is withheld from stripe', function () {
config()->set('billing.own_vat_id', 'DE999999999');
Http::fake();
$user = User::factory()->create();
BillingAddress::factory()->create([
'user_id' => $user->id,
'country_code' => 'DE',
'vat_id' => 'ATU12345678',
]);
expect(invokeVerifiedVatId($user->fresh()))->toBeNull();
Http::assertNothingSent();
});
/**
* @return array<string, mixed>
*/
@ -97,3 +183,10 @@ function invokeSessionOptions(User $user, string $method = 'sessionOptions'): ar
return $method->invoke($service, $user);
}
function invokeVerifiedVatId(User $user): ?string
{
$service = app(StripeCheckoutService::class);
return (new ReflectionMethod($service, 'verifiedVatIdForCheckout'))->invoke($service, $user);
}