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

13 KiB
Raw Blame History

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

  • PENDINGACTIVE | SCHEDULED | EXPIRED
  • SCHEDULEDACTIVE | EXPIRED
  • TRIALINGEXPIRED
  • ACTIVEPAST_DUE | EXPIRED
  • PAST_DUEACTIVE | 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.