Files
fotospiel-app/docs/ops/billing-ops.md
Codex Agent 66bf9e4a8c
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Document Paddle cutover
2026-01-02 22:29:00 +01:00

12 KiB
Raw Permalink Blame History

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

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

1. Komponentenüberblick

  • Paddle
    • Abwicklung von WebCheckout, Paketen und Subscriptions.
    • Webhooks für Käufe, 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 PaddleMigration und zum KatalogSync.

2. Typische Problemszenarien

  • Webhook kommt nicht an / schlägt fehl
    • Symptom: Paddle zeigt Zahlung „completed“, TenantPaket im Backend bleibt unverändert.
    • Checkliste:
      • Logs der WebhookRoutes prüfen (Statuscodes, Exceptions).
        • Endpoint: POST /paddle/webhookPaddleWebhookController::handle().
        • Controller ruft CheckoutWebhookService::handlePaddleEvent() auf.
      • WebhookReplay über das Paddle 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 (Paddle/RevenueCat) mit internem Ledger.
    • Bei doppelten Buchungen: Prozess definieren (Refund via Paddle, 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
    • PaddleDashboard: ist die Zahlung dort korrekt verbucht? (Transaktions/AbonnementAnsicht).
  3. Backend-Status prüfen
    • Paketzuweisung und Limits in der DB (Readonly!) inspizieren:
      • checkout_sessions wurde die Session korrekt auf completed gesetzt? (provider = paddle, paddle_transaction_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:
      • PaddleEvents erneut senden lassen, ggf. tests/api/_testing/checkout/sessions/{session}/simulate-paddle (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 Paddle/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
    • PaddleKeys, 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 PaddleMigration abgeschlossen ist.

6. Konkrete Paddle-Flows im System

6.1 Checkout-Erstellung

  • Marketing-Checkout / API:
    • MarketingController und PackageController nutzen PaddleCheckoutService::createCheckout() (App\Services\Paddle\PaddleCheckoutService).
    • Der Service:
      • Stellt sicher, dass ein paddle_customer_id für den Tenant existiert (PaddleCustomerService::ensureCustomerId()).
      • Baut Metadaten (tenant_id, package_id, optional checkout_session_id) für spätere Zuordnung.
      • Ruft POST /checkout/links im PaddleAPI auf und erhält eine checkout_url.
  • Ops-Sicht:
    • Wenn paddle_price_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 (PaddleCheckoutService, Controller) und PackageKonfiguration prüfen.

6.2 Webhook-Verarbeitung & Idempotenz

  • Endpoint: POST /paddle/webhookPaddleWebhookController::handle().
  • Service: CheckoutWebhookService (App\Services\Checkout\CheckoutWebhookService):
    • Unterscheidet zwischen TransaktionsEvents (transaction.*) und SubscriptionEvents (subscription.*).
    • Idempotenz:
      • Nutzt ein CacheLock (checkout:webhook:paddle:{transaction_id|session_id}), um parallele Verarbeitung desselben Events zu verhindern.
      • Schreibt Metadaten (paddle_last_event, paddle_status, paddle_checkout_id) in checkout_sessions.provider_metadata.
  • Ergebnis:
    • Bei transaction.completed:
      • CheckoutSession wird als processing markiert.
      • CheckoutAssignmentService::finalise() weist Paket/Tenant zu.
      • Session wird auf completed gesetzt.
    • Bei transaction.failed / transaction.cancelled:
      • Session wird auf failed gesetzt, Coupons werden als fehlgeschlagen markiert.

6.2.1 SandboxWebhook registrieren

  • Command (Sandbox-Umgebung aktiv):
    • php artisan paddle:webhooks:register --traffic-source=simulation
  • Optional mit URL-Override:
    • php artisan paddle:webhooks:register --url=https://staging.example.com/paddle/webhook --traffic-source=simulation
  • EventListe kommt aus config/paddle.php (webhook_events). Override möglich:
    • php artisan paddle:webhooks:register --events=transaction.completed --events=subscription.created

6.2.2 Verarbeitete Paddle-Events

Die WebhookHandler erwarten mindestens diese Events:

  • transaction.created
  • transaction.processing
  • transaction.completed (inkl. Add-ons/GiftVouchers)
  • transaction.failed
  • transaction.cancelled
  • subscription.created
  • subscription.updated
  • subscription.paused
  • subscription.resumed
  • subscription.cancelled
  • subscription.past_due

6.3 Subscriptions & TenantPackages

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

6.4 Paket- & Coupon-Synchronisation

  • Pakete:
    • ArtisanCommand paddle:sync-packages (App\Console\Commands\PaddleSyncPackages) stößt für ausgewählte oder alle Pakete SyncPackageToPaddle/PullPackageFromPaddle Jobs an.
    • SyncJobs nutzen PaddleCatalogService, um Produkte/Preise in Paddle zu erstellen/aktualisieren und paddle_product_id/paddle_price_id lokal zu pflegen.
  • Coupons:
    • SyncCouponToPaddleJob spiegelt interne CouponKonfiguration in Paddle Discounts (PaddleDiscountService).
  • Ops-Sicht:
    • Bei KatalogAbweichungen paddle:sync-packages --dry-run verwenden, um Snapshots zu prüfen, bevor tatsächliche Änderungen gesendet werden.
    • Fehlgeschlagene Syncs in den Logs (Paddle package sync failed, Paddle discount sync failed) 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 paddle_sync_status, paddle_synced_at und Letzter Fehler.
    • Fehlermeldung stammt aus paddle_snapshot.error.message.
  2. Trockenlauf ausführen
    • php artisan paddle:sync-packages --package=<id|slug> --dry-run
    • Prüfe die erzeugten PayloadSnapshots (in paddle_snapshot) auf falsche IDs/Preise.
  3. Mapping prüfen
    • Falls paddle_product_id oder paddle_price_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 paddle:sync-packages --package=<id|slug> --queue
    • Bei Bedarf: --allow-unmapped nur bewusst verwenden (z.B. initiales Mapping).
  5. Pull für Abgleich
    • php artisan paddle:sync-packages --package=<id|slug> --pull zum Abgleich mit Paddle.
  6. Logs prüfen
    • Erwartete Logeinträge: Paddle package sync failed, Paddle addon sync failed, Paddle discount sync failed.
    • Achte auf wiederkehrende Fehler (z.B. invalid product/price IDs).

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

7. Production Cutover: Paddle Migration

Diese Checkliste beschreibt den kontrollierten Wechsel auf Paddle in Produktion.

  1. Vorbereitung (T1 Woche)
    • Confirm: PADDLE_ENVIRONMENT=production, PADDLE_API_KEY, PADDLE_CLIENT_TOKEN, PADDLE_WEBHOOK_SECRET.
    • Package IDs validieren: alle aktiven Packages haben paddle_product_id und paddle_price_id.
    • paddle:sync-packages --dry-run auf eine Stichprobe anwenden.
    • EventListe prüfen: config/paddle.php (webhook_events).
  2. Staging Smoke (T2 Tage)
    • paddle:webhooks:register --traffic-source=simulation auf Staging ausfuehren.
    • Testkauf via Paddle Sandbox und Webhook Replay verifizieren.
  3. Cutover Window (T0)
    • MarketingCheckout kurz einfrieren (kein Checkout waehrend der Umschaltung).
    • Production Webhook registrieren:
      • paddle:webhooks:register --traffic-source=platform --url=https://<prod-domain>/paddle/webhook
    • Queue worker laufen lassen (Queue: webhooks/billing sofern konfiguriert).
  4. Activation
    • Erstes ProduktionsCheckout ausfuehren.
    • Verify: checkout_sessions.provider_metadata wird mit paddle_* Feldern befuellt.
    • Verify: TenantPackage aktiv und subscription_status korrekt.
  5. Rollback (falls notwendig)
    • Checkout wieder deaktivieren (MarketingCheckout ausblenden).
    • Paddle Webhook Destination im Paddle Dashboard deaktivieren.
    • Status und Logs sichern (Webhooks, paddle_sync Log).
  6. PostCutover (T+1)
    • Stichproben auf neue Tenants/Packages.
    • Monitoring: Fehlgeschlagene Webhooks, SyncFehler, Support Tickets.