wiki/b-reference/platform-core/billing.md
2026-06-02 09:42:12 +02:00

181 lines
13 KiB
Markdown
Raw Permalink 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.

<!-- status: canonical -->
<!-- lastReviewed: 2026-04-05 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-05) -->
# Billing & Subscriptions
## Überblick
Die Plattform trennt zwei Ebenen:
1. **Usage-Billing (LLM-Verbrauch):** Prepaid-Guthaben in CHF, Buchung pro AI-Operation (`priceCHF`), Transaktionen mit Mandanten- und Provider-Kontext. Zentrale Erfassung über Billing-Interfaces und `serviceBilling`; Integration im AI-Pfad (`mainServiceAi`).
2. **Mandanten-Subscription (SaaS-Lizenz):** Wiederkehrende Pläne, Kapazitäten (User-Seats, Feature-Instanzen), Stripe für Plattformgebühr und Zahlungsstatus. Vorgesehen als eigener Domänen-Service `serviceSubscription`, gekoppelt an Billing-Checks.
**Gateway-Audit (2026-03-29):** Produktseitig liegt der Schwerpunkt auf **PREPAY_MANDATE** (gemeinsames Mandantenkonto); Billing-Checks und Datenvolumen-Assertions laufen vor AI-Calls. Ergänzend beschreiben die Konzeptdokumente Subscription-Pflicht, Stripe und eine explizite Zustandsmaschine.
Ohne gültige, zahlungswirksame Subscription (soweit implementiert) sollen **nur AI-Calls** blockiert werden; Lesen, Navigation und Datenhaltung bleiben möglich. `Mandate.enabled` bleibt für technische Sperren; Subscription steuert die geschäftliche Nutzbarkeit von KI-Funktionen.
---
## Billing-Modell
### Abrechnungseinheit und Hierarchie
- **Mandant** ist die zentrale Kostenstelle; Benutzer können mehreren Mandanten angehören — Verbrauch wird pro Mandant getrennt erfasst.
- Kosten entstehen an **AICore-Connectoren** (z. B. `anthropic`, `openai`, `perplexity`, `tavily`, `internal`); die erlaubte Provider-Liste ist dynamisch (Model Registry / Plugins).
### Abrechnungsmodell
Im Gateway gibt es aktuell **kein `billingModel`-Feld** auf `BillingSettings`. Das operative Modell ist faktisch **PREPAY_MANDATE** (gemeinsames Mandantenkonto). Konzeptionell waren weitere Modelle (`PREPAY_USER`, `UNLIMITED`, `CREDIT_POSTPAY`) vorgesehen — diese sind im Code nicht implementiert.
### Kernentitäten
- **`BillingAccount`:** Guthaben (`balance` CHF), `mandateId`, optional `userId`, `warningThreshold`, `enabled`. Kein explizites `accountType`-Feld im Code — Mandant vs. User wird über Vorhandensein von `userId` unterschieden.
- **`BillingTransaction`:** `CREDIT` | `DEBIT` | `ADJUSTMENT`; Referenzen (`referenceType` / `referenceId`, `workflowId`, `featureInstanceId`, `aicoreProvider`). Mandanten-Kontext über **`BillingAccount.mandateId`** — Chat-Modelle bleiben user-owned ohne `mandateId` in `ChatWorkflow`; Zuordnung für Statistiken über Transaktionen.
- **`BillingSettings`:** `warningThresholdPercent`, `notifyEmails`, `notifyOnWarning`, `stripeCustomerId` (Organisation = Mandant). Zusätzlich: `autoRechargeEnabled`, `rechargeAmountCHF`, `rechargeThresholdCHF` (Auto-Recharge) sowie Storage-Felder (`storageLimitGB`, `STORAGE_PRICE_PER_GB_CHF`).
- **`UsageStatistics`:** Aggregationen nach Periode, Provider und Feature (Konzept; Auswertung aus Transaktionen).
### Preise und Währung
- Verrechnung in **CHF**; zentraler Aufschlag via `calculatePriceWithMarkup()` in `mainServiceBilling.py` mit `BILLING_MARKUP_PERCENT = 400` (Multiplikator ×5 auf Provider-Kosten). **Achtung:** Code-Kommentare referenzieren inkonsistent „Faktor 2.0" und „50 %", die Konstante ist aktuell 400 %.
### Provider-Steuerung (RBAC)
- Ressourcen: `resource.aicore.{connectorType}` mit Aktion `USE`.
- Keine parallele Provider-Whitelist in den Chat-Tabellen; Filterung bei Modellauswahl über erlaubte Provider / RBAC.
---
## Subscription-System
### Abgrenzung zu Usage-Billing
- **Billing:** Verbrauch, Guthaben, Transaktionen.
- **Subscription:** Vertrag, Laufzeit, Stripe-Subscription, Kapazitätsregeln, Status für „darf der Mandant KI nutzen“.
Empfohlene Platzierung: **`serviceSubscription`** parallel zu `serviceBilling` im Service Center; Persistenz im gemeinsamen Billing-Kontext (z. B. DB `poweron_billing`). **`BillingService.checkBalance`** soll intern **`SubscriptionService.assertActive(mandateId)`** aufrufen, damit Call-Sites (z. B. `mainServiceAi`) eine zusammengefasste Prüfung behalten.
### Pläne und Kapazität
- **Katalog `SubscriptionPlan`:** u. a. `planKey`, mehrsprachige Texte, `billingPeriod` (`monthly` | `yearly` | `none`), `pricePerUserCHF` / `pricePerFeatureInstanceCHF`, optional `maxUsers` / `maxFeatureInstances` (`None` = unbegrenzt), `trialDays`, `successorPlanKey`, Stripe-Product/Price-IDs für User- und Instance-Items.
- **Instanz `MandateSubscription`:** `mandateId`, `planKey`, Snapshot-Preise, Status, Laufzeit (`startedAt`, `endedAt`, `currentPeriodStart` / `currentPeriodEnd`, `trialEndsAt`), Stripe-IDs (`stripeSubscriptionId`, Item-IDs für User/Instanzen).
### Nutzungsbasierte Mengen (Stripe)
- Zwei Subscription-Items mit dynamischer **Quantity:** aktive `UserMandate` (`enabled = true`) und aktive `FeatureInstance` (`enabled = true`). Änderungen an User/Instanz → DB-Update → **Stripe `subscription_items.update`** mit Proration (Standard: `create_prorations`).
- **Trial:** harte Caps (Konzept: z. B. `maxUsers: 1`, `maxFeatureInstances: 3`). **Standard/Enterprise:** ohne harte Plan-Caps, rein nutzungsbasiert. **Root:** Plan `ROOT`, `billingPeriod: none`, keine Stripe-Subscription, unbegrenzt; Usage-Billing kann **PREPAY_USER** mit Startguthaben bleiben.
### Source of Truth
- **App** führt Plan-Definitionen und Entitlements; Anlage/Änderung von Products/Prices über Stripe-API aus der App, nicht ad hoc im Dashboard.
- **Stripe** liefert Source of Truth für **Zahlungsstatus**; Webhooks (`invoice.paid`, `invoice.payment_failed`, `customer.subscription.updated`, …) synchronisieren in die lokale DB.
### Integration (Konzept)
- **AI-Hot-Path:** Subscription-Status (gecacht, TTL z. B. 60 s) + bestehende Balance-Prüfung; erweiterte `BillingCheckResult`-Gründe (`SUBSCRIPTION_INACTIVE`, …) und UI-Pfade (`subscriptionUiPath`, `userAction`).
- **Mutationen:** Cap-Check nur bei Plänen mit Limits; danach Stripe-Quantity-Sync. Periodischer Job: Abgleich Stripe-Quantity vs. DB-Counts.
- **Downgrade:** nur wenn `aktive User/Instanzen ≤ neue Plan-Caps`.
Referenz **Gateway-Audit** zu Prepaid-Beispielen: Trial **5 CHF**, Standard **10 CHF/Monat** — als kompakte Audit-Angabe; detaillierte Plan-Beispiele (z. B. CHF pro Seat/Instanz und Periode) stehen in den Subscription-Konzepttabellen.
---
## Subscription State Machine
### Grundregeln
- Pro Mandant ist höchstens **eine** Subscription **operativ** im Sinne von **`ACTIVE` oder `TRIALING`** (für volle KI-Nutzung laut Konzept; siehe unten zu `PAST_DUE`).
- Zusätzlich höchstens eine in **`PENDING`** oder **`SCHEDULED`** (Wechsel while Vorgänger läuft).
- Wechsel bei laufendem Abo: Vorgänger **`recurring = false`** (Stripe `cancel_at_period_end`), neues Abo startet nach Periodenende des Vorgängers bzw. laut Scheduler/Webhook.
- **`CANCELLED` als Status entfällt:** gekündigt = **`ACTIVE`** mit **`recurring = false`** bis Periodenende, danach **`EXPIRED`**.
### Felder (Ergänzung zum Instanzmodell)
- **`recurring: bool`** — automatische Verlängerung vs. Auslauf am Periodenende.
- **`effectiveFrom: Optional[datetime]`** — bei `SCHEDULED`: Wirksamkeit ab Periodenende des Vorgängers; `None` = sofort.
### Zustände
| State | Bedeutung |
|-------|-----------|
| **PENDING** | Checkout gestartet, Zahlung noch nicht bestätigt |
| **SCHEDULED** | Bestätigt, wartet auf Ende des Vorgänger-Abos |
| **TRIALING** | Testphase |
| **ACTIVE** | Bezahlt / laufend (ggf. `recurring` false bei Kündigung zum Laufzeitende) |
| **PAST_DUE** | Zahlung fehlgeschlagen, Stripe-Retry-Phase |
| **EXPIRED** | Beendet (terminal) |
**AI-Gate (Code-Stand 2026-04-05):** Im Code ist `PAST_DUE` Teil der operativen Statusmenge (`OPERATIVE_STATUSES = ACTIVE, TRIALING, PAST_DUE`). `_checkSubscription` in `mainServiceBilling.py` gibt fuer alle drei Status `None` (erlaubt) zurueck. AI-Calls werden bei `PAST_DUE` **nicht blockiert**. Nur `EXPIRED`, `PENDING`, `SCHEDULED` und fehlende Subscription blockieren AI.
> Konzept vs. Code: Eine strengere Produktregel (PAST_DUE blockiert AI) war konzeptionell vorgesehen, ist aber aktuell nicht implementiert.
### Erlaubte Statusübergänge (Kernmenge)
Alle Writes mit expliziter **`subscriptionId`**; Validierung zentral (z. B. `transitionStatus(from, to)`).
- `PENDING``ACTIVE` | `SCHEDULED` | `EXPIRED`
- `SCHEDULED``ACTIVE` | `EXPIRED`
- `TRIALING``EXPIRED`
- `ACTIVE``PAST_DUE` | `EXPIRED`
- `PAST_DUE``ACTIVE` | `EXPIRED`
Typische Trigger: Stripe-Webhooks (`checkout.session.completed`, `invoice.payment_failed`, `customer.subscription.updated` / `deleted`), Admin-Aktionen (Kündigung = `recurring` false, Force-Cancel = sofort `EXPIRED`), Trial-Ende (Cron/Webhook).
---
## Billing-Checks
### AI-Call (Hot Path)
- **`mainServiceAi`:** `_preflightBillingCheck` und **`_checkBillingBeforeAiCall`** vor Provider-Auswahl bzw. Call.
- Reihenfolge (Zielbild nach Konzept): zuerst **Subscription aktiv** (`assertActive` / eingebettet in `checkBalance`), dann **Guthaben** (`checkBalance` / Prepaid), um unnötige DB-Runden zu vermeiden.
- **Datenvolumen:** **`assertCapacity("dataVolumeMB")`** prüft die RAG-Index-Grösse (Gateway-Kontext).
### Mutationen (kein Hot Path)
- Kapazitäts-Caps (**`assertCapacity`**) bei User-Zuordnung und Feature-Instanz-Erstellung, nicht bei jedem AI-Request.
- Nach Änderungen: Stripe-Quantity-Sync.
### Konsistenz-Job
- Vergleich Stripe-Quantities mit DB-Counts; bei Drift Korrektur Richtung DB und Benachrichtigung.
---
## Schlüssel-Dateien
| Datei / Bereich | Rolle |
|-----------------|--------|
| `serviceCenter/services/serviceBilling/` | Balance, Usage-Recording, Orchestrierung mit Subscription-Assert (Zielbild) |
| `serviceCenter/services/serviceAi/mainServiceAi.py` | `_preflightBillingCheck`, `_checkBillingBeforeAiCall`, zentrales AI-Gate |
| `serviceCenter/services/serviceSubscription/` | Subscription-Domäne, Cache, Stripe-Sync, Trial (vorgesehen) |
| `serviceCenter/registry.py` | Registrierung `billing`, `subscription` |
| `interfaces/interfaceDbBilling.py` | `BillingAccount`, `BillingTransaction`, `checkBalance` |
| `interfaces/interfaceDbSubscription.py` | Subscription-CRUD, `assertActive`, `assertCapacity` (vorgesehen) |
| `interfaces/interfaceDbApp.py` | `createUserMandate` / Root-Zuordnung — Hooks für Cap + Stripe-Quantity |
| `routes/routeBilling.py` | Billing-APIs, Stripe Checkout Top-Up / Webhooks |
| `routes/routeAdminFeatures.py` | `create_feature_instance` — Cap + Quantity-Sync (vorgesehen) |
| `routes/routeSubscription.py` | Plan-Aktivierung, Status, Wechsel (vorgesehen) |
| `datamodels/datamodelBilling.py` | `BillingSettings`, Transaktionen, `stripeCustomerId` |
| `datamodels/datamodelSubscription.py` | `SubscriptionPlan`, `MandateSubscription` (vorgesehen) |
| `features/workspace/routeFeatureWorkspace.py` | Pattern für Billing-/Fehlerantworten an UI |
| `aicore/aicoreModelSelector.py` | Filter nach erlaubten Providern |
| `aicore/*Plugin*.py` | `connectorType`, Preislogik CHF |
| `ui-nyla/src/pages/billing/*` | Dashboard, Admin, Stripe-Flow, Subscription-UI |
| `ui-nyla/src/hooks/useBilling.ts` | Billing-Settings / Subscription-Daten |
| `ui-nyla/src/pages/views/workspace/useWorkspace.ts` | `billingUiPath` / User-Actions — gleiches Muster für Subscription-Pfade |
---
## Regeln / Invarianten
1. **Zwei Ebenen:** LLM-Verbrauch (Prepaid/Transaktionen) und Plattform-Subscription (Pläne, Stripe, Caps) — gemeinsames Gate vor AI-Calls.
2. **Mandant** ist die primäre Verrechnungseinheit für Usage; Transaktionen tragen den Mandanten-Kontext über das **BillingAccount**, nicht über Chat-Stammdaten.
3. **Pro Mandant** hoechstens eine operative Subscription; `ACTIVE`, `TRIALING` und `PAST_DUE` gelten als **operativ** (AI erlaubt). Nur `EXPIRED`, `PENDING`, `SCHEDULED` und fehlende Subscription blockieren AI.
4. **Kündigung:** Nutzung bis **Ende der bezahlten Periode** (`recurring = false`, Stripe `cancel_at_period_end`); kein separater Status `CANCELLED`.
5. **Status-Änderungen** nur über definierte Transitionen mit **`subscriptionId`** — kein implizites Scannen nach „aktueller“ Zeile allein über `mandateId` bei Writes.
6. **Provider-Zugriff** über RBAC `resource.aicore.*`, konsistent mit Billing-Erfassung pro `aicoreProvider`.
7. **Aktive Zählung:** nur `UserMandate.enabled` und `FeatureInstance.enabled`; Einladungen und Deaktivierte zählen nicht für Quantity/Caps.
8. **`BillingService.checkBalance`** soll **`SubscriptionService.assertActive`** kapseln, damit der AI-Pfad nicht doppelt verteilt ist.
9. **Kapazität** (User/Instanz-Limits) an **Mutations-Endpunkten** enforcen, **nicht** auf jedem AI-Call.
10. **Root-Mandant:** systemische Subscription ohne Stripe-Abrechnung; unbegrenzte Seats/Instanzen; Usage-Billing kann **PREPAY_USER** mit Bootstrap-Guthaben bleiben.