Migrate billing from Paddle to Lemon Squeezy

This commit is contained in:
Codex Agent
2026-02-03 10:59:54 +01:00
parent 2f4ebfefd4
commit a0ef90e13a
228 changed files with 4369 additions and 4067 deletions

View File

@@ -14,7 +14,7 @@ Internal troubleshooting guide for superadmins and on-call.
- **Guest cannot join**: confirm event is published and the join link is current.
## Billing and quota blocks
- Check Paddle / RevenueCat status dashboards.
- Check Lemon Squeezy / RevenueCat status dashboards.
- Confirm webhook freshness and retry failures if needed.
## Communications

View File

@@ -3,33 +3,33 @@ title: Billing & Zahlungs-Operationen
sidebar_label: Billing-Runbook
---
Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Paddle/RevenueCatWebhooks und PaketInkonsistenzen operativ umzugehen ist.
Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Lemon Squeezy/RevenueCatWebhooks und PaketInkonsistenzen operativ umzugehen ist.
## 1. Komponentenüberblick
- **Paddle**
- **Lemon Squeezy**
- Abwicklung von WebCheckout, Paketen und Subscriptions.
- Webhooks für Käufe, Verlängerungen, Stornos.
- Webhooks für Bestellungen, 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.
> Details zur Architektur findest du in den PRPKapiteln (Billing/Freemium) sowie in den bd-Issues zur Lemon SqueezyMigration und zum KatalogSync.
## 2. Typische Problemszenarien
- **Webhook kommt nicht an / schlägt fehl**
- Symptom: Paddle zeigt Zahlung „completed“, TenantPaket im Backend bleibt unverändert.
- Symptom: Lemon Squeezy zeigt Zahlung „completed/paid“, 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).
- Endpoint: `POST /lemonsqueezy/webhook``LemonSqueezyWebhookController::handle()`.
- Controller ruft `CheckoutWebhookService::handleLemonSqueezyEvent()` auf.
- WebhookReplay über das Lemon Squeezy 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).
- Abgleich von ZahlungsproviderDaten (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/RevenueCatWebhoo
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).
- Lemon SqueezyDashboard: ist die Zahlung dort korrekt verbucht? (Order/SubscriptionAnsicht).
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?)
- `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 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.
- Lemon SqueezyEvents erneut senden lassen, ggf. `tests/api/_testing/checkout/sessions/{session}/simulate-lemonsqueezy` (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.
> 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/RevenueCatWebhoo
## 5. Hinweise zur Implementierung
- **Konfiguration**
- PaddleKeys, WebhookSecrets und FeatureFlags sollten ausschließlich in `.env`/ConfigDateien liegen und niemals im Code/Logs landen.
- Lemon SqueezyKeys, 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.
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 Lemon SqueezyMigration 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 PaddleAPI 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 SqueezyAPI 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.
- Wenn `lemonsqueezy_variant_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 (`LemonSqueezyCheckoutService`, Controller) und PackageKonfiguration prüfen.
### 6.2 Webhook-Verarbeitung & Idempotenz
- Endpoint: `POST /paddle/webhook``PaddleWebhookController::handle()`.
- Endpoint: `POST /lemonsqueezy/webhook``LemonSqueezyWebhookController::handle()`.
- Service: `CheckoutWebhookService` (`App\Services\Checkout\CheckoutWebhookService`):
- Unterscheidet zwischen **TransaktionsEvents** (`transaction.*`) und **SubscriptionEvents** (`subscription.*`).
- Unterscheidet zwischen **OrderEvents** (`order_*`) 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`.
- Nutzt ein CacheLock (`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 `transaction.completed`:
- 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 `transaction.failed` / `transaction.cancelled`:
- Bei `order_payment_failed` / `order_refunded`:
- 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`
- Command (TestMode aktiv):
- `php artisan lemonsqueezy:webhooks:register --test-mode`
- 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`
- `php artisan lemonsqueezy:webhooks:register --url=https://staging.example.com/lemonsqueezy/webhook --test-mode`
- EventListe 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 Paddle-Events
### 6.2.2 Verarbeitete Lemon Squeezy-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`
- `order_created`
- `order_updated`
- `order_payment_failed`
- `order_refunded`
- `subscription_created`
- `subscription_updated`
- `subscription_cancelled`
- `subscription_expired`
- `subscription_paused`
### 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`).
- SubscriptionEvents (`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):
- SubscriptionEvents im PaddleDashboard 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):
- SubscriptionEvents im Lemon SqueezyDashboard 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.
- ArtisanCommand `lemonsqueezy:sync-packages` (`App\Console\Commands\LemonSqueezySyncPackages`) stößt für ausgewählte oder alle Pakete `SyncPackageToLemonSqueezy`/`PullPackageFromLemonSqueezy` Jobs an.
- SyncJobs 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 CouponKonfiguration in Paddle Discounts (`PaddleDiscountService`).
- `SyncCouponToLemonSqueezy`Job spiegelt interne CouponKonfiguration in Lemon Squeezy Discounts (`LemonSqueezyDiscountService`).
- 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.
- Bei KatalogAbweichungen `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: 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`.
- `PackageResource` → Felder `lemonsqueezy_sync_status`, `lemonsqueezy_synced_at` und `Letzter Fehler`.
- Fehlermeldung stammt aus `lemonsqueezy_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.
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --dry-run`
- Prüfe die erzeugten PayloadSnapshots (in `lemonsqueezy_snapshot`) auf falsche IDs/Preise.
3. **Mapping prüfen**
- Falls `paddle_product_id` oder `paddle_price_id` fehlt oder falsch ist: im PaketAdmin korrigieren.
- Falls `lemonsqueezy_product_id` oder `lemonsqueezy_variant_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`
- `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 paddle:sync-packages --package=<id|slug> --pull` zum Abgleich mit Paddle.
- `php artisan lemonsqueezy:sync-packages --package=<id|slug> --pull` zum Abgleich mit Lemon Squeezy.
6. **Logs prüfen**
- Erwartete Logeinträge: `Paddle package sync failed`, `Paddle addon sync failed`, `Paddle discount sync failed`.
- 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 PaddleAktionen im System abgebildet sind und an welchen Stellen du im Fehlerfall ansetzen kannst.
Diese Untersektion soll dir als Operator helfen zu verstehen, wie Lemon SqueezyAktionen 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.
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`).
- 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.
- EventListe prüfen: `config/lemonsqueezy.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.
- `lemonsqueezy:webhooks:register --test-mode` auf Staging ausführen.
- Testkauf via Lemon Squeezy Test Mode 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`
- `lemonsqueezy:webhooks:register --url=https://<prod-domain>/lemonsqueezy/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: `checkout_sessions.provider_metadata` wird mit `lemonsqueezy_*` 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).
- Lemon Squeezy Webhook Destination im Lemon Squeezy Dashboard deaktivieren.
- Status und Logs sichern (Webhooks, `lemonsqueezy-sync` Log).
6. **PostCutover (T+1)**
- Stichproben auf neue Tenants/Packages.
- Monitoring: Fehlgeschlagene Webhooks, SyncFehler, Support Tickets.

View File

@@ -30,13 +30,12 @@ flowchart LR
flowchart LR
Tenant[Browser Tenant-Admin] -->|Paket wählen| App[Laravel App]
App -->|CheckoutSession anlegen| DB[(DB: checkout_sessions,\n tenant_packages)]
App -->|Redirect| Paddle[Paddle Checkout]
App -->|Redirect| LemonSqueezy[Lemon Squeezy Checkout]
Paddle -->|Zahlung erfolgreich| Webhook[Paddle Webhook Endpoint]
LemonSqueezy -->|Zahlung erfolgreich| Webhook[Lemon Squeezy Webhook Endpoint]
Webhook -->|Event verarbeiten| BillingService[CheckoutWebhookService]
BillingService -->|TenantPackage aktualisieren| DB
DB --> App
App --> Tenant
```

