Files
fotospiel-app/docs/ops/billing-ops.md
Codex Agent 10c99de1e2
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Migrate billing from Paddle to Lemon Squeezy
2026-02-03 10:59:54 +01:00

12 KiB
Raw Blame History

title, sidebar_label
title sidebar_label
Billing & Zahlungs-Operationen Billing-Runbook

Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Lemon Squeezy/RevenueCatWebhooks und PaketInkonsistenzen operativ umzugehen ist.

1. Komponentenüberblick

  • Lemon Squeezy
    • Abwicklung von WebCheckout, Paketen und Subscriptions.
    • Webhooks für Bestellungen, Verlängerungen, Stornos.
  • Fotospiel Backend
    • Modelle wie Tenant, Packages, tenant_packages, event_packages.
    • Services/Jobs zur PaketZuweisung, LimitBerechnung und Nutzungstracking.

Details zur Architektur findest du in den PRPKapiteln (Billing/Freemium) sowie in den bd-Issues zur Lemon SqueezyMigration und zum KatalogSync.

2. Typische Problemszenarien

  • Webhook kommt nicht an / schlägt fehl
    • Symptom: Lemon Squeezy zeigt Zahlung „completed/paid“, TenantPaket im Backend bleibt unverändert.
    • Checkliste:
      • Logs der WebhookRoutes prüfen (Statuscodes, Exceptions).
        • Endpoint: POST /lemonsqueezy/webhookLemonSqueezyWebhookController::handle().
        • Controller ruft CheckoutWebhookService::handleLemonSqueezyEvent() auf.
      • WebhookReplay über das Lemon Squeezy Dashboard auslösen (für einzelne Events).
      • QueueStatus prüfen:
        • Falls Webhooks über Queues verarbeitet werden, auf default/billingQueues achten (je nach Konfiguration).
  • Doppelte oder fehlende Abbuchungen
    • Abgleich von ZahlungsproviderDaten (Lemon Squeezy/RevenueCat) mit internem Ledger.
    • Bei doppelten Buchungen: Prozess definieren (Refund via Lemon Squeezy, Anpassung im Ledger).
    • Bei fehlenden Buchungen: ggf. manuelle Paketzuweisung nach erfolgter Zahlung.
  • Pakete/Limits passen nicht zur Realität
    • Tenant meldet: „Paket falsch“, „Galerie schon abgelaufen“ o.Ä.
    • Prüfen:
      • Aktives Paket (tenant_packages, event_packages).
      • LimitZähler (used_photos, used_events) und aktuelle Nutzung.
      • Letzte relevante Webhooks/Jobs (z.B. vor kurzem migriert?).

3. Operative Schritte bei Payment Incidents

  1. Event/Tenant identifizieren
    • IDs und relevante PaketInfos aus DB/Admin UI holen.
  2. Provider-Status prüfen
    • Lemon SqueezyDashboard: ist die Zahlung dort korrekt verbucht? (Order/SubscriptionAnsicht).
  3. Backend-Status prüfen
    • Paketzuweisung und Limits in der DB (Readonly!) inspizieren:
      • checkout_sessions wurde die Session korrekt auf completed gesetzt? (provider = lemonsqueezy, lemonsqueezy_order_id gefüllt?)
      • package_purchases existiert ein Eintrag für Tenant/Package mit erwarteter ProviderReferenz?
      • tenant_packages stimmt der activeStatus und expires_at mit dem erwarteten Abostatus überein?
  4. Entscheidung
    • Automatische Nachverarbeitung via WebhookReplay/JobRetry:
      • Lemon SqueezyEvents erneut senden lassen, ggf. tests/api/_testing/checkout/sessions/{session}/simulate-lemonsqueezy (in TestUmgebungen) nutzen.
    • Notfall: manuelle PaketAnpassung (nur mit klar dokumentierter Begründung):
      • Paket in tenant_packages aktivieren/verlängern und package_purchases sauber nachziehen.
  5. Dokumentation
    • Vorgang im Ticket oder als bd-Issue festhalten, falls wiederkehrend.

TODO: Ergänze konkrete Tabellen-/Modellnamen und die relevanten Jobs/Artisan Commands, sobald Lemon Squeezy/RevenueCat Migration finalisiert ist.

4. Zusammenarbeit mit Finance/Support

  • Klar definieren, wer Rückerstattungen freigibt und durchführt.
  • Playbook für Support:
    • Welche Informationen sie sammeln sollen, bevor sie an Ops eskalieren (TenantID, EventID, PaymentProviderReferenz, Zeitstempel).
    • Welche Standardantworten es gibt (z.B. „Zahlung in Prüfung, Paket kurzfristig manuell freigeschaltet“).

