Commit graph

6 commits

Author SHA1 Message Date
ff2eee0988 billing: asynchrone Zahlarten (PayPal, SEPA) korrekt erfüllen
checkout.session.completed feuert bei asynchronen Zahlarten schon
nach dem Redirect mit payment_status=unpaid — der Listener hat
trotzdem sofort erfüllt (Credits gutgeschrieben / Kauf bezahlt),
auch wenn die Zahlung später scheitert. Erfüllung jetzt nur bei
payment_status paid/no_payment_required, zusätzlich reagiert der
Listener auf checkout.session.async_payment_succeeded; Fehlschläge
werden geloggt, der Kauf bleibt wiederholbar auf Pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 14:27:09 +00:00
7043ea8ad5 billing: Nebenläufigkeit & Idempotenz härten (H4, Stripe, Boost-Slots)
- CreditTopup-Gutschrift, MAN-Rechnung, Proof-PDF und Stripe-Webhook-
  Spiegelung sperren die betroffene Zeile innerhalb der Transaktion und
  prüfen die Idempotenz erst danach (verhindert Doppelgutschrift bei
  at-least-once-Webhooks, H4).
- Gemeinsamer LocksModelForUpdate-Trait für die häufige Lock-by-Key-Form
  (CreditTopup, ManualInvoice).
- Unique-Index auf invoices.stripe_invoice_id; Boost-Inventar über
  boost_slot_locks serialisiert, verwaiste Kategorie-Locks werden beim
  Löschen der Rubrik entfernt.

Deployment: vor migrate prüfen, ob invoices.stripe_invoice_id bereits
Duplikate enthält (die Unique-Migration bricht sonst bewusst ab).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:47:21 +00:00
ef2b4fe2ad 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>
2026-07-07 13:38:57 +00:00
4b7f6d3f0f 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>
2026-07-07 13:22:30 +00:00
4cda4d3bda STR-Spiegelung: USt aus total_taxes lesen (Stripe-API ohne tax-Feld)
Aktuelle Stripe-API-Versionen liefern kein Top-Level-`tax` mehr auf der
Invoice — die Spiegelung buchte 0 USt und markierte die Rechnung als
netto (QA-Befund 07.07., STR-00001). Fallback-Kette: tax (alt) →
total_taxes (aktuell) → total_tax_amounts (Übergangsname).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:42:31 +00:00
38fab64e10 Phase 9E (Backbone): Stripe-Produkt-Sync und Webhook-Verarbeitung mit STR-Spiegelung
- billing:sync-stripe-plans: legt die Tarife als Netto-Produkte/Preise
  (tax_behavior exclusive, EUR) in Stripe an und pflegt die IDs zurueck
  nach plans; idempotent, --dry-run. Gegen Stripe Test-Mode ausgefuehrt —
  alle 4 Tiers verknuepft.
- ProcessStripeWebhook (Listener auf Cashier WebhookReceived):
  - invoice.payment_succeeded -> Spiegelung in den lokalen STR-Kreis
    (fortlaufende Nummer via InvoiceNumberGenerator, Adress-Snapshot
    bevorzugt aus dem Stripe-Payload inkl. lokaler USt-ID, Status paid,
    idempotent gegen doppelte Zustellung)
  - checkout.session.completed -> markiert den referenzierten
    single_purchases-Datensatz als bezahlt (Metadata single_purchase_id)
- CASHIER_CURRENCY=eur (+ Locale de_DE); Cashier-Webhook-Route aktiv
- Doku: Billing-Referenz §7 + Phase-9-Plan (9E-Backbone) aktualisiert

Offen fuer 9E-Rest: Checkout-Flows (Abo + Einmalkauf), Webhook-Endpoint
im Stripe-Dashboard + STRIPE_WEBHOOK_SECRET, Slot-Logik auf
Plan-Kontingent (fachliche Frage: Grandfathered = unbegrenzt?).

Tests: StripeWebhookProcessingTest (7, inkl. Event-Wiring).
Suite: 497 passed, 4 skipped. Pint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:07:09 +00:00