Files
fotospiel-app/docs/ops/billing-ops.md
Codex Agent 0430f0b1cc
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Document Paddle sync recovery
2026-01-02 21:52:37 +01:00

9.2 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.

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.