5. Hinweise zur Implementierung

  • Konfiguration
    • Lemon SqueezyKeys, WebhookSecrets und FeatureFlags sollten ausschließlich in .env/ConfigDateien liegen und niemals im Code/Logs landen.
    • Sandbox vs. LiveKeys klar trennen; Ops sollte wissen, welche Umgebung gerade aktiv ist.
  • Sicherheit
    • WebhookSignaturen und Timestamps prüfen; bei verdächtigen Mustern (z.B. ReplayAngriffe) SecurityRunbooks konsultieren.
    • Keine sensiblen PaymentDetails in ApplikationsLogs ausgeben.

Diese Sektion ist bewusst generisch gehalten, damit sie auch nach Implementation der finalen BillingArchitektur noch passt. Details zu Tabellen/Jobs sollten ergänzt werden, sobald die Lemon SqueezyMigration abgeschlossen ist.

6. Konkrete Lemon Squeezy-Flows im System

6.1 Checkout-Erstellung

  • Marketing-Checkout / API:
    • MarketingController und PackageController nutzen LemonSqueezyCheckoutService::createCheckout() (App\Services\LemonSqueezy\LemonSqueezyCheckoutService).
    • Der Service:
      • Baut custom_data (tenant_id, package_id, optional checkout_session_id) für spätere Zuordnung.
      • Ruft POST /checkouts im Lemon SqueezyAPI auf und erhält eine checkout_url.
  • Ops-Sicht:
    • Wenn lemonsqueezy_variant_id bei einem Package fehlt, wird kein Checkout erzeugt MarketingUI zeigt entsprechende Fehlertexte (siehe resources/lang/*/marketing.php).
    • Bei wiederkehrenden „checkout failed“Fehlern die Logs (LemonSqueezyCheckoutService, Controller) und PackageKonfiguration prüfen.

6.2 Webhook-Verarbeitung & Idempotenz

  • Endpoint: POST /lemonsqueezy/webhookLemonSqueezyWebhookController::handle().
  • Service: CheckoutWebhookService (App\Services\Checkout\CheckoutWebhookService):
    • Unterscheidet zwischen OrderEvents (order_*) und SubscriptionEvents (subscription_*).
    • Idempotenz:
      • Nutzt ein CacheLock (checkout:webhook:lemonsqueezy:{order_id|session_id}), um parallele Verarbeitung desselben Events zu verhindern.
      • Schreibt Metadaten (lemonsqueezy_last_event, lemonsqueezy_status, lemonsqueezy_checkout_id, lemonsqueezy_order_id) in checkout_sessions.provider_metadata.
  • Ergebnis:
    • Bei order_created/order_updated mit Status paid:
      • CheckoutSession wird als processing markiert.
      • CheckoutAssignmentService::finalise() weist Paket/Tenant zu.
      • Session wird auf completed gesetzt.
    • Bei order_payment_failed / order_refunded:
      • Session wird auf failed gesetzt, Coupons werden als fehlgeschlagen markiert.

6.2.1 SandboxWebhook registrieren

  • Command (TestMode aktiv):
    • php artisan lemonsqueezy:webhooks:register --test-mode
  • Optional mit URL-Override:
    • php artisan lemonsqueezy:webhooks:register --url=https://staging.example.com/lemonsqueezy/webhook --test-mode
  • EventListe kommt aus config/lemonsqueezy.php (webhook_events). Override möglich:
    • php artisan lemonsqueezy:webhooks:register --events=order_created --events=subscription_created

6.2.2 Verarbeitete Lemon Squeezy-Events

Die WebhookHandler erwarten mindestens diese Events:

  • order_created
  • order_updated
  • order_payment_failed
  • order_refunded
  • subscription_created
  • subscription_updated
  • subscription_cancelled
  • subscription_expired
  • subscription_paused

6.3 Subscriptions & TenantPackages

  • SubscriptionEvents (subscription_*) werden ebenfalls von CheckoutWebhookService behandelt:
    • Tenant wird aus metadata.tenant_id oder lemonsqueezy_customer_id ermittelt.
    • Package wird über metadata.package_id oder lemonsqueezy_variant_id aufgelöst.
    • TenantPackage wird erstellt/aktualisiert (lemonsqueezy_subscription_id, expires_at, active).
    • Tenant.subscription_status und subscription_expires_at werden gesteuert.
  • Ops-Sicht:
    • Bei abweichenden Abostatus (z.B. Lemon Squeezy zeigt „active“, Tenant nicht):
      • SubscriptionEvents im Lemon SqueezyDashboard prüfen.
      • Letzte subscription_*Events in den Logs, TenantPackage und TenantFelder gegenprüfen.

6.4 Paket- & Coupon-Synchronisation

  • Pakete:
    • ArtisanCommand lemonsqueezy:sync-packages (App\Console\Commands\LemonSqueezySyncPackages) stößt für ausgewählte oder alle Pakete SyncPackageToLemonSqueezy/PullPackageFromLemonSqueezy Jobs an.
    • SyncJobs nutzen LemonSqueezyCatalogService, um Produkte/Preise in Lemon Squeezy zu erstellen/aktualisieren und lemonsqueezy_product_id/lemonsqueezy_variant_id lokal zu pflegen.
  • Coupons:
    • SyncCouponToLemonSqueezyJob spiegelt interne CouponKonfiguration in Lemon Squeezy Discounts (LemonSqueezyDiscountService).
  • Ops-Sicht:
    • Bei KatalogAbweichungen lemonsqueezy:sync-packages --dry-run verwenden, um Snapshots zu prüfen, bevor tatsächliche Änderungen gesendet werden.
    • Fehlgeschlagene Syncs in den Logs (Lemon Squeezy package sync failed, Lemon Squeezy addon sync failed, Failed syncing coupon to Lemon Squeezy) beobachten.

6.5 Recovery-Playbook: KatalogSync fehlgeschlagen

Wenn der KatalogSync fehlschlägt oder Pakete nicht mehr korrekt verknüpft sind:

  1. Status im Admin prüfen
    • PackageResource → Felder lemonsqueezy_sync_status, lemonsqueezy_synced_at und Letzter Fehler.
    • Fehlermeldung stammt aus lemonsqueezy_snapshot.error.message.
  2. Trockenlauf ausführen
    • php artisan lemonsqueezy:sync-packages --package=<id|slug> --dry-run
    • Prüfe die erzeugten PayloadSnapshots (in lemonsqueezy_snapshot) auf falsche IDs/Preise.
  3. Mapping prüfen
    • Falls lemonsqueezy_product_id oder lemonsqueezy_variant_id fehlt oder falsch ist: im PaketAdmin korrigieren.
    • BulkSync blockiert unmapped Pakete; gezielte Korrektur vor dem nächsten Lauf ist Pflicht.
  4. Sync erneut anstoßen
    • php artisan lemonsqueezy:sync-packages --package=<id|slug> --queue
    • Bei Bedarf: --allow-unmapped nur bewusst verwenden (z.B. initiales Mapping).
  5. Pull für Abgleich
    • php artisan lemonsqueezy:sync-packages --package=<id|slug> --pull zum Abgleich mit Lemon Squeezy.
  6. Logs prüfen
    • Erwartete Logeinträge: Lemon Squeezy package sync failed, Lemon Squeezy addon sync failed, Failed syncing coupon to Lemon Squeezy.
    • Achte auf wiederkehrende Fehler (z.B. invalid product/price IDs).

Diese Untersektion soll dir als Operator helfen zu verstehen, wie Lemon SqueezyAktionen im System abgebildet sind und an welchen Stellen du im Fehlerfall ansetzen kannst.

7. Production Cutover: Lemon Squeezy Migration

Diese Checkliste beschreibt den kontrollierten Wechsel auf Lemon Squeezy in Produktion.

  1. Vorbereitung (T1 Woche)
    • Confirm: LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_WEBHOOK_SECRET, LEMONSQUEEZY_TEST_MODE=false.
    • Package IDs validieren: alle aktiven Packages haben lemonsqueezy_product_id und lemonsqueezy_variant_id.
    • lemonsqueezy:sync-packages --dry-run auf eine Stichprobe anwenden.
    • EventListe prüfen: config/lemonsqueezy.php (webhook_events).
  2. Staging Smoke (T2 Tage)
    • lemonsqueezy:webhooks:register --test-mode auf Staging ausführen.
    • Testkauf via Lemon Squeezy Test Mode und Webhook Replay verifizieren.
  3. Cutover Window (T0)
    • MarketingCheckout kurz einfrieren (kein Checkout waehrend der Umschaltung).
    • Production Webhook registrieren:
      • lemonsqueezy:webhooks:register --url=https://<prod-domain>/lemonsqueezy/webhook
    • Queue worker laufen lassen (Queue: webhooks/billing sofern konfiguriert).
  4. Activation
    • Erstes ProduktionsCheckout ausfuehren.
    • Verify: checkout_sessions.provider_metadata wird mit lemonsqueezy_* Feldern befuellt.
    • Verify: TenantPackage aktiv und subscription_status korrekt.
  5. Rollback (falls notwendig)
    • Checkout wieder deaktivieren (MarketingCheckout ausblenden).
    • Lemon Squeezy Webhook Destination im Lemon Squeezy Dashboard deaktivieren.
    • Status und Logs sichern (Webhooks, lemonsqueezy-sync Log).
  6. PostCutover (T+1)
    • Stichproben auf neue Tenants/Packages.
    • Monitoring: Fehlgeschlagene Webhooks, SyncFehler, Support Tickets.