Downgrades ohne Gutschrift: Tarifwechsel nach unten erst zum Periodenende

QA-Entscheidung 07.07.: Eine Buchung ist nicht rückabwickelbar — es darf
nie eine negative Proration-Rechnung oder ein Stripe-Guthaben entstehen.

- StripeCheckoutService::isDowngrade: sofortiger Wechsel nur, wenn der
  Periodenpreis des Ziel-Tarifs den aktuellen erreicht (Wechsel-Rechnung
  damit garantiert >= 0); sonst Downgrade
- scheduleDowngrade: Stripe Subscription Schedule stellt das Abo zum Ende
  der bezahlten Periode um (proration_behavior none), hebt eine
  vorgemerkte Kündigung auf und spiegelt die Vormerkung lokal
  (neue Spalten scheduled_stripe_price/scheduled_change_at/
  stripe_schedule_id an subscriptions)
- Sofort-Upgrade löst eine bestehende Vormerkung vorher auf (release)
- Webhook customer.subscription.updated räumt die Vormerkung nach
  Vollzug ab
- Buchungs-Modal unterscheidet Upgrade (sofort, Differenz-Rechnung) und
  Downgrade (Vormerkung, keine Zahlung heute); Banner + Hinweis auf der
  Paket-Karte zeigen den vorgemerkten Wechsel mit Datum
- Kontingent-Sperre aus 4b7f6d3 entfernt — ohne Gutschrift gegenstandslos

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-07 13:38:57 +00:00
parent 388ca0e620
commit 80e04e26e0
8 changed files with 419 additions and 89 deletions

View file

@ -94,23 +94,17 @@ class CheckoutController extends Controller
return $this->backToBookings(__('Dieser Tarif ist bereits Ihr aktueller Tarif.'));
}
// Downgrade-Vorkehrung (QA-Befund 07.07.): Der Kontingent-Zähler
// läuft pro Kalendermonat — wer mehr PMs genutzt hat, als der
// Ziel-Tarif monatlich enthält, bekäme beim sofortigen Wechsel die
// anteilige Gutschrift für einen bereits konsumierten Monat. Solche
// Wechsel sind erst ab dem nächsten Monatsersten möglich.
$quotaUsed = (int) $user->press_release_quota_used_this_month;
if ($quotaUsed > (int) $plan->press_release_quota) {
return $this->backToBookings(__('Ein Wechsel zu :plan ist erst ab dem :date möglich: Sie haben in diesem Monat bereits :used Pressemitteilungen genutzt — dieser Tarif enthält :quota pro Monat.', [
'plan' => $plan->name,
'date' => now()->addMonth()->startOfMonth()->format('d.m.Y'),
'used' => $quotaUsed,
'quota' => (int) $plan->press_release_quota,
]));
}
try {
// Downgrade-Vorkehrung (QA-Befund 07.07.): Ein günstigerer Tarif
// wird erst zum Ende der bezahlten Periode wirksam — es entsteht
// nie eine Gutschrift oder ein negativer Rechnungsbetrag. Nur
// Upgrades werden sofort umgestellt und anteilig nachberechnet.
if ($this->checkout->isDowngrade($user, $plan, $interval)) {
$this->checkout->scheduleDowngrade($user, $plan, $interval);
return redirect()->route('me.bookings.index', ['checkout' => 'wechsel-geplant']);
}
$this->checkout->swapSubscription($user, $plan, $interval);
} catch (IncompletePayment $exception) {
return redirect()->route('cashier.payment', [