Billing Phase 2: Stripe Checkout, Kundenportal und Webhook-Verarbeitung

app/stripe.php: minimaler REST-Client fuer die Stripe-API auf Basis von
PHP-Streams statt eines vendorten SDKs - diese PHP-Installation hat keine
curl-Extension, Streams sind zudem portabler und brauchen kein
composer.json.

- Fuer jeden bezahlten Tarif per API ein Stripe-Produkt mit monatlichem
  Preis angelegt, referenziert ueber einen stabilen lookup_key statt
  hartcodierter Price-ID; Erstellung ist idempotent.
- abo-upgrade.php: erstellt eine Stripe-Checkout-Session fuer den
  gewaehlten Tarif, tenant_id/plan_code als Metadaten auf Session UND
  Subscription (damit spaetere Subscription-Events zuordenbar bleiben).
- abo-portal.php: oeffnet das Stripe Customer Portal fuer bestehende
  Kunden (Zahlungsmittel/Kuendigung, ohne eigene UI dafuer).
- stripe-webhook.php: verifiziert die Stripe-Signature per HMAC-SHA256
  mit Zeitstempel-Toleranz, verarbeitet checkout.session.completed,
  customer.subscription.updated/.deleted, invoice.paid/.payment_failed
  und haelt tenant_billing aktuell. Bewusst kein CSRF-/Login-Check
  (Stripe ruft unauthentifiziert auf), stattdessen ausschliesslich
  Signaturpruefung als Echtheitsnachweis.
- mandant-einstellungen.php: echter "Jetzt upgraden"-Button (Stripe
  Checkout) sowie "Zahlungsmethode verwalten/Abo kuendigen" (Customer
  Portal), sobald ein Stripe-Kunde existiert.

Live getestet (Stripe-Testmodus, kein echtes Geld): voller Checkout-Flow
bis zum echten Redirect auf checkout.stripe.com, Webhook-Verarbeitung
durch selbst erzeugte, korrekt signierte Test-Events (da diese
Dev-Umgebung keine oeffentlich erreichbare URL fuer echte Stripe-
Zustellung hat) inklusive Ablehnung falscher Signaturen, Customer Portal
mit echtem per API angelegtem Test-Kunden. Alle Regressionstests
weiterhin gruen (36 Seiten HTTP-Smoke, Golden Master, M8-Isolation/
Rollenmatrix).

Noch offen: echten Webhook-Endpunkt auf die Produktions-Domain eintragen,
sobald diese feststeht; Wechsel auf Live-Keys; Dolibarr-Sync (Phase 3).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 23:56:48 +02:00
co-authored by Claude Sonnet 5
parent ade1fd0ed9
commit d642814fb5
8 changed files with 618 additions and 20 deletions
+78 -11
View File
@@ -66,19 +66,86 @@ als „basic" erkannt) geprüft; UI-Anzeige in Mandant-Einstellungen und
Back-Office live verifiziert. Alle bestehenden M8-Isolationstests und der
Rollen-Matrix-Test bleiben unverändert grün.
## Phase 2: Stripe-Anbindung (noch offen)
## Phase 2: Stripe-Anbindung (umgesetzt)
Wartet auf Stripe-Test-API-Keys vom Kunden. Geplanter Umfang:
Umgesetzte Dateien:
- Stripe Checkout zum Abschließen/Wechseln eines bezahlten Tarifs.
- Webhook-Endpunkt für `checkout.session.completed`,
`customer.subscription.updated`/`.deleted`, `invoice.paid`/`.payment_failed`,
der `tenant_billing` aktuell hält.
- Stripe Customer Portal für Zahlungsmittel-Verwaltung/Kündigung durch
den Kunden selbst.
- Automatischer Stufenwechsel: Sobald `billing_check_tenant()` einen
höheren Bedarf erkennt, Wechsel der Stripe-Subscription auslösen
(mit Proration) statt nur anzuzeigen.
```text
app/stripe.php
app/billing.php (Stripe-Preis-Auflösung ergänzt)
abo-upgrade.php
abo-portal.php
stripe-webhook.php
mandant-einstellungen.php (Upgrade-/Portal-Buttons)
```
- `app/stripe.php`: minimaler REST-Client für die Stripe-API auf Basis von
PHP-Streams (`file_get_contents` + `stream_context_create`), **kein**
vendortes SDK und **kein** composer.json nötig diese PHP-Installation
hat ohnehin keine curl-Extension, Streams sind portabler.
- Für jeden bezahlten Tarif (`basic`/`plus`/`pro`) wurde per API ein
Stripe-Produkt mit monatlichem Preis angelegt, referenziert über einen
stabilen `lookup_key` (`kaffeeliste_basic`/`_plus`/`_pro`) statt einer
hartcodierten Price-ID `stripe_find_or_create_price()` ist idempotent,
ein erneuter Aufruf legt nichts doppelt an.
- `abo-upgrade.php`: erstellt eine Stripe-Checkout-Session (Modus
`subscription`) für den vom Kunden gewählten Tarif und leitet dorthin
weiter. `tenant_id`/`plan_code` werden sowohl auf der Checkout-Session
als auch auf der Subscription selbst als Metadaten gesetzt, damit spätere
Subscription-Events (die kein Checkout-Session-Objekt mehr enthalten)
trotzdem einem Mandanten zugeordnet werden können.
- `abo-portal.php`: öffnet das Stripe Customer Portal für den bestehenden
Stripe-Kunden (Zahlungsmittel ändern, Abo kündigen) Self-Service ohne
eigene UI dafür.
- `stripe-webhook.php`: verifiziert die `Stripe-Signature` nach dem von
Stripe dokumentierten HMAC-SHA256-Schema (inklusive Zeitstempel-Toleranz
gegen Replay), verarbeitet `checkout.session.completed`,
`customer.subscription.updated`/`.deleted`,
`invoice.paid`/`.payment_failed` und hält `tenant_billing` aktuell.
Bewusst kein `app_require_csrf()`/`saas_require_login()` (Stripe ruft
unauthentifiziert von außen auf), stattdessen ausschließlich die
Signaturprüfung als Echtheitsnachweis.
- `mandant-einstellungen.php`: zeigt bei Bedarf einen echten
„Jetzt upgraden"-Button (führt zu Stripe Checkout) sowie, sobald ein
Stripe-Kunde existiert, „Zahlungsmethode verwalten / Abo kündigen"
(führt zum Customer Portal).
**Noch nicht umgesetzt:** automatischer Stufenwechsel bei wachsender
Teilnehmerzahl (aktuell muss der Kunde selbst erneut auf „Upgraden"
klicken, wenn `billing_check_tenant()` einen höheren Bedarf anzeigt).
### Live getestet (Testmodus, kein echtes Geld)
- Checkout-Session-Erstellung direkt über die API sowie über den vollen
Seiten-Flow (Login → Mandant-Einstellungen zeigt Upgrade-Button bei 13
Teilnehmern → Klick → echter 302-Redirect zu einer
`checkout.stripe.com`-URL).
- Webhook-Verarbeitung durch selbst erzeugte, korrekt signierte
Test-Ereignisse (da diese Dev-Umgebung keine öffentlich erreichbare URL
für echte Stripe-Zustellung hat): `checkout.session.completed` setzt
`tenant_billing` korrekt (Tarif, Status, Stripe-IDs) und wird im
Mandanten-Audit-Log protokolliert; `customer.subscription.deleted`
setzt den Mandanten korrekt auf `free`/`canceled` zurück.
- Signaturprüfung: eine Anfrage mit falscher Signatur wird korrekt mit
`400` abgelehnt.
- Customer Portal: mit einem echten, per API angelegten Stripe-Test-Kunden
liefert `abo-portal.php` einen echten 302-Redirect zu
`billing.stripe.com`.
- Alle bestehenden Regressionstests (Golden Master, M8-Isolation,
M8-Rollenmatrix, HTTP-Smoke mit 36 Seiten) weiterhin grün.
- Alle Testdaten (Stripe-Testobjekte kosten nichts und wurden im
Stripe-Testmodus belassen; lokale DB-Testdaten wurden entfernt)
aufgeräumt.
### Für den Produktivbetrieb noch zu tun
- Echten Webhook-Endpunkt im Stripe-Dashboard (oder per API) auf die
produktive Domain (`https://app.kaffeeliste.de/stripe-webhook.php`)
eintragen und das dortige Signing-Secret als `STRIPE_WEBHOOK_SECRET`
hinterlegen das aktuelle Secret ist nur ein lokal erzeugter Platzhalter
für die Simulation in dieser Dev-Umgebung.
- Von Test- auf Live-API-Keys wechseln, sobald ein echter Stripe-Account
mit Bankverbindung eingerichtet ist.
## Phase 3: Dolibarr-Anbindung (noch offen)