13 KiB
Billing & Subscriptions
Überblick
Die Plattform trennt zwei Ebenen:
- Usage-Billing (LLM-Verbrauch): Prepaid-Guthaben in CHF, Buchung pro AI-Operation (
priceCHF), Transaktionen mit Mandanten- und Provider-Kontext. Zentrale Erfassung über Billing-Interfaces undserviceBilling; Integration im AI-Pfad (mainServiceAi). - 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).
Abrechnungsmodelle (BillingSettings.billingModel)
| Modell | Kostenstelle | Kurzbeschreibung |
|---|---|---|
PREPAY_MANDATE |
Mandant | Gemeinsames Guthaben für alle User des Mandanten |
PREPAY_USER |
User im Mandanten-Kontext | Eigenes Guthaben pro User; automatisches Startguthaben v. a. für Root-Mandant / Bootstrap (defaultUserCredit, z. B. 5 CHF) |
UNLIMITED |
— | Keine Guthabenlimitierung (interne Mandanten) |
CREDIT_POSTPAYist entfernt; gespeicherte Legacy-Werte werden beim Lesen aufPREPAY_MANDATEnormalisiert (ohne separate DB-Migration).- Audit-Hinweis: Im Gateway-Kontext wird der operative Fokus als PREPAY_MANDATE (kein PREPAY_USER im Sinne des Standard-Mandanten-Produkts) geführt; Root bleibt im Konzept mit PREPAY_USER und Startguthaben beschrieben.
Kernentitäten
BillingAccount: Guthaben (balanceCHF), optionaluserIdbeiPREPAY_USER,accountTypeMANDATE | USER,warningThreshold,enabled.BillingTransaction:CREDIT|DEBIT|ADJUSTMENT; Referenzen (referenceType/referenceId,workflowId,featureInstanceId,aicoreProvider). Mandanten-Kontext überBillingAccount.mandateId— Chat-Modelle bleiben user-owned ohnemandateIdinChatWorkflow; Zuordnung für Statistiken über Transaktionen.BillingSettings: Modell,defaultUserCredit,warningThresholdPercent,notifyEmails,notifyOnWarning; ErweiterungstripeCustomerId(Organisation = Mandant, nicht einzelne Subscription).UsageStatistics: Aggregationen nach Periode, Provider und Feature (Konzept; Auswertung aus Transaktionen).
Preise und Währung
- Verrechnung in CHF; Plugin-seitig typisch
calculatePriceCHF()mit dokumentiertem Aufschlag (Konzept: z. B. 50 % auf Provider-Kosten), Werte in den AICore-Plugins gepflegt.
Provider-Steuerung (RBAC)
- Ressourcen:
resource.aicore.{connectorType}mit AktionUSE. - 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, optionalmaxUsers/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 aktiveFeatureInstance(enabled = true). Änderungen an User/Instanz → DB-Update → Stripesubscription_items.updatemit Proration (Standard:create_prorations). - Trial: harte Caps (Konzept: z. B.
maxUsers: 1,maxFeatureInstances: 3). Standard/Enterprise: ohne harte Plan-Caps, rein nutzungsbasiert. Root: PlanROOT,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
ACTIVEoderTRIALING(für volle KI-Nutzung laut Konzept; siehe unten zuPAST_DUE). - Zusätzlich höchstens eine in
PENDINGoderSCHEDULED(Wechsel while Vorgänger läuft). - Wechsel bei laufendem Abo: Vorgänger
recurring = false(Stripecancel_at_period_end), neues Abo startet nach Periodenende des Vorgängers bzw. laut Scheduler/Webhook. CANCELLEDals Status entfällt: gekündigt =ACTIVEmitrecurring = falsebis Periodenende, danachEXPIRED.
Felder (Ergänzung zum Instanzmodell)
recurring: bool— automatische Verlängerung vs. Auslauf am Periodenende.effectiveFrom: Optional[datetime]— beiSCHEDULED: 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 (Produktregel aus Mandanten-Konzept): Für die Freigabe von AI-Calls gelten ACTIVE und TRIALING; bei PAST_DUE, EXPIRED und fehlender Subscription sind AI-Calls blockiert (auch wenn die Stripe-State-Maschine PAST_DUE als „Grace“ modelliert).
Erlaubte Statusübergänge (Kernmenge)
Alle Writes mit expliziter subscriptionId; Validierung zentral (z. B. transitionStatus(from, to)).
PENDING→ACTIVE|SCHEDULED|EXPIREDSCHEDULED→ACTIVE|EXPIREDTRIALING→EXPIREDACTIVE→PAST_DUE|EXPIREDPAST_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:_preflightBillingCheckund_checkBillingBeforeAiCallvor Provider-Auswahl bzw. Call.- Reihenfolge (Zielbild nach Konzept): zuerst Subscription aktiv (
assertActive/ eingebettet incheckBalance), 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 |
frontend_nyla/src/pages/billing/* |
Dashboard, Admin, Stripe-Flow, Subscription-UI |
frontend_nyla/src/hooks/useBilling.ts |
Billing-Settings / Subscription-Daten |
frontend_nyla/src/pages/views/workspace/useWorkspace.ts |
billingUiPath / User-Actions — gleiches Muster für Subscription-Pfade |
Regeln / Invarianten
- Zwei Ebenen: LLM-Verbrauch (Prepaid/Transaktionen) und Plattform-Subscription (Pläne, Stripe, Caps) — gemeinsames Gate vor AI-Calls.
- Mandant ist die primäre Verrechnungseinheit für Usage; Transaktionen tragen den Mandanten-Kontext über das BillingAccount, nicht über Chat-Stammdaten.
- Pro Mandant höchstens eine operative Subscription
ACTIVE/TRIALINGfür KI-Freigabe;PAST_DUEundEXPIREDblockieren AI gemäß Mandanten-Konzept. - Kündigung: Nutzung bis Ende der bezahlten Periode (
recurring = false, Stripecancel_at_period_end); kein separater StatusCANCELLED. - Status-Änderungen nur über definierte Transitionen mit
subscriptionId— kein implizites Scannen nach „aktueller“ Zeile allein übermandateIdbei Writes. - Provider-Zugriff über RBAC
resource.aicore.*, konsistent mit Billing-Erfassung proaicoreProvider. - Aktive Zählung: nur
UserMandate.enabledundFeatureInstance.enabled; Einladungen und Deaktivierte zählen nicht für Quantity/Caps. BillingService.checkBalancesollSubscriptionService.assertActivekapseln, damit der AI-Pfad nicht doppelt verteilt ist.- Kapazität (User/Instanz-Limits) an Mutations-Endpunkten enforce’n, nicht auf jedem AI-Call.
- Root-Mandant: systemische Subscription ohne Stripe-Abrechnung; unbegrenzte Seats/Instanzen; Usage-Billing kann PREPAY_USER mit Bootstrap-Guthaben bleiben.