---
title: Billing & Zahlungs-Operationen
sidebar_label: Billing-Runbook
---

Dieses Runbook beschreibt, wie mit Zahlungsproblemen, Paddle/RevenueCat‑Webhooks und Paket‑Inkonsistenzen operativ umzugehen ist.

## 1. Komponentenüberblick

- **Paddle**
  - Abwicklung von Web‑Checkout, Paketen und Subscriptions.
  - Webhooks für Käufe, 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 Paddle‑Migration und zum Katalog‑Sync.

## 2. Typische Problemszenarien

- **Webhook kommt nicht an / schlägt fehl**
  - Symptom: Paddle zeigt Zahlung „completed“, Tenant‑Paket im Backend bleibt unverändert.
  - Checkliste:
    - Logs der Webhook‑Routes prüfen (Statuscodes, Exceptions).
      - Endpoint: `POST /paddle/webhook` → `PaddleWebhookController::handle()`.
      - Controller ruft `CheckoutWebhookService::handlePaddleEvent()` auf.
    - Webhook‑Replay über das Paddle 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).
  - 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**
    - Paddle‑Dashboard: ist die Zahlung dort korrekt verbucht? (Transaktions‑/Abonnement‑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?)
      - `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.
    - 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.

## 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**
  - Paddle‑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.

## 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 Paddle‑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.

### 6.2 Webhook-Verarbeitung & Idempotenz

- Endpoint: `POST /paddle/webhook` → `PaddleWebhookController::handle()`.
- Service: `CheckoutWebhookService` (`App\Services\Checkout\CheckoutWebhookService`):
  - Unterscheidet zwischen **Transaktions‑Events** (`transaction.*`) und **Subscription‑Events** (`subscription.*`).
  - Idempotenz:
    - Nutzt ein Cache‑Lock (`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 Sandbox‑Webhook 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`
- Event‑Liste 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 Webhook‑Handler erwarten mindestens diese Events:

- `transaction.created`
- `transaction.processing`
- `transaction.completed` (inkl. Add-ons/Gift‑Vouchers)
- `transaction.failed`
- `transaction.cancelled`
- `subscription.created`
- `subscription.updated`
- `subscription.paused`
- `subscription.resumed`
- `subscription.cancelled`
- `subscription.past_due`

### 6.3 Subscriptions & TenantPackages

- 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`).
  - `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.

### 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.
- Coupons:
  - `SyncCouponToPaddle`‑Job spiegelt interne Coupon‑Konfiguration in Paddle Discounts (`PaddleDiscountService`).
- 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.

### 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 `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 Payload‑Snapshots (in `paddle_snapshot`) auf falsche IDs/Preise.
3. **Mapping prüfen**
   - Falls `paddle_product_id` oder `paddle_price_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 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 Paddle‑Aktionen 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 (T‑1 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.
   - Event‑Liste prüfen: `config/paddle.php` (`webhook_events`).
2. **Staging Smoke (T‑2 Tage)**
   - `paddle:webhooks:register --traffic-source=simulation` auf Staging ausfuehren.
   - Testkauf via Paddle Sandbox und Webhook Replay verifizieren.
3. **Cutover Window (T‑0)**
   - Marketing‑Checkout 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 Produktions‑Checkout 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 (Marketing‑Checkout ausblenden).
   - Paddle Webhook Destination im Paddle Dashboard deaktivieren.
   - Status und Logs sichern (Webhooks, `paddle_sync` Log).
6. **Post‑Cutover (T+1)**
   - Stichproben auf neue Tenants/Packages.
   - Monitoring: Fehlgeschlagene Webhooks, Sync‑Fehler, Support Tickets.
