207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
---
|
||
title: Billing & Zahlungs-Operationen
|
||
sidebar_label: Billing-Runbook
|
||
---
|
||
|
||
Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Lemon Squeezy/RevenueCat‑Webhooks und Paket‑Inkonsistenzen operativ umzugehen ist.
|
||
|
||
## 1. Komponentenüberblick
|
||
|
||
- **Lemon Squeezy**
|
||
- Abwicklung von Web‑Checkout, Paketen und Subscriptions.
|
||
- Webhooks für Bestellungen, Verlängerungen, Stornos.
|
||
- **Fotospiel Backend**
|
||
- Modelle wie `Tenant`, `Packages`, `tenant_packages`, `event_packages`.
|
||
- Services/Jobs zur Paket‑Zuweisung, Limit‑Berechnung und Nutzungstracking.
|
||
|
||
> Details zur Architektur findest du in den PRP‑Kapiteln (Billing/Freemium) sowie in den bd-Issues zur Lemon Squeezy‑Migration und zum Katalog‑Sync.
|
||
|
||
## 2. Typische Problemszenarien
|
||
|
||
- **Webhook kommt nicht an / schlägt fehl**
|
||
- Symptom: Lemon Squeezy zeigt Zahlung „completed/paid“, Tenant‑Paket im Backend bleibt unverändert.
|
||
- Checkliste:
|
||
- Logs der Webhook‑Routes prüfen (Statuscodes, Exceptions).
|
||
- Endpoint: `POST /lemonsqueezy/webhook` → `LemonSqueezyWebhookController::handle()`.
|
||
- Controller ruft `CheckoutWebhookService::handleLemonSqueezyEvent()` auf.
|
||
- Webhook‑Replay über das Lemon Squeezy 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).
|
||
- **Doppelte oder fehlende Abbuchungen**
|
||
- Abgleich von Zahlungsprovider‑Daten (Lemon Squeezy/RevenueCat) mit internem Ledger.
|
||
- Bei doppelten Buchungen: Prozess definieren (Refund via Lemon Squeezy, 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?).
|
||
|
||
## 3. Operative Schritte bei Payment Incidents
|
||
|
||
1. **Event/Tenant identifizieren**
|
||
- IDs und relevante Paket‑Infos aus DB/Admin UI holen.
|
||
2. **Provider-Status prüfen**
|
||
- Lemon Squeezy‑Dashboard: ist die Zahlung dort korrekt verbucht? (Order‑/Subscription‑Ansicht).
|
||
3. **Backend-Status prüfen**
|
||
- Paketzuweisung und Limits in der DB (Read‑only!) inspizieren:
|
||
- `checkout_sessions` – wurde die Session korrekt auf `completed` gesetzt? (`provider = lemonsqueezy`, `lemonsqueezy_order_id` gefüllt?)
|
||
- `package_purchases` – existiert ein Eintrag für Tenant/Package mit erwarteter Provider‑Referenz?
|
||
- `tenant_packages` – stimmt der `active`‑Status und `expires_at` mit dem erwarteten Abostatus überein?
|
||
4. **Entscheidung**
|
||
- Automatische Nachverarbeitung via Webhook‑Replay/Job‑Retry:
|
||
- Lemon Squeezy‑Events erneut senden lassen, ggf. `tests/api/_testing/checkout/sessions/{session}/simulate-lemonsqueezy` (in Test‑Umgebungen) nutzen.
|
||
- Notfall: manuelle Paket‑Anpassung (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 Lemon Squeezy/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**
|
||
- Lemon Squeezy‑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.
|
||
- **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 Lemon Squeezy‑Migration abgeschlossen ist.
|
||
|
||
## 6. Konkrete Lemon Squeezy-Flows im System
|
||
|
||
### 6.1 Checkout-Erstellung
|
||
|
||
- Marketing-Checkout / API:
|
||
- `MarketingController` und `PackageController` nutzen `LemonSqueezyCheckoutService::createCheckout()` (`App\Services\LemonSqueezy\LemonSqueezyCheckoutService`).
|
||
- Der Service:
|
||
- Baut `custom_data` (`tenant_id`, `package_id`, optional `checkout_session_id`) für spätere Zuordnung.
|
||
- Ruft `POST /checkouts` im Lemon Squeezy‑API auf und erhält eine `checkout_url`.
|
||
- Ops-Sicht:
|
||
- Wenn `lemonsqueezy_variant_id` bei einem Package fehlt, wird kein Checkout erzeugt – Marketing‑UI zeigt entsprechende Fehlertexte (siehe `resources/lang/*/marketing.php`).
|
||
- Bei wiederkehrenden „checkout failed“‑Fehlern die Logs (`LemonSqueezyCheckoutService`, Controller) und Package‑Konfiguration prüfen.
|
||
|
||
### 6.2 Webhook-Verarbeitung & Idempotenz
|
||
|
||
- Endpoint: `POST /lemonsqueezy/webhook` → `LemonSqueezyWebhookController::handle()`.
|
||
- Service: `CheckoutWebhookService` (`App\Services\Checkout\CheckoutWebhookService`):
|
||
- Unterscheidet zwischen **Order‑Events** (`order_*`) und **Subscription‑Events** (`subscription_*`).
|
||
- Idempotenz:
|
||
- Nutzt ein Cache‑Lock (`checkout:webhook:lemonsqueezy:{order_id|session_id}`), um parallele Verarbeitung desselben Events zu verhindern.
|
||
- Schreibt Metadaten (`lemonsqueezy_last_event`, `lemonsqueezy_status`, `lemonsqueezy_checkout_id`, `lemonsqueezy_order_id`) in `checkout_sessions.provider_metadata`.
|
||
- Ergebnis:
|
||
- Bei `order_created`/`order_updated` mit Status `paid`:
|
||
- `CheckoutSession` wird als `processing` markiert.
|
||
- `CheckoutAssignmentService::finalise()` weist Paket/Tenant zu.
|
||
- Session wird auf `completed` gesetzt.
|
||
- Bei `order_payment_failed` / `order_refunded`:
|
||
- Session wird auf `failed` gesetzt, Coupons werden als fehlgeschlagen markiert.
|
||
|
||
### 6.2.1 Sandbox‑Webhook registrieren
|
||
|
||
- Command (Test‑Mode aktiv):
|
||
- `php artisan lemonsqueezy:webhooks:register --test-mode`
|
||
- Optional mit URL-Override:
|
||
- `php artisan lemonsqueezy:webhooks:register --url=https://staging.example.com/lemonsqueezy/webhook --test-mode`
|
||
- Event‑Liste kommt aus `config/lemonsqueezy.php` (`webhook_events`). Override möglich:
|
||
- `php artisan lemonsqueezy:webhooks:register --events=order_created --events=subscription_created`
|
||
|
||
### 6.2.2 Verarbeitete Lemon Squeezy-Events
|
||
|
||
Die Webhook‑Handler erwarten mindestens diese Events:
|
||
|
||
- `order_created`
|
||
- `order_updated`
|
||
- `order_payment_failed`
|
||
- `order_refunded`
|
||
- `subscription_created`
|
||
- `subscription_updated`
|
||
- `subscription_cancelled`
|
||
- `subscription_expired`
|
||
- `subscription_paused`
|
||
|
||
### 6.3 Subscriptions & TenantPackages
|
||
|
||
- Subscription‑Events (`subscription_*`) werden ebenfalls von `CheckoutWebhookService` behandelt:
|
||
- Tenant wird aus `metadata.tenant_id` oder `lemonsqueezy_customer_id` ermittelt.
|
||
- Package wird über `metadata.package_id` oder `lemonsqueezy_variant_id` aufgelöst.
|
||
- `TenantPackage` wird erstellt/aktualisiert (`lemonsqueezy_subscription_id`, `expires_at`, `active`).
|
||
- `Tenant.subscription_status` und `subscription_expires_at` werden gesteuert.
|
||
- Ops-Sicht:
|
||
- Bei abweichenden Abostatus (z.B. Lemon Squeezy zeigt „active“, Tenant nicht):
|
||
- Subscription‑Events im Lemon Squeezy‑Dashboard prüfen.
|
||
- Letzte `subscription_*`‑Events in den Logs, `TenantPackage`‑ und `Tenant`‑Felder gegenprüfen.
|
||
|
||
### 6.4 Paket- & Coupon-Synchronisation
|
||
|
||
- Pakete:
|
||
- Artisan‑Command `lemonsqueezy:sync-packages` (`App\Console\Commands\LemonSqueezySyncPackages`) stößt für ausgewählte oder alle Pakete `SyncPackageToLemonSqueezy`/`PullPackageFromLemonSqueezy` Jobs an.
|
||
- Sync‑Jobs nutzen `LemonSqueezyCatalogService`, um Produkte/Preise in Lemon Squeezy zu erstellen/aktualisieren und `lemonsqueezy_product_id`/`lemonsqueezy_variant_id` lokal zu pflegen.
|
||
- Coupons:
|
||
- `SyncCouponToLemonSqueezy`‑Job spiegelt interne Coupon‑Konfiguration in Lemon Squeezy Discounts (`LemonSqueezyDiscountService`).
|
||
- Ops-Sicht:
|
||
- Bei Katalog‑Abweichungen `lemonsqueezy:sync-packages --dry-run` verwenden, um Snapshots zu prüfen, bevor tatsächliche Änderungen gesendet werden.
|
||
- Fehlgeschlagene Syncs in den Logs (`Lemon Squeezy package sync failed`, `Lemon Squeezy addon sync failed`, `Failed syncing coupon to Lemon Squeezy`) beobachten.
|
||
|
||
### 6.5 Recovery-Playbook: Katalog‑Sync fehlgeschlagen
|
||
|
||
Wenn der Katalog‑Sync fehlschlägt oder Pakete nicht mehr korrekt verknüpft sind:
|
||
|
||
1. **Status im Admin prüfen**
|
||
- `PackageResource` → Felder `lemonsqueezy_sync_status`, `lemonsqueezy_synced_at` und `Letzter Fehler`.
|
||
- Fehlermeldung stammt aus `lemonsqueezy_snapshot.error.message`.
|
||
2. **Trockenlauf ausführen**
|
||
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --dry-run`
|
||
- Prüfe die erzeugten Payload‑Snapshots (in `lemonsqueezy_snapshot`) auf falsche IDs/Preise.
|
||
3. **Mapping prüfen**
|
||
- Falls `lemonsqueezy_product_id` oder `lemonsqueezy_variant_id` fehlt oder falsch ist: im Paket‑Admin korrigieren.
|
||
- Bulk‑Sync blockiert unmapped Pakete; gezielte Korrektur vor dem nächsten Lauf ist Pflicht.
|
||
4. **Sync erneut anstoßen**
|
||
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --queue`
|
||
- Bei Bedarf: `--allow-unmapped` nur bewusst verwenden (z.B. initiales Mapping).
|
||
5. **Pull für Abgleich**
|
||
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --pull` zum Abgleich mit Lemon Squeezy.
|
||
6. **Logs prüfen**
|
||
- Erwartete Logeinträge: `Lemon Squeezy package sync failed`, `Lemon Squeezy addon sync failed`, `Failed syncing coupon to Lemon Squeezy`.
|
||
- Achte auf wiederkehrende Fehler (z.B. invalid product/price IDs).
|
||
|
||
Diese Untersektion soll dir als Operator helfen zu verstehen, wie Lemon Squeezy‑Aktionen im System abgebildet sind und an welchen Stellen du im Fehlerfall ansetzen kannst.
|
||
|
||
## 7. Production Cutover: Lemon Squeezy Migration
|
||
|
||
Diese Checkliste beschreibt den kontrollierten Wechsel auf Lemon Squeezy in Produktion.
|
||
|
||
1. **Vorbereitung (T‑1 Woche)**
|
||
- Confirm: `LEMONSQUEEZY_API_KEY`, `LEMONSQUEEZY_STORE_ID`, `LEMONSQUEEZY_WEBHOOK_SECRET`, `LEMONSQUEEZY_TEST_MODE=false`.
|
||
- Package IDs validieren: alle aktiven Packages haben `lemonsqueezy_product_id` und `lemonsqueezy_variant_id`.
|
||
- `lemonsqueezy:sync-packages --dry-run` auf eine Stichprobe anwenden.
|
||
- Event‑Liste prüfen: `config/lemonsqueezy.php` (`webhook_events`).
|
||
2. **Staging Smoke (T‑2 Tage)**
|
||
- `lemonsqueezy:webhooks:register --test-mode` auf Staging ausführen.
|
||
- Testkauf via Lemon Squeezy Test Mode und Webhook Replay verifizieren.
|
||
3. **Cutover Window (T‑0)**
|
||
- Marketing‑Checkout kurz einfrieren (kein Checkout waehrend der Umschaltung).
|
||
- Production Webhook registrieren:
|
||
- `lemonsqueezy:webhooks:register --url=https://<prod-domain>/lemonsqueezy/webhook`
|
||
- Queue worker laufen lassen (Queue: `webhooks`/`billing` sofern konfiguriert).
|
||
4. **Activation**
|
||
- Erstes Produktions‑Checkout ausfuehren.
|
||
- Verify: `checkout_sessions.provider_metadata` wird mit `lemonsqueezy_*` Feldern befuellt.
|
||
- Verify: `TenantPackage` aktiv und `subscription_status` korrekt.
|
||
5. **Rollback (falls notwendig)**
|
||
- Checkout wieder deaktivieren (Marketing‑Checkout ausblenden).
|
||
- Lemon Squeezy Webhook Destination im Lemon Squeezy Dashboard deaktivieren.
|
||
- Status und Logs sichern (Webhooks, `lemonsqueezy-sync` Log).
|
||
6. **Post‑Cutover (T+1)**
|
||
- Stichproben auf neue Tenants/Packages.
|
||
- Monitoring: Fehlgeschlagene Webhooks, Sync‑Fehler, Support Tickets.
|