create(['name' => 'Login Anzeigename']); BillingAddress::factory()->create([ 'user_id' => $user->id, 'company' => 'Beispiel GmbH', 'name' => 'Max Mustermann', ]); expect($user->fresh()->stripeName())->toBe('Beispiel GmbH'); }); test('the stripe recipient name falls back to the billing name, then the display name', function () { $user = User::factory()->create(['name' => 'Login Anzeigename']); BillingAddress::factory()->create([ 'user_id' => $user->id, 'company' => null, 'name' => 'Max Mustermann', ]); expect($user->fresh()->stripeName())->toBe('Max Mustermann'); $withoutAddress = User::factory()->create(['name' => 'Nur Anzeigename']); expect($withoutAddress->stripeName())->toBe('Nur Anzeigename'); }); test('the stripe address mirrors the billing address', function () { $user = User::factory()->create(); BillingAddress::factory()->create([ 'user_id' => $user->id, 'address1' => 'Rechnungsweg 12', 'address2' => 'Hinterhaus', 'postal_code' => '33739', 'city' => 'Bielefeld', 'country_code' => 'DE', ]); expect($user->fresh()->stripeAddress())->toBe([ 'line1' => 'Rechnungsweg 12', 'line2' => 'Hinterhaus', 'postal_code' => '33739', 'city' => 'Bielefeld', 'country' => 'DE', ]); expect(User::factory()->create()->stripeAddress())->toBeNull(); }); test('checkout session options leave the stripe customer untouched when a billing address exists', function () { $user = User::factory()->create(); BillingAddress::factory()->create(['user_id' => $user->id]); $options = invokeSessionOptions($user->fresh()); expect($options['billing_address_collection'])->toBe('auto') ->and($options['automatic_tax'])->toBe(['enabled' => true]) ->and($options)->not->toHaveKeys(['customer_update', 'tax_id_collection']); }); test('checkout session options collect the address in stripe when no billing address exists', function () { $options = invokeSessionOptions(User::factory()->create()); expect($options['billing_address_collection'])->toBe('required') ->and($options['tax_id_collection'])->toBe(['enabled' => true]) ->and($options['customer_update'])->toBe(['address' => 'auto', 'name' => 'auto']); }); test('one-time checkouts enable stripe invoice creation, subscription checkouts do not', function () { $user = User::factory()->create(); BillingAddress::factory()->create(['user_id' => $user->id]); $user = $user->fresh(); // Payment-Mode: ohne invoice_creation erzeugt Stripe keine Rechnung — // und damit gäbe es kein invoice.payment_succeeded für die STR-Spiegelung. $oneTime = invokeSessionOptions($user, 'oneTimeSessionOptions'); expect($oneTime['invoice_creation'])->toBe(['enabled' => true]) ->and($oneTime['automatic_tax'])->toBe(['enabled' => true]); // Subscription-Mode lehnt invoice_creation ab — dort erzeugt Stripe // die Rechnungen ohnehin selbst. 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 */ function invokeSessionOptions(User $user, string $method = 'sessionOptions'): array { $service = app(StripeCheckoutService::class); $method = new ReflectionMethod($service, $method); return $method->invoke($service, $user); } function invokeVerifiedVatId(User $user): ?string { $service = app(StripeCheckoutService::class); return (new ReflectionMethod($service, 'verifiedVatIdForCheckout'))->invoke($service, $user); }