- 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 (Paddle/RevenueCat) mit internem Ledger.
- Bei doppelten Buchungen: Prozess definieren (Refund via Paddle, Anpassung im Ledger).
- 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.Ä.
@@ -43,21 +43,21 @@ Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Paddle/RevenueCat‑Webhoo
1. **Event/Tenant identifizieren**
- IDs und relevante Paket‑Infos aus DB/Admin UI holen.
2. **Provider-Status prüfen**
- Paddle‑Dashboard: ist die Zahlung dort korrekt verbucht? (Transaktions‑/Abonnement‑Ansicht).
- 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 = paddle`, `paddle_transaction_id` gefüllt?)
- `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:
- Paddle‑Events erneut senden lassen, ggf. `tests/api/_testing/checkout/sessions/{session}/simulate-paddle` (in Test‑Umgebungen) nutzen.
- 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 Paddle/RevenueCat Migration finalisiert ist.
> TODO: Ergänze konkrete Tabellen-/Modellnamen und die relevanten Jobs/Artisan Commands, sobald Lemon Squeezy/RevenueCat Migration finalisiert ist.
## 4. Zusammenarbeit mit Finance/Support
@@ -69,141 +69,138 @@ Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Paddle/RevenueCat‑Webhoo
## 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.
- 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 Paddle‑Migration abgeschlossen ist.
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 Paddle-Flows im System
## 6. Konkrete Lemon Squeezy-Flows im System
### 6.1 Checkout-Erstellung
- Marketing-Checkout / API:
- `MarketingController` und `PackageController` nutzen `PaddleCheckoutService::createCheckout()` (`App\Services\Paddle\PaddleCheckoutService`).
- `MarketingController` und `PackageController` nutzen `LemonSqueezyCheckoutService::createCheckout()` (`App\Services\LemonSqueezy\LemonSqueezyCheckoutService`).
- 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 Paddle‑API auf und erhält eine `checkout_url`.
- 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 `paddle_price_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 (`PaddleCheckoutService`, Controller) und Package‑Konfiguration prüfen.
- 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.
- Subscription‑Events (`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`).
- 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. Paddle zeigt „active“, Tenant nicht):
- Subscription‑Events im Paddle‑Dashboard prüfen.
- Letzte `subscription.*`‑Events in den Logs, `TenantPackage`‑ und `Tenant`‑Felder gegenprüfen.
- 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 `paddle:sync-packages` (`App\Console\Commands\PaddleSyncPackages`) stößt für ausgewählte oder alle Pakete `SyncPackageToPaddle`/`PullPackageFromPaddle` Jobs an.
- Sync‑Jobs nutzen `PaddleCatalogService`, um Produkte/Preise in Paddle zu erstellen/aktualisieren und `paddle_product_id`/`paddle_price_id` lokal zu pflegen.
- 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:
- `SyncCouponToPaddle`‑Job spiegelt interne Coupon‑Konfiguration in Paddle Discounts (`PaddleDiscountService`).
- `SyncCouponToLemonSqueezy`‑Job spiegelt interne Coupon‑Konfiguration in Lemon Squeezy Discounts (`LemonSqueezyDiscountService`).
- Ops-Sicht:
- Bei Katalog‑Abweichungen `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.
- 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.
- Achte auf wiederkehrende Fehler (z.B. invalid product/price IDs).
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.
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: Paddle Migration
## 7. Production Cutover: Lemon Squeezy Migration
Diese Checkliste beschreibt den kontrollierten Wechsel auf Paddle in Produktion.
Diese Checkliste beschreibt den kontrollierten Wechsel auf Lemon Squeezy in Produktion.
- Betroffenes Paket (Name oder Beschreibung, z.B. „Pro‑Paket 79 €“).
- Zeitpunkt der Zahlung (Datum/Uhrzeit, ggf. Screenshot).
- Ggf. Auszug aus der Paddle‑Bestätigung (ohne vollständige Kartendaten!).
- Ggf. Auszug aus der Lemon Squeezy‑Bestätigung (ohne vollständige Kartendaten!).
Diese Infos erlauben dir, die korrekte Transaktion sowohl in Paddle als auch im Backend zu finden.
Diese Infos erlauben dir, die korrekte Transaktion sowohl in Lemon Squeezy als auch im Backend zu finden.
## 2. Paddle-Status prüfen
## 2. Lemon Squeezy-Status prüfen
1. Im Paddle‑Dashboard:
- Suche nach E‑Mail, Tenant‑Name oder dem vom Tenant genannten Transaktions‑Identifier.
- Stelle sicher, dass die Zahlung dort als „completed“/„paid“ markiert ist.
1. Im Lemon Squeezy‑Dashboard:
- Suche nach E‑Mail, Tenant‑Name oder der vom Tenant genannten Order‑ID.
- Stelle sicher, dass die Zahlung dort als „paid“/„completed“ markiert ist.
2. Notiere:
- Paddle‑Transaction‑ID und ggf. Checkout‑ID.
- Lemon Squeezy‑Order‑ID und ggf. Checkout‑ID.
- Status (paid/processing/failed/cancelled).
Wenn Paddle die Zahlung nicht als erfolgreich zeigt, ist dies primär ein Finance‑/Customer‑Topic – ggf. mit Customer Support klären, ob eine neue Zahlung oder Klärung mit dem Kunden notwendig ist.
Wenn Lemon Squeezy die Zahlung nicht als erfolgreich zeigt, ist dies primär ein Finance‑/Customer‑Topic – ggf. mit Customer Support klären, ob eine neue Zahlung oder Klärung mit dem Kunden notwendig ist.
## 3. Backend-Status prüfen
Mit bestätigter Zahlung in Paddle:
Mit bestätigter Zahlung in Lemon Squeezy:
1. `checkout_sessions`:
- Suche nach Sessions des Tenants (`tenant_id`) mit dem betroffenen `package_id`:
- Achte auf `status` (erwartet `completed`) und `provider = paddle`.
- Prüfe `provider_metadata` auf `paddle_last_event`, `paddle_status`, `paddle_checkout_id`.
- Wenn du die Session über Paddle‑Metadaten finden möchtest:
- `paddle_checkout_id`aus dem Webhook/Provider‑Metadata oder `transaction_id` verwenden.
- Achte auf `status` (erwartet `completed`) und `provider = lemonsqueezy`.
- Prüfe `provider_metadata` auf `lemonsqueezy_last_event`, `lemonsqueezy_status`, `lemonsqueezy_checkout_id`, `lemonsqueezy_order_id`.
- Wenn du die Session über Lemon Squeezy‑Metadaten finden möchtest:
- Prüfe, ob ein Eintrag für `(tenant_id, package_id)` mit passender Provider‑Referenz existiert:
- z.B. `provider = 'paddle'`, `provider_id` = Transaction‑ID.
- z.B. `provider = 'lemonsqueezy'`, `provider_id` = Order‑ID.
3. `tenant_packages`:
- Prüfe, ob es einen aktiven Eintrag für `(tenant_id, package_id)` gibt:
- `active = 1`, `expires_at` in der Zukunft.
@@ -51,34 +51,34 @@ Wenn `checkout_sessions` noch nicht auf `completed` steht oder `tenant_packages`
1. Logs prüfen:
- `storage/logs/laravel.log` und ggf. `billing`‑Channel.
- Suche nach Einträgen von `PaddleWebhookController` / `CheckoutWebhookService` rund um den Zahlungszeitpunkt.
- Suche nach Einträgen von `LemonSqueezyWebhookController` / `CheckoutWebhookService` rund um den Zahlungszeitpunkt.
2. Typische Ursachen:
- Webhook nicht zugestellt (Netzwerk/SSL).
- Webhook konnte die Session nicht auflösen (`[CheckoutWebhook] Paddle session not resolved`).
- Idempotenz‑Lock (`Paddle lock busy`) hat dazu geführt, dass Event nur teilweise verarbeitet wurde.
- Webhook konnte die Session nicht auflösen (`[CheckoutWebhook] Lemon Squeezy session not resolved`).
- Idempotenz‑Lock (`Lemon Squeezy lock busy`) hat dazu geführt, dass Event nur teilweise verarbeitet wurde.
## 5. Korrektur-Schritte
### 5.1 Automatischer Replay (empfohlen)
1. Im Paddle‑Dashboard:
- Den betreffenden `transaction.*`‑Event finden.
1. Im Lemon Squeezy‑Dashboard:
- Den betreffenden `order_*`‑Event finden.
- Webhook‑Replay auslösen.
2. In den Logs beobachten:
- Ob `CheckoutWebhookService::handlePaddleEvent()` diesmal die Session findet und `CheckoutAssignmentService::finalise()` ausführt.
- Ob `CheckoutWebhookService::handleLemonSqueezyEvent()` diesmal die Session findet und `CheckoutAssignmentService::finalise()` ausführt.
3. Nochmal `checkout_sessions` und `tenant_packages` prüfen:
- Session sollte auf `completed` stehen, Paket aktiv sein.
### 5.2 Manuelle Korrektur (Notfall)
Nur anwenden, wenn klare Freigabe vorliegt und Paddle die Zahlung eindeutig als erfolgreich listet.
Nur anwenden, wenn klare Freigabe vorliegt und Lemon Squeezy die Zahlung eindeutig als erfolgreich listet.
1. `tenant_packages` aktualisieren:
- Entweder neuen Eintrag anlegen oder bestehenden für `(tenant_id, package_id)` so setzen, dass:
- `active = 1`,
- `purchased_at` und `expires_at` zu Paddle‑Daten passen.
- `active = 1`,
- `purchased_at` und `expires_at` zu Lemon Squeezy‑Daten passen.
2. `package_purchases` ergänzen:
- Sicherstellen, dass die Zahlung als Zeile mit `provider = 'paddle'`, `provider_id = Transaction‑ID` und passender `price` existiert (für spätere Audits).
- Sicherstellen, dass die Zahlung als Zeile mit `provider = 'lemonsqueezy'`, `provider_id = Order‑ID` und passender `price` existiert (für spätere Audits).
3. Konsistenz prüfen:
- Admin UI für Tenant öffnen und prüfen, ob Limits/Paketstatus jetzt korrekt angezeigt werden.
4. Dokumentation:
@@ -88,7 +88,7 @@ Nur anwenden, wenn klare Freigabe vorliegt und Paddle die Zahlung eindeutig als
- Sobald der Backend‑Status korrigiert ist:
- Kurz bestätigen, dass das Paket aktiv ist und welche Auswirkungen das hat (z.B. neue Limits, verlängerte Galerie).
- Falls Paddle die Zahlung nicht als erfolgreich führt:
- Falls Lemon Squeezy die Zahlung nicht als erfolgreich führt:
- Ehrlich kommunizieren, dass laut Zahlungsprovider noch keine endgültige Zahlung vorliegt und welche Optionen es gibt (z.B. neue Zahlung, Klärung mit Bank/Kreditkarte).
Dieses How‑to sollte dem Support/On‑Call helfen, den gängigsten Billing‑Fehlerfall strukturiert abzuarbeiten. Für tiefere Ursachenanalysen siehe `docs/ops/billing-ops.md`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.