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

210 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Billing & Zahlungs-Operationen
sidebar_label: 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/webhook``PaddleWebhookController::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`/`billing`Queues 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 `active`Status 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/webhook``PaddleWebhookController::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 `Tenant`Felder 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:
- `SyncCouponToPaddle`Job 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.