View File

@@ -34,7 +34,7 @@ Dieses Howto beschreibt, wie du für einen Tenant kurz vor Vertragsende einen
## 4. Billing-Unterlagen
- Rechnungen / Zahlungsbelege:
- PaddleBelege (Links oder PDFs).
- Lemon SqueezyBelege (Links oder PDFs).
- Interne RechnungsPDFs (falls generiert).
## 5. Nach dem Export
@@ -49,4 +49,3 @@ Siehe auch:
- `docs/ops/compliance-dsgvo-ops.md`
- `docs/ops/backup-restore.md`

View File

@@ -13,34 +13,34 @@ Bevor du nachschaust:
- TenantID oder TenantSlug.
- Betroffenes Paket (Name oder Beschreibung, z.B. „ProPaket 79 €“).
- Zeitpunkt der Zahlung (Datum/Uhrzeit, ggf. Screenshot).
- Ggf. Auszug aus der PaddleBestätigung (ohne vollständige Kartendaten!).
- Ggf. Auszug aus der Lemon SqueezyBestä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 PaddleDashboard:
- Suche nach EMail, TenantName oder dem vom Tenant genannten TransaktionsIdentifier.
- Stelle sicher, dass die Zahlung dort als „completed“/„paid“ markiert ist.
1. Im Lemon SqueezyDashboard:
- Suche nach EMail, TenantName oder der vom Tenant genannten OrderID.
- Stelle sicher, dass die Zahlung dort als „paid“/„completed“ markiert ist.
2. Notiere:
- PaddleTransactionID und ggf. CheckoutID.
- Lemon SqueezyOrderID und ggf. CheckoutID.
- Status (paid/processing/failed/cancelled).
Wenn Paddle die Zahlung nicht als erfolgreich zeigt, ist dies primär ein Finance/CustomerTopic 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/CustomerTopic 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 PaddleMetadaten finden möchtest:
- `paddle_checkout_id` aus dem Webhook/ProviderMetadata 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 SqueezyMetadaten finden möchtest:
- `lemonsqueezy_checkout_id` oder `lemonsqueezy_order_id` verwenden.
2. `package_purchases`:
- Prüfe, ob ein Eintrag für `(tenant_id, package_id)` mit passender ProviderReferenz existiert:
- z.B. `provider = 'paddle'`, `provider_id` = TransactionID.
- z.B. `provider = 'lemonsqueezy'`, `provider_id` = OrderID.
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`).
- IdempotenzLock (`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`).
- IdempotenzLock (`Lemon Squeezy lock busy`) hat dazu geführt, dass Event nur teilweise verarbeitet wurde.
## 5. Korrektur-Schritte
### 5.1 Automatischer Replay (empfohlen)
1. Im PaddleDashboard:
- Den betreffenden `transaction.*`Event finden.
1. Im Lemon SqueezyDashboard:
- Den betreffenden `order_*`Event finden.
- WebhookReplay 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 PaddleDaten passen.
- `active = 1`,
- `purchased_at` und `expires_at` zu Lemon SqueezyDaten passen.
2. `package_purchases` ergänzen:
- Sicherstellen, dass die Zahlung als Zeile mit `provider = 'paddle'`, `provider_id = TransactionID` und passender `price` existiert (für spätere Audits).
- Sicherstellen, dass die Zahlung als Zeile mit `provider = 'lemonsqueezy'`, `provider_id = OrderID` 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 BackendStatus 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 Howto sollte dem Support/OnCall helfen, den gängigsten BillingFehlerfall strukturiert abzuarbeiten. Für tiefere Ursachenanalysen siehe `docs/ops/billing-ops.md`.

View File

@@ -49,7 +49,7 @@ Nutze bei PublicAPIProblems zusätzlich das `docs/ops/deployment/public-ap
- Fälle: FTP nicht erreichbar, Ingest nicht laufend, falsche Credentials, SecurityVorfälle.
- **Abrechnung & Billing**
- Siehe `docs/ops/billing-ops.md`.
- Fälle: Paddle/RevenueCatWebhookFehler, falsche PaketZustände, doppelte/fehlende Buchungen.
- Fälle: Lemon Squeezy/RevenueCatWebhookFehler, falsche PaketZustände, doppelte/fehlende Buchungen.
Dieses Dokument verweist immer nur auf die jeweils tieferen Runbooks bei konkreten Problemen gehst du dort in die Details.

