Checkout: Profil-Rechnungsadresse als Stripe-Rechnungsempfaenger

Die lokal gepflegte Rechnungsadresse stand nur bei der Erstanlage am
Stripe-Customer; der Checkout erzwang die Neueingabe und ueberschrieb
sie — auf der Rechnung stand die getippte Adresse statt der Profildaten
(QA-Befund 07.07.). Jetzt: Customer-Sync (Name/Firma, Adresse, USt-ID)
vor jedem Checkout, stripeName()-Hook liefert Firma bzw. Rechnungsname
statt Login-Anzeigename, billing_address_collection auf 'auto' (keine
Zwangs-Neueingabe bei hinterlegter Adresse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-07 12:09:05 +00:00
parent 5166fc7b40
commit 304c5817b5
3 changed files with 98 additions and 5 deletions

View file

@ -107,6 +107,19 @@ class User extends Authenticatable implements MustVerifyEmail
];
}
/**
* Name für den Stripe-Customer (Cashier-Hook): Rechnungsempfänger ist
* die Firma bzw. der Name aus der Rechnungsadresse — nicht der
* Login-Anzeigename (QA-Befund 07.07.: falscher Empfänger auf der
* Stripe-Rechnung).
*/
public function stripeName(): ?string
{
$address = $this->billingAddress;
return $address?->company ?: ($address?->name ?: $this->name);
}
/**
* Buchungs-Voraussetzung (Entscheidung 12.06.2026): Tarif- und
* Einzel-PM-Checkouts erfordern eine vollständige Rechnungsadresse.

View file

@ -31,13 +31,37 @@ class StripeCheckoutService
? $plan->stripe_price_id_yearly
: $plan->stripe_price_id_monthly;
$this->syncTaxIdFromBillingAddress($user);
$this->syncCustomerFromBillingAddress($user);
return $user
->newSubscription('default', $priceId)
->checkout($this->sessionOptions());
}
/**
* Stripe-Customer vor jedem Checkout auf den Stand der lokal gepflegten
* Rechnungsadresse bringen: Name/Firma und Adresse (stripeName/
* stripeAddress-Hooks am User) plus USt-ID. Ohne diesen Sync stand die
* Profil-Rechnungsadresse nur bei der Erstanlage am Customer und die im
* Checkout getippte Adresse landete auf der Rechnung (QA-Befund 07.07.).
* Fehler blockieren den Checkout nicht — Stripe erfasst die Adresse
* dann selbst im Checkout.
*/
private function syncCustomerFromBillingAddress(User $user): void
{
try {
$user->createOrGetStripeCustomer();
$user->syncStripeCustomerDetails();
} catch (\Throwable $exception) {
Log::warning('Stripe-Customer konnte nicht mit der Rechnungsadresse synchronisiert werden.', [
'user_id' => $user->id,
'error' => $exception->getMessage(),
]);
}
$this->syncTaxIdFromBillingAddress($user);
}
/**
* Lokal gepflegte USt-ID vor dem Checkout an den Stripe-Customer
* übergeben (User-Panel-Restarbeiten, 12.06.2026) — Stripe Tax
@ -95,7 +119,11 @@ class StripeCheckoutService
return [
'success_url' => route('me.bookings.index', ['checkout' => 'erfolg']),
'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']),
'billing_address_collection' => 'required',
// 'auto': Stripe fragt die Adresse nur ab, wenn am Customer keine
// hinterlegt ist — die Profil-Rechnungsadresse (Sync oben) bleibt
// damit maßgeblich und erscheint auf der Rechnung. Unser
// Buchungs-Gate verlangt ohnehin eine vollständige Adresse.
'billing_address_collection' => 'auto',
'automatic_tax' => ['enabled' => true],
'tax_id_collection' => ['enabled' => true],
'customer_update' => [
@ -141,7 +169,7 @@ class StripeCheckoutService
*/
public function forSinglePurchase(User $user, SinglePurchase $purchase): Checkout
{
$this->syncTaxIdFromBillingAddress($user);
$this->syncCustomerFromBillingAddress($user);
return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [
...$this->sessionOptions(),
@ -156,7 +184,7 @@ class StripeCheckoutService
*/
public function forDepublication(User $user, SinglePurchase $purchase): Checkout
{
$this->syncTaxIdFromBillingAddress($user);
$this->syncCustomerFromBillingAddress($user);
return $user->checkout(
[$this->adHocLineItem($purchase->price_cents, 'Depublikation einer Pressemitteilung')],
@ -174,7 +202,7 @@ class StripeCheckoutService
*/
public function forCreditTopup(User $user, CreditTopup $topup): Checkout
{
$this->syncTaxIdFromBillingAddress($user);
$this->syncCustomerFromBillingAddress($user);
return $user->checkout(
[$this->adHocLineItem($topup->price_cents, "Credit-Guthaben ({$topup->credits} Credits)")],

View file

@ -0,0 +1,52 @@
<?php
use App\Models\BillingAddress;
use App\Models\User;
test('the stripe recipient name prefers the billing company over names', function () {
$user = User::factory()->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();
});