billing: Stripe-Belege auf Deutsch und sprechende Produktnamen

Drei Stellschrauben für die Stripe-Rechnung (Befund Test-Buchung:
englischer Beleg, Position nur 'Starter'):
- stripePreferredLocales() am User: Stripe rendert Rechnung/Quittung
  in der Kontosprache (de-DE, bei language=en englisch)
- Checkout-Session mit locale in der Kontosprache statt Auto-Erkennung
- billing:sync-stripe-plans vergibt sprechende Produktnamen inkl.
  PM-Kontingent und zieht sie bei bereits verknüpften Plänen an den
  bestehenden Stripe-Produkten nach (Preise bleiben unverändert)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-15 15:25:07 +00:00
parent f1dcb9cd6b
commit 5585a26380
4 changed files with 51 additions and 2 deletions

View file

@ -36,7 +36,20 @@ class SyncStripePlans extends Command
foreach (Plan::query()->active()->get() as $plan) {
if ($plan->stripe_product_id && $plan->stripe_price_id_monthly && $plan->stripe_price_id_yearly) {
$this->line("{$plan->slug}: vollständig verknüpft, übersprungen.");
// Preise sind unveränderlich, der Produktname nicht: Er wird
// bei jedem Lauf nachgezogen, damit Stripe-Belege den
// sprechenden Namen tragen (statt z. B. nur "Starter").
if ($dryRun) {
$this->line("[dry-run] {$plan->slug}: verknüpft — Produktname auf \"{$this->stripeProductName($plan)}\" aktualisieren.");
continue;
}
$stripe->products->update($plan->stripe_product_id, [
'name' => $this->stripeProductName($plan),
]);
$this->line("{$plan->slug}: verknüpft — Produktname aktualisiert.");
continue;
}
@ -56,7 +69,7 @@ class SyncStripePlans extends Command
if (! $productId) {
$product = $stripe->products->create([
'name' => $plan->name,
'name' => $this->stripeProductName($plan),
'metadata' => ['plan_slug' => $plan->slug],
]);
$productId = $product->id;
@ -124,6 +137,19 @@ class SyncStripePlans extends Command
$this->warn("Bitte in die .env eintragen: STRIPE_PRICE_SINGLE_PM={$price->id}");
}
/**
* Sprechender Produktname für Stripe-Belege: Auf Rechnung und
* Checkout-Seite erscheint sonst nur der nackte Tarifname.
*/
private function stripeProductName(Plan $plan): string
{
return sprintf(
'Tarif %s — %d Pressemitteilungen/Monat (pressekonto)',
$plan->name,
$plan->press_release_quota,
);
}
private function createPrice(object $stripe, string $productId, Plan $plan, int $unitAmount, string $interval): string
{
$price = $stripe->prices->create([

View file

@ -123,6 +123,18 @@ class User extends Authenticatable implements MustVerifyEmail
return $address?->company ?: ($address?->name ?: $this->name);
}
/**
* Sprache für Stripe-Belege (Cashier-Hook): Rechnungen, Quittungen und
* Mahn-Mails von Stripe folgen der Kontosprache statt Stripes
* Englisch-Default.
*
* @return array<int, string>
*/
public function stripePreferredLocales(): ?array
{
return [$this->language === 'en' ? 'en' : 'de-DE'];
}
/**
* Buchungs-Voraussetzung (Entscheidung 12.06.2026): Tarif- und
* Einzel-PM-Checkouts erfordern eine vollständige Rechnungsadresse.

View file

@ -348,6 +348,8 @@ class StripeCheckoutService
'cancel_url' => route('me.bookings.index', ['checkout' => 'abbruch']),
'billing_address_collection' => 'auto',
'automatic_tax' => ['enabled' => true],
// Checkout-Seite in der Kontosprache statt Browser-Erkennung.
'locale' => $user->language === 'en' ? 'en' : 'de',
];
if ($user->stripeAddress() === null) {