Files
fotospiel-app/docs/ops/billing-ops.md

7.9 KiB
Raw 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.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.

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