12 KiB
title, sidebar_label
| title | sidebar_label |
|---|---|
| Billing & Zahlungs-Operationen | Billing-Runbook |
Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Paddle/RevenueCat‑Webhooks und Paket‑Inkonsistenzen operativ umzugehen ist.
1. Komponentenüberblick
- Paddle
- Abwicklung von Web‑Checkout, Paketen und Subscriptions.
- Webhooks für Käufe, Verlängerungen, Stornos.
- Fotospiel Backend
- Modelle wie
Tenant,Packages,tenant_packages,event_packages. - Services/Jobs zur Paket‑Zuweisung, Limit‑Berechnung und Nutzungstracking.
- Modelle wie
Details zur Architektur findest du in den PRP‑Kapiteln (Billing/Freemium) sowie in den bd-Issues zur Paddle‑Migration und zum Katalog‑Sync.
2. Typische Problemszenarien
- Webhook kommt nicht an / schlägt fehl
- Symptom: Paddle zeigt Zahlung „completed“, Tenant‑Paket im Backend bleibt unverändert.
- Checkliste:
- Logs der Webhook‑Routes prüfen (Statuscodes, Exceptions).
- Endpoint:
POST /paddle/webhook→PaddleWebhookController::handle(). - Controller ruft
CheckoutWebhookService::handlePaddleEvent()auf.
- Endpoint:
- Webhook‑Replay über das Paddle Dashboard auslösen (für einzelne Events).
- Queue‑Status prüfen:
- Falls Webhooks über Queues verarbeitet werden, auf
default/billing‑Queues achten (je nach Konfiguration).
- Falls Webhooks über Queues verarbeitet werden, auf
- Logs der Webhook‑Routes prüfen (Statuscodes, Exceptions).
- Doppelte oder fehlende Abbuchungen
- Abgleich von Zahlungsprovider‑Daten (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). - Limit‑Zähler (
used_photos,used_events) und aktuelle Nutzung. - Letzte relevante Webhooks/Jobs (z.B. vor kurzem migriert?).
- Aktives Paket (
3. Operative Schritte bei Payment Incidents
- Event/Tenant identifizieren
- IDs und relevante Paket‑Infos aus DB/Admin UI holen.
- Provider-Status prüfen
- Paddle‑Dashboard: ist die Zahlung dort korrekt verbucht? (Transaktions‑/Abonnement‑Ansicht).
- Backend-Status prüfen
- Paketzuweisung und Limits in der DB (Read‑only!) inspizieren:
checkout_sessions– wurde die Session korrekt aufcompletedgesetzt? (provider = paddle,paddle_transaction_idgefüllt?)package_purchases– existiert ein Eintrag für Tenant/Package mit erwarteter Provider‑Referenz?tenant_packages– stimmt deractive‑Status undexpires_atmit dem erwarteten Abostatus überein?
- Paketzuweisung und Limits in der DB (Read‑only!) inspizieren:
- Entscheidung
- Automatische Nachverarbeitung via Webhook‑Replay/Job‑Retry:
- Paddle‑Events erneut senden lassen, ggf.
tests/api/_testing/checkout/sessions/{session}/simulate-paddle(in Test‑Umgebungen) nutzen.
- Paddle‑Events erneut senden lassen, ggf.
- Notfall: manuelle Paket‑Anpassung (nur mit klar dokumentierter Begründung):
- Paket in
tenant_packagesaktivieren/verlängern undpackage_purchasessauber nachziehen.
- Paket in
- Automatische Nachverarbeitung via Webhook‑Replay/Job‑Retry:
- 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 (Tenant‑ID, Event‑ID, Payment‑Provider‑Referenz, Zeitstempel).
- Welche Standardantworten es gibt (z.B. „Zahlung in Prüfung, Paket kurzfristig manuell freigeschaltet“).
5. Hinweise zur Implementierung
- Konfiguration
- Paddle‑Keys, Webhook‑Secrets und Feature‑Flags sollten ausschließlich in
.env/Config‑Dateien liegen und niemals im Code/Logs landen. - Sandbox vs. Live‑Keys klar trennen; Ops sollte wissen, welche Umgebung gerade aktiv ist.
- Paddle‑Keys, Webhook‑Secrets und Feature‑Flags sollten ausschließlich in
- Sicherheit
- Webhook‑Signaturen und Timestamps prüfen; bei verdächtigen Mustern (z.B. Replay‑Angriffe) Security‑Runbooks konsultieren.
- Keine sensiblen Payment‑Details in Applikations‑Logs ausgeben.
Diese Sektion ist bewusst generisch gehalten, damit sie auch nach Implementation der finalen Billing‑Architektur noch passt. Details zu Tabellen/Jobs sollten ergänzt werden, sobald die Paddle‑Migration abgeschlossen ist.
6. Konkrete Paddle-Flows im System
6.1 Checkout-Erstellung
- Marketing-Checkout / API:
MarketingControllerundPackageControllernutzenPaddleCheckoutService::createCheckout()(App\Services\Paddle\PaddleCheckoutService).- Der Service:
- Stellt sicher, dass ein
paddle_customer_idfür den Tenant existiert (PaddleCustomerService::ensureCustomerId()). - Baut Metadaten (
tenant_id,package_id, optionalcheckout_session_id) für spätere Zuordnung. - Ruft
POST /checkout/linksim Paddle‑API auf und erhält einecheckout_url.
- Stellt sicher, dass ein
- Ops-Sicht:
- Wenn
paddle_price_idbei einem Package fehlt, wird kein Checkout erzeugt – Marketing‑UI zeigt entsprechende Fehlertexte (sieheresources/lang/*/marketing.php). - Bei wiederkehrenden „checkout failed“‑Fehlern die Logs (
PaddleCheckoutService, Controller) und Package‑Konfiguration prüfen.
- Wenn
6.2 Webhook-Verarbeitung & Idempotenz
- Endpoint:
POST /paddle/webhook→PaddleWebhookController::handle(). - Service:
CheckoutWebhookService(App\Services\Checkout\CheckoutWebhookService):- Unterscheidet zwischen Transaktions‑Events (
transaction.*) und Subscription‑Events (subscription.*). - Idempotenz:
- Nutzt ein Cache‑Lock (
checkout:webhook:paddle:{transaction_id|session_id}), um parallele Verarbeitung desselben Events zu verhindern. - Schreibt Metadaten (
paddle_last_event,paddle_status,paddle_checkout_id) incheckout_sessions.provider_metadata.
- Nutzt ein Cache‑Lock (
- Unterscheidet zwischen Transaktions‑Events (
- Ergebnis:
- Bei
transaction.completed:CheckoutSessionwird alsprocessingmarkiert.CheckoutAssignmentService::finalise()weist Paket/Tenant zu.- Session wird auf
completedgesetzt.
- Bei
transaction.failed/transaction.cancelled:- Session wird auf
failedgesetzt, Coupons werden als fehlgeschlagen markiert.
- Session wird auf
- Bei
6.2.1 Sandbox‑Webhook 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
- Event‑Liste 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 Webhook‑Handler erwarten mindestens diese Events:
transaction.createdtransaction.processingtransaction.completed(inkl. Add-ons/Gift‑Vouchers)transaction.failedtransaction.cancelledsubscription.createdsubscription.updatedsubscription.pausedsubscription.resumedsubscription.cancelledsubscription.past_due
6.3 Subscriptions & TenantPackages
- Subscription‑Events (
subscription.*) werden ebenfalls vonCheckoutWebhookServicebehandelt:- Tenant wird aus
metadata.tenant_idoderpaddle_customer_idermittelt. - Package wird über
metadata.package_idoderpaddle_price_idaufgelöst. TenantPackagewird erstellt/aktualisiert (paddle_subscription_id,expires_at,active).Tenant.subscription_statusundsubscription_expires_atwerden gesteuert.
- Tenant wird aus
- Ops-Sicht:
- Bei abweichenden Abostatus (z.B. Paddle zeigt „active“, Tenant nicht):
- Subscription‑Events im Paddle‑Dashboard prüfen.
- Letzte
subscription.*‑Events in den Logs,TenantPackage‑ undTenant‑Felder gegenprüfen.
- Bei abweichenden Abostatus (z.B. Paddle zeigt „active“, Tenant nicht):
6.4 Paket- & Coupon-Synchronisation
- Pakete:
- Artisan‑Command
paddle:sync-packages(App\Console\Commands\PaddleSyncPackages) stößt für ausgewählte oder alle PaketeSyncPackageToPaddle/PullPackageFromPaddleJobs an. - Sync‑Jobs nutzen
PaddleCatalogService, um Produkte/Preise in Paddle zu erstellen/aktualisieren undpaddle_product_id/paddle_price_idlokal zu pflegen.
- Artisan‑Command
- Coupons:
SyncCouponToPaddle‑Job spiegelt interne Coupon‑Konfiguration in Paddle Discounts (PaddleDiscountService).
- Ops-Sicht:
- Bei Katalog‑Abweichungen
paddle:sync-packages --dry-runverwenden, 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.
- Bei Katalog‑Abweichungen
6.5 Recovery-Playbook: Katalog‑Sync fehlgeschlagen
Wenn der Katalog‑Sync fehlschlägt oder Pakete nicht mehr korrekt verknüpft sind:
- Status im Admin prüfen
PackageResource→ Felderpaddle_sync_status,paddle_synced_atundLetzter Fehler.- Fehlermeldung stammt aus
paddle_snapshot.error.message.
- Trockenlauf ausführen
php artisan paddle:sync-packages --package=<id|slug> --dry-run- Prüfe die erzeugten Payload‑Snapshots (in
paddle_snapshot) auf falsche IDs/Preise.
- Mapping prüfen
- Falls
paddle_product_idoderpaddle_price_idfehlt oder falsch ist: im Paket‑Admin korrigieren. - Bulk‑Sync blockiert unmapped Pakete; gezielte Korrektur vor dem nächsten Lauf ist Pflicht.
- Falls
- Sync erneut anstoßen
php artisan paddle:sync-packages --package=<id|slug> --queue- Bei Bedarf:
--allow-unmappednur bewusst verwenden (z.B. initiales Mapping).
- Pull für Abgleich
php artisan paddle:sync-packages --package=<id|slug> --pullzum Abgleich mit Paddle.
- 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).
- Erwartete Logeinträge:
Diese Untersektion soll dir als Operator helfen zu verstehen, wie Paddle‑Aktionen 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.
- Vorbereitung (T‑1 Woche)
- Confirm:
PADDLE_ENVIRONMENT=production,PADDLE_API_KEY,PADDLE_CLIENT_TOKEN,PADDLE_WEBHOOK_SECRET. - Package IDs validieren: alle aktiven Packages haben
paddle_product_idundpaddle_price_id. paddle:sync-packages --dry-runauf eine Stichprobe anwenden.- Event‑Liste prüfen:
config/paddle.php(webhook_events).
- Confirm:
- Staging Smoke (T‑2 Tage)
paddle:webhooks:register --traffic-source=simulationauf Staging ausfuehren.- Testkauf via Paddle Sandbox und Webhook Replay verifizieren.
- Cutover Window (T‑0)
- Marketing‑Checkout 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/billingsofern konfiguriert).
- Activation
- Erstes Produktions‑Checkout ausfuehren.
- Verify:
checkout_sessions.provider_metadatawird mitpaddle_*Feldern befuellt. - Verify:
TenantPackageaktiv undsubscription_statuskorrekt.
- Rollback (falls notwendig)
- Checkout wieder deaktivieren (Marketing‑Checkout ausblenden).
- Paddle Webhook Destination im Paddle Dashboard deaktivieren.
- Status und Logs sichern (Webhooks,
paddle_syncLog).
- Post‑Cutover (T+1)
- Stichproben auf neue Tenants/Packages.
- Monitoring: Fehlgeschlagene Webhooks, Sync‑Fehler, Support Tickets.