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:
parent
5166fc7b40
commit
304c5817b5
3 changed files with 98 additions and 5 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)")],
|
||||
|
|
|
|||
52
tests/Feature/Billing/StripeCustomerHooksTest.php
Normal file
52
tests/Feature/Billing/StripeCustomerHooksTest.php
Normal 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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue