--- 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= --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= --queue` - Bei Bedarf: `--allow-unmapped` nur bewusst verwenden (z.B. initiales Mapping). 5. **Pull für Abgleich** - `php artisan lemonsqueezy:sync-packages --package= --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:///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.