181 lines
13 KiB
Markdown
181 lines
13 KiB
Markdown
<!-- 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** enforce’n, **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.
|