View File

@@ -20,7 +20,7 @@ Dieses Dokument sammelt die wichtigsten MonitoringPunkte der Plattform und so
- HTTP 5xx/4xx spitzenweise, gruppiert nach Route/Service.
- ApplikationsLogs mit Error/WarningLevel.
- **Billing & Webhooks**
- Fehlgeschlagene Paddle/RevenueCatWebhooks.
- Fehlgeschlagene Lemon Squeezy/RevenueCatWebhooks.
- Differenz zwischen erwarteten und verarbeiteten Zahlungen (optional).
## 2. Werkzeuge & Quellen
@@ -134,7 +134,7 @@ Nachfolgend beispielhafte Formulierungen, wie Alerts unabhängig vom verwendeten
### 6.4 Billing-Webhook Alert
**Ziel**: Fehler bei Paddle/RevenueCatWebhookVerarbeitung erkennen.
**Ziel**: Fehler bei Lemon Squeezy/RevenueCatWebhookVerarbeitung erkennen.
- Bedingung:
- Mehr als 10 fehlgeschlagene WebhookVerarbeitungen innerhalb von 10 Minuten, oder Verhältnis `failed/success` > 0,2.

View File

@@ -34,7 +34,7 @@ Dieser Spickzettel ist für OnCallPersonen gedacht, die im Incident schnel
- APIFehlerRate (5xx, 4xx für Public API).
- QueueBacklog (`default`, `media-storage`, `media-security`, `notifications`).
- ResponseTime Guest/TenantPWA.
- PaddleWebhookFehler (falls im Monitoring abgebildet).
- Lemon SqueezyWebhookFehler (falls im Monitoring abgebildet).
> Ergänze hier konkrete Links zu euren Grafana/DatadogDashboards, sobald diese stabil sind.
@@ -45,4 +45,3 @@ Dieser Spickzettel ist für OnCallPersonen gedacht, die im Incident schnel
- SEV3: Einzelne Tenants oder Funktionen, Workaround vorhanden.
Siehe auch `docs/ops/incidents-major.md` für detaillierte SEVDefinitionen und Kommunikationsregeln.

View File

@@ -17,7 +17,7 @@ Ziel ist, dass du von hier aus schnell zu den relevanten Runbooks und Referenzen
- Laravel App + Nginx + Redis + MySQL.
- AsyncPipeline: Queues (`default`, `media-storage`, `media-security`, `notifications`) und Horizon.
- Satelliten: PhotoboothFTP + ControlService, DocsSite (`/internal-docs`), Monitoring/Dokploy.
- Externe Dienste: Paddle (Billing), RevenueCat (MobileAbos), EMail Provider, Logging/Monitoring (Loki/Grafana o.ä.).
- Externe Dienste: Lemon Squeezy (Billing), RevenueCat (MobileAbos), EMail Provider, Logging/Monitoring (Loki/Grafana o.ä.).
> TODO: Ergänze ein Architekturdiagramm aus Sicht des Betriebs (z.B. als PNG oder PlantUML) und verlinke es hier.
@@ -61,7 +61,7 @@ Diese Seiten sollen praktische Steuerung über TenantAdmins und die GuestE
- **ComplianceTools:** DSGVOExportRequests und RetentionOverrides pro Tenant/Event.
- **SuperadminAuditLog:** Jede AdminAktion nachvollziehbar (ohne PIIPayloads).
- **TenantAnnouncements:** Zielgerichtete Hinweise/ReleaseNotes an TenantAdmins, inkl. Zeitplanung.
- **IntegrationsHealth:** StatusBoard für Paddle/RevenueCat/Webhooks inkl. Störungen.
- **IntegrationsHealth:** StatusBoard für Lemon Squeezy/RevenueCat/Webhooks inkl. Störungen.
## 2. Deployments & Infrastruktur

View File

@@ -32,7 +32,7 @@ Dieses Dokument beschreibt, welche Informationen der Support einsammeln sollte,
- Zeitpunkt der letzten sichtbaren Fotos.
- Ob die Photobooth selbst Fehler anzeigt.
- **Paket nicht aktiv / Limits falsch**
- Bestellnummer / PaddleCheckoutID (falls vorhanden).
- Bestellnummer / Lemon SqueezyCheckoutID (falls vorhanden).
- Zeitpunkt der Zahlung.
- Welches Paket wurde erwartet?
@@ -47,4 +47,3 @@ Siehe auch:
- `docs/ops/oncall-roles.md`
- `docs/ops/oncall-cheatsheet.md`