From 6eedaa474eda73382d32b32c924ce906b33acb46 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 7 Jul 2026 12:09:05 +0000 Subject: [PATCH] Checkout: Profil-Rechnungsadresse als Stripe-Rechnungsempfaenger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/Models/User.php | 13 +++++ .../Billing/StripeCheckoutService.php | 38 ++++++++++++-- .../Billing/StripeCustomerHooksTest.php | 52 +++++++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/Billing/StripeCustomerHooksTest.php diff --git a/app/Models/User.php b/app/Models/User.php index ddfa8dcb..96b8109f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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. diff --git a/app/Services/Billing/StripeCheckoutService.php b/app/Services/Billing/StripeCheckoutService.php index ce2de996..071b4f08 100644 --- a/app/Services/Billing/StripeCheckoutService.php +++ b/app/Services/Billing/StripeCheckoutService.php @@ -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)")], diff --git a/tests/Feature/Billing/StripeCustomerHooksTest.php b/tests/Feature/Billing/StripeCustomerHooksTest.php new file mode 100644 index 00000000..4597e8d9 --- /dev/null +++ b/tests/Feature/Billing/StripeCustomerHooksTest.php @@ -0,0 +1,52 @@ +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(); +});