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