stripe_price_id_yearly : $plan->stripe_price_id_monthly; $this->syncTaxIdFromBillingAddress($user); return $user ->newSubscription('default', $priceId) ->checkout($this->sessionOptions()); } /** * Lokal gepflegte USt-ID vor dem Checkout an den Stripe-Customer * übergeben (User-Panel-Restarbeiten, 12.06.2026) — Stripe Tax * berücksichtigt sie dann ohne erneute Eingabe im Checkout. Fehler * (z. B. von Stripe abgelehnte ID) blockieren den Checkout nicht: * Stripe validiert die im Checkout erfasste ID ohnehin selbst. */ private function syncTaxIdFromBillingAddress(User $user): void { $vatId = strtoupper((string) preg_replace('/\s+/', '', (string) $user->billingAddress?->vat_id)); if ($vatId === '') { return; } $prefixCountry = substr($vatId, 0, 2) === 'EL' ? 'GR' : substr($vatId, 0, 2); if ($prefixCountry !== 'DE' && ! $this->vatResolver->isEuCountry($prefixCountry)) { return; } try { $user->createOrGetStripeCustomer(); $alreadySet = collect($user->taxIds()) ->contains(fn (object $taxId): bool => strtoupper((string) $taxId->value) === $vatId); if (! $alreadySet) { $user->createTaxId('eu_vat', $vatId); } } catch (\Throwable $exception) { Log::warning('USt-ID konnte nicht an Stripe übergeben werden.', [ 'user_id' => $user->id, 'error' => $exception->getMessage(), ]); } } /** * Gemeinsame Session-Optionen: Stripe Tax braucht eine gültige * Kundenadresse — die im Checkout erfasste Rechnungsadresse (und der * Name, Pflicht bei USt-ID-Abfrage) wird darum am Stripe-Customer * gespeichert (`customer_update: auto`). * * `automatic_tax` muss hier explizit gesetzt werden: * Cashier::calculateTaxes() greift nur bei direkter Abo-Erstellung, * NICHT bei Checkout-Sessions — ohne das Flag zeigt Stripe nur den * Nettopreis an (QA-Befund 07.07.). Voraussetzung im Stripe-Account: * Stripe Tax mit Origin-Adresse und DE-Registrierung (WS-7). * * @return array */ private function sessionOptions(): array { return [ 'success_url' => route('me.bookings.index', ['checkout' => 'erfolg']), 'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']), 'billing_address_collection' => 'required', 'automatic_tax' => ['enabled' => true], 'tax_id_collection' => ['enabled' => true], 'customer_update' => [ 'address' => 'auto', 'name' => 'auto', ], ]; } /** * Line-Item für Ad-hoc-Beträge (ohne Stripe-Price). `tax_behavior` * ist Pflicht, sobald `automatic_tax` aktiv ist — Cashiers * checkoutCharge() setzt es nicht und ist darum hier ersetzt. * * @return array */ private function adHocLineItem(int $priceCents, string $name): array { return [ 'price_data' => [ 'currency' => config('cashier.currency', 'eur'), 'product_data' => ['name' => $name], 'unit_amount' => $priceCents, 'tax_behavior' => 'exclusive', ], 'quantity' => 1, ]; } /** * URL zum Stripe Billing Portal (Zahlungsmethode, Rechnungen, Kündigung). * Rücksprung auf die Buchungs-Seite. */ public function billingPortalUrl(User $user): string { return $user->billingPortalUrl(route('me.bookings.index')); } /** * Stripe-Checkout für eine Einzel-PM. Die `single_purchase_id` in den * Session-Metadaten schließt den Kreis: `checkout.session.completed` * markiert den Kauf über ProcessStripeWebhook als bezahlt. */ public function forSinglePurchase(User $user, SinglePurchase $purchase): Checkout { $this->syncTaxIdFromBillingAddress($user); return $user->checkout([config('billing.single_pm_stripe_price_id') => 1], [ ...$this->sessionOptions(), 'metadata' => ['single_purchase_id' => (string) $purchase->id], ]); } /** * Stripe-Checkout für eine Depublikation (T12). Ad-hoc-Charge über den * konfigurierten Netto-Preis (wie Credit-Topups, keine Stripe-Price-ID); * der Webhook depubliziert die PM nach Zahlungseingang. */ public function forDepublication(User $user, SinglePurchase $purchase): Checkout { $this->syncTaxIdFromBillingAddress($user); return $user->checkout( [$this->adHocLineItem($purchase->price_cents, 'Depublikation einer Pressemitteilung')], [ ...$this->sessionOptions(), 'metadata' => ['single_purchase_id' => (string) $purchase->id], ], ); } /** * Stripe-Checkout für eine Wallet-Aufladung. Ad-hoc-Charge über den * Netto-Paketpreis (Stripe Tax ergänzt die USt.); die `credit_topup_id` * in den Metadaten schließt den Kreis im Webhook (Wallet-Gutschrift). */ public function forCreditTopup(User $user, CreditTopup $topup): Checkout { $this->syncTaxIdFromBillingAddress($user); return $user->checkout( [$this->adHocLineItem($topup->price_cents, "Credit-Guthaben ({$topup->credits} Credits)")], [ ...$this->sessionOptions(), 'metadata' => ['credit_topup_id' => (string) $topup->id], ], ); } }