Files
kaffeekasse-saas/docs/billing.md
T
clemensandClaude Sonnet 5 d642814fb5 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>
2026-07-16 23:56:48 +02:00

164 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Preis- und Abrechnungslogik
Stand: 2026-07-16
Geplante Architektur (mit dem Kunden abgestimmt): **Stripe Billing für den
Zahlungseinzug, bestehendes Dolibarr für Rechnungsstellung/Buchhaltung.**
Kein eigenes Rechnungssystem, kein zusätzliches ERP nur für Kaffeeliste
siehe Begründung unten.
## Warum diese Aufteilung
Drei getrennte Probleme:
1. **Rechnung erzeugen** löst Dolibarr (bereits im Einsatz für ein
anderes Business des Kunden), fortlaufend nummeriert, GoBD-konform.
2. **Geld tatsächlich einziehen** der eigentlich schwierige Teil
(wiederkehrende Belastung, Fehlschlag-Wiederholung, Zahlungsmittel-
Verwaltung). Weder ein selbstgebautes System noch Dolibarr allein lösen
das; ein Zahlungsanbieter mit Abo-Logik schon.
3. **Tarif-/Nutzungsstand verwalten** das übernimmt Kaffeeliste selbst,
da die Teilnehmerzahl je Mandant ohnehin schon vorhanden ist.
Empfehlung: **Stripe Billing** statt Mollie, weil eine echte
Abo-Logik mit automatischem Stufenwechsel gebraucht wird (Stripes
Billing-Produkt ist dafür ausgereifter als Mollies Subscriptions-API);
Mollie ist bei SEPA-Lastschrift günstiger, aber weniger auf SaaS-Abos
zugeschnitten.
## Phase 1: Datenmodell und Tariflogik (umgesetzt, keine externen Zugänge nötig)
Umgesetzte Dateien:
```text
database/migrations/0014_saas_tenant_billing.sql
app/billing.php
app/saas-auth.php (Init bei Registrierung)
mandant-einstellungen.php (Anzeige "Abo & Tarif")
app/platform-admin.php, backoffice.php (Tarif-Spalte im Back-Office)
```
- Neue Tabelle `tenant_billing`: `plan_code`, `subscription_status`,
`stripe_customer_id`, `stripe_subscription_id`, `current_period_end`,
`dolibarr_thirdparty_id` (letztere beide vorbereitet für Phase 2/3,
noch ungenutzt).
- `billing_plans()` ist die einzige Quelle für die Tarifstufen
(`free`/`basic`/`plus`/`pro`/`enterprise`), synchron mit den Preisen auf
`preise.php`.
- `billing_required_plan_code()` errechnet anhand der aktiven
Teilnehmerzahl die günstigste ausreichende Stufe.
- `billing_check_tenant()` vergleicht gebuchten mit benötigtem Tarif;
rein informativ **kein Enforcement**, kein Sperren bei Überschreitung.
Solange Stripe nicht angebunden ist, wird bei Bedarf manuell
umgestellt (Hinweistext verweist auf Kontakt per E-Mail).
- Neue Mandanten starten automatisch mit `plan_code = 'free'`.
- Owner/Admin sehen ihren aktuellen Tarif und die aktive Teilnehmerzahl
unter „Abo & Tarif" auf `mandant-einstellungen.php`; bei Bedarf einer
höheren Stufe erscheint ein Hinweis.
- Das Back-Office (`backoffice.php`) zeigt zusätzlich zur Mandantenliste
den gebuchten Tarif und, falls abweichend, den tatsächlich benötigten
Tarif der Betreiber sieht auf einen Blick, wer manuell hochgestuft
werden müsste.
Live getestet: Tarifgrenzen-Berechnung (10/25/50/150) mit einem Mandanten
knapp unter und einem mit 12 Teilnehmern (über dem Freikontingent, korrekt
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 (umgesetzt)
Umgesetzte Dateien:
```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)
Wartet auf Zugang zu einer Dolibarr-Sandbox/Testinstanz (bewusst nicht
direkt gegen die produktive Instanz des Kunden, um deren bestehende
Buchhaltungsdaten nicht zu gefährden). Geplanter Umfang:
- Bei jedem erfolgreichen Stripe-Zahlungsereignis (`invoice.paid`) über
die Dolibarr-REST-API eine Rechnung im bestehenden Dolibarr des Kunden
anlegen (`dolibarr_thirdparty_id` verknüpft den Mandanten mit dem
Dolibarr-Kunden).
- Stripe bleibt alleiniger Zahlungsweg; Dolibarr wird nur als Buchhaltungs-
Spiegel befüllt, nicht selbst zum Auslösen von Zahlungen verwendet.
- Erst nach erfolgreichem Test gegen die Sandbox gegen die produktive
Dolibarr-Instanz freigeben.