Downgrade-Vorkehrungen: Kontingent-Sperre beim Tarifwechsel, Gutschriften nicht spiegeln

QA-Befund 07.07.: Der Wechsel zurück in einen kleineren Tarif erzeugte
eine Proration-Gutschrift (negative Stripe-Rechnung) — die STR-Spiegelung
crashte an den unsigned-Spalten (Webhook-500), und der Wechsel war trotz
verbrauchtem Monatskontingent möglich.

- ProcessStripeWebhook: Belege ohne positiven Rechnungsbetrag
  (Gutschrift/Null-Rechnung) werden geloggt übersprungen statt gespiegelt
- CheckoutController::swap: Wechsel gesperrt, wenn der Monatsverbrauch
  das Kontingent des Ziel-Tarifs übersteigt (möglich ab Monatserstem)
- Buchungs-Modal: erklärt die Sperre inkl. Datum statt Bestätigungs-
  Button; Proration-Hinweis deckt jetzt auch die Guthaben-Richtung ab

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-07 13:22:30 +00:00
parent 090a893cc6
commit 4b7f6d3f0f
6 changed files with 165 additions and 3 deletions

View file

@ -94,6 +94,22 @@ 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 {
$this->checkout->swapSubscription($user, $plan, $interval);
} catch (IncompletePayment $exception) {

View file

@ -80,6 +80,21 @@ class ProcessStripeWebhook
?? collect($stripeInvoice['total_taxes'] ?? $stripeInvoice['total_tax_amounts'] ?? [])->sum('amount'));
$total = (int) ($stripeInvoice['total'] ?? $subtotal + $tax);
// Ein Tarifwechsel nach unten erzeugt eine Proration-Gutschrift:
// Stripe meldet sie als "bezahlte" Rechnung mit negativem Betrag,
// das Guthaben verbleibt als Kundenkonto-Saldo und mindert künftige
// Rechnungen. In den STR-Rechnungsausgang gehört so ein Beleg nicht
// (QA-Befund 07.07.: unsigned-Spalten → Webhook-500) — der Beleg
// selbst bleibt bei Stripe einsehbar.
if ($total <= 0) {
Log::info('STR-Spiegelung übersprungen: Stripe-Beleg ohne positiven Rechnungsbetrag (Gutschrift/Null-Rechnung).', [
'stripe_invoice_id' => $stripeInvoiceId,
'total_cents' => $total,
]);
return;
}
$invoice = Invoice::query()->create([
'user_id' => $user->id,
'invoice_billing_address_id' => $this->snapshotAddress($user, $stripeInvoice)->id,