wiki/concepts/Mandanten-Subscription-Konzept.md
2026-03-22 10:52:02 +01:00

26 KiB
Raw Blame History

Mandanten-Subscription & Kosten pro Mandant — Konzept

Zweck und Abgrenzung

Dieses Dokument ergänzt Billing-Konzept.md. Dort geht es um LLM-Verbrauch (Prepaid-Guthaben, Transaktionen, Provider-RBAC). Hier geht es um SaaS-Lizenzierung pro Mandant: wiederkehrende Pläne, enthaltene Kapazitäten (User, Feature-Instanzen), Stripe-Abrechnung der Plattformgebühr, und die Kopplung dieser Regeln an Mandanten-Aktivität und Verbrauchs-Billing.

Ziel: Ein Mandant ist nur dann voll produktiv nutzbar, wenn eine aktive Subscription existiert, die Kapazität und Zahlungsstatus abdeckt. Ohne gültige Subscription werden AI-Calls blockiert; Zugriff auf bestehende Daten (lesen, navigieren) bleibt erhalten. Das bestehende Usage-Billing (Token/Kosten) bleibt bestehen und wird um Subscription-Prüfungen erweitert.

Kein Legacy-Modus: Die Plattform ist noch nicht produktiv — es gibt keine Backwards Compatibility. Die Subscription ist ab Einführung obligatorisch für jeden Mandanten (Root-Mandant hat eine systemgenerierte Subscription).


Bestand in der Codebase (Stand Analyse)

Gateway

Bereich Rolle
modules/serviceCenter/services/serviceBilling/ Zentraler Billing-Service; Balance-Checks, Usage-Recording
modules/serviceCenter/registry.py Registriert billing als importierbaren Service (objectKey: service.billing)
modules/interfaces/interfaceDbBilling.py checkBalance(mandateId, userId, estimatedCost) — aktuell nur Prepaid-Logik (PREPAY_USER / PREPAY_MANDATE)
modules/serviceCenter/services/serviceAi/mainServiceAi.py _preflightBillingCheck, _checkBillingBeforeAiCall — hier wird die Subscription-Prüfung integriert
modules/datamodels/datamodelUam.py Mandate mit enabled, isSystem
modules/datamodels/datamodelBilling.py BillingSettings pro Mandant — hier wird stripeCustomerId ergänzt
modules/routes/routeBilling.py APIs inkl. Stripe Checkout für Top-Up (bestehend)
modules/routes/routeAdminFeatures.py create_feature_instance — Stripe-Quantity-Sync hier einhängen
modules/interfaces/interfaceDbApp.py _assignUserToRootMandate, createUserMandate — Stripe-Quantity-Sync hier einhängen
modules/features/workspace/routeFeatureWorkspace.py Blockierung bei Billing-Problemen (Pattern für Subscription-Fehler nutzbar)

Frontend (Nyla)

Bereich Rolle
frontend_nyla/src/pages/billing/* Dashboard, Mandanten-Ansicht, Admin, Stripe-Flow
frontend_nyla/src/hooks/useBilling.ts Laden/Speichern der Billing-Settings pro Mandant
frontend_nyla/src/pages/views/workspace/useWorkspace.ts Nutzung von billingUiPath / User-Actions bei Budget — gleiches Muster für Subscription-Upgrade-Pfad

1. Wo die Subscription als „separater Container" leben soll

Empfehlung

Neuer Service parallel zu Billing:
modules/serviceCenter/services/serviceSubscription/
mit klarer Schnittstelle zu Billing, aber eigenem Domänenmodell und Persistenz (Tabellen in poweron_billing-DB, da gemeinsamer Kontext).

Begründung

  • Billing (serviceBilling) ist bereits auf Verbrauch und Guthaben fokussiert und wird von ai und Features als Dependency gezogen. Subscription ist Vertrags- und Kapazitätslogik (Zeiträume, Stripe Subscriptions, Limits). Vermischung in einer Klasse erhöht Komplexität und Testaufwand.
  • Das Service Center ist der etablierte Ort für mandantenbezogene Querschnitts-Services (siehe registry.py). Ein Eintrag subscription neben billing erlaubt saubere Abhängigkeiten.
  • Ein reines Ablegen unter modules/subscription/ ohne Service-Center-Anbindung würde die bestehende Resolver-/RBAC-Konvention brechen.

Orchestrierung

BillingService.checkBalance ruft intern SubscriptionService.assertActive(mandateId) auf. Call-Sites (z. B. mainServiceAi) prüfen weiterhin eine Methode — keine doppelten Aufrufe an 50 Stellen.


2. Lebenszyklus, Laufzeiten und Stripe

Datenregeln

  • Pro Mandant existieren mehrere Subscription-Instanzen (Historie, geplante Wechsel).
  • Genau eine hat Status aktiv (ACTIVE oder TRIALING) für einen Zeitpunkt t.
  • Pflichtfeld startedAt. Optional endedAt (Kündigungs-/Enddatum).

Laufzeiten und automatische Erneuerung

Jeder Plan hat eine Laufzeit (billingPeriod): Monat oder Jahr. Nach Ablauf einer Periode wird die Subscription automatisch erneuert (Stripe handhabt dies nativ über interval: month | year). Bei Erneuerung:

  1. Stripe erstellt automatisch eine Invoice für die neue Periode.
  2. Invoice wird eingezogen (Zahlungsmittel des Stripe Customer).
  3. Bei Erfolg: Subscription bleibt ACTIVE, currentPeriodEnd wird aktualisiert.
  4. Bei Fehlschlag: Subscription geht auf PAST_DUE → AI-Calls blockiert, E-Mail an Billing-Kontakt.

Pläne ohne Laufzeit (Root): billingPeriod: none, keine Stripe-Subscription, keine Erneuerung.

Stripe: automatische wiederkehrende Verrechnung

Mit Stripe Billing (Products, Prices, Subscriptions) werden wiederkehrende Zahlungen automatisch eingezogen. Webhooks (invoice.paid, invoice.payment_failed, customer.subscription.updated, …) synchronisieren den Status in die eigene Datenbank.

Source of Truth: App → Stripe

Die App ist führend für Preis-Definitionen und Entitlements. Stripe ist das Inkasso-System.

  • Pläne und Preise werden in der App definiert und über die Stripe API als Products/Prices angelegt.
  • Änderungen an Plänen immer über die App → Stripe-API. Nie manuell im Stripe Dashboard editieren.
  • Stripe ist Source of Truth für den Zahlungsstatus (bezahlt, fehlgeschlagen, Rechnungs-PDF). Webhooks schreiben diesen Status in unsere DB.

3. Nutzungsbasierte Abrechnung (Usage-Based Quantity)

Grundprinzip

Es wird immer abgerechnet, was effektiv aktiv ist. Der User wählt keinen festen maxUsers/maxFeatureInstances-Wert — die Subscription skaliert automatisch mit der tatsächlichen Nutzung. Wenn User oder Instanzen hinzukommen, wird sofort nachverrechnet (Stripe Proration).

Mechanismus

Die Stripe-Subscription hat zwei Subscription Items mit dynamischer Quantity:

  • User-Seats Item: quantity = Anzahl aktive UserMandate für diesen Mandanten
  • Instance Item: quantity = Anzahl aktive FeatureInstance für diesen Mandanten

Ablauf bei Mutations

User/Instanz wird hinzugefügt oder entfernt
    │
    ├─ 1. Lokale DB aktualisieren (UserMandate / FeatureInstance)
    │
    ├─ 2. Effektive Anzahl zählen:
    │     activeUsers = COUNT(UserMandate WHERE mandateId=X AND enabled=true)
    │     activeInstances = COUNT(FeatureInstance WHERE mandateId=X AND enabled=true)
    │
    ├─ 3. Stripe Subscription Items Quantity aktualisieren:
    │     stripe.subscription_items.update(userItemId, quantity=activeUsers)
    │     stripe.subscription_items.update(instanceItemId, quantity=activeInstances)
    │
    └─ 4. Stripe berechnet automatisch:
          - Proration für die laufende Periode (Teilperiode wird nachverrechnet)
          - Nächste Rechnung reflektiert die neue Quantity

Proration

Stripe prorated sofort bei Quantity-Änderungen (Standardverhalten proration_behavior: create_prorations):

  • User hinzugefügt Mitte des Monats → Nachverrechnung für die verbleibenden Tage auf der nächsten Rechnung.
  • User entfernt Mitte des Monats → Gutschrift für die verbleibenden Tage auf der nächsten Rechnung.

Kein manuelles Limit für Standard-Pläne

Beim Standard-Plan gibt es keine harte Obergrenze für Users oder Instanzen — der Mandant zahlt, was er nutzt. Die einzigen Limitierungen sind:

  • Trial: harte Caps (maxUsers: 1, maxFeatureInstances: 3)
  • Root: keine Limits, keine Abrechnung
  • Standard/Enterprise: keine Plan-Caps, rein nutzungsbasiert

Das maxUsers/maxFeatureInstances auf dem Plan bleibt als optionales Feld für Pläne die harte Caps brauchen (Trial, zukünftige Budget-Pläne). Bei None = unbegrenzt = rein nutzungsbasiert.


4. Mandant nur „aktiv", wenn Subscription aktiv

Semantik: Was wird blockiert?

Bei fehlender oder inaktiver Subscription (inkl. PAST_DUE, EXPIRED, CANCELLED) wird:

  • AI-Calls blockiert_checkBillingBeforeAiCall in mainServiceAi.py gibt strukturierten Fehler zurück.
  • Daten bleiben lesbar — Workspace öffnen, Dokumente ansehen, Navigation, Export: alles weiterhin möglich. Nur kostenverursachende Operationen (AI) werden gesperrt.

Das bestehende Pattern in routeFeatureWorkspace.py (Zeile 807, Billing-Block) kann als Vorlage dienen, muss aber differenzieren: AI-Block ja, Read-Zugriff nein.

Mandate.enabled vs. Subscription-Status

  • Mandate.enabled bleibt für technische Admin-Sperren (z. B. Missbrauch).
  • Subscription-Status steuert die geschäftliche Nutzbarkeit. Beides muss true/ACTIVE sein für volle Funktion.

Root-Mandant

  • Jeder neu registrierte User liegt im Root-Mandanten mit der Root-Subscription:
    • Keine Verrechnung über Stripe für diese Subscription.
    • Unbegrenzt für User und Feature-Instanzen.
    • Usage-Billing kann wie heute PREPAY_USER mit Startguthaben bleiben — Subscription blockiert Root nicht.
    • Root-Subscription wird beim Bootstrap automatisch erstellt.

5. E-Mail bei jeder Subscription-Änderung und bei Verrechnung

Ereignisse (mindestens)

  • Subscription aktiviert, gewechselt, gekündigt (Enddatum gesetzt), erneuert, reaktiviert.
  • Trial-Ende → automatische Transition zu Standard (inkl. erster Rechnung).
  • Stripe Invoice paid (Buchhaltungs-relevante Daten: Betrag, Periode, Steuern falls vorhanden, Stripe-Invoice-URL/PDF, Positionen inkl. User-Seats und Instance-Count).
  • Zahlung fehlgeschlagen / Subscription past_due.
  • Quantity-Änderung (User/Instanz hinzugefügt/entfernt) — optional als Zusammenfassung statt pro Einzelereignis.

Empfänger

  • Mandanten-Billing-Kontakt (bestehendes Feld aus BillingSettings.notifyEmails).

Inhalt

  • Maschinenlesbare Kurzfassung + human-readable für Buchhaltung (Referenznummern, Zeitraum, CHF, Positionen mit Quantity, Link zu Stripe-Invoice wo zulässig).

Technisch: analog zu billingExhaustedNotify.py einen SubscriptionNotify-Pfad; Templates über serviceMessaging.


6. Pydantic-Modelle (Definition + Verrechnung + mehrsprachige UI-Texte)

A) SubscriptionPlan (Katalog)

Beschreibt was verkauft wird. Nicht pro Mandant dupliziert; sysadmin-gepflegt, versionierbar.

  • planKey: str (z. B. STANDARD_MONTHLY, STANDARD_YEARLY, TRIAL_7D, ROOT)
  • selectableByUser: boolfalse für Root, zukünftige interne Pläne
  • Mehrsprachige Texte: title: dict[str, str], description: dict[str, str] (en / de / fr …)
  • Verrechnungsparameter:
    • currency: str (fix CHF)
    • billingPeriod: str (monthly | yearly | none)
    • pricePerUserCHF: float (Beispiel: 200.00 pro Periode)
    • pricePerFeatureInstanceCHF: float (Beispiel: 400.00 pro Periode)
    • autoRenew: bool (default true — Stripe erneuert automatisch)
  • Optionale Kapazitäts-Caps (nur für Pläne mit harten Grenzen):
    • maxUsers: Optional[int]None = unbegrenzt (Standard, Root)
    • maxFeatureInstances: Optional[int]None = unbegrenzt (Standard, Root)
    • trialDays: Optional[int] — nur bei Trial-Plänen (z. B. 7)
    • successorPlanKey: Optional[str] — Plan, auf den bei Trial-Ende automatisch gewechselt wird (z. B. STANDARD_MONTHLY)
  • Stripe-Mapping:
    • stripeProductId: Optional[str]
    • stripePriceIdUsers: Optional[str] (Price für User-Seat Item, mit recurring.interval)
    • stripePriceIdInstances: Optional[str] (Price für Instance Item, mit recurring.interval)

B) MandateSubscription (Instanz am Mandanten)

  • id: str, mandateId: str
  • planKey: str — Referenz zum SubscriptionPlan
  • Snapshot der Plan-Parameter bei Aktivierung (für Rechnungshistorie):
    • snapshotPricePerUserCHF, snapshotPricePerInstanceCHF
  • Status und Laufzeit:
    • status: strACTIVE | CANCELLED | EXPIRED | PAST_DUE | TRIALING
    • startedAt: datetime (Pflicht), endedAt: Optional[datetime]
    • currentPeriodStart: datetime, currentPeriodEnd: datetime — aktuelle Abrechnungsperiode (synchron mit Stripe)
    • trialEndsAt: Optional[datetime]
  • Stripe: stripeSubscriptionId: Optional[str], stripeItemIdUsers: Optional[str], stripeItemIdInstances: Optional[str]

Stripe-Customer gehört zum Mandant, nicht zur Subscription

stripeCustomerId wird auf BillingSettings ergänzt (1:1 pro Mandant, existiert bereits). Begründung: Ein Stripe Customer repräsentiert die Organisation. Wenn Mandant A Subscription 1 kündigt und Subscription 2 startet, nutzen beide denselben Stripe Customer — Zahlungsmittel, Rechnungshistorie und Kontaktdaten bleiben erhalten.

BillingSettings (existierend, erweitert)
├── stripeCustomerId: Optional[str]  ← NEU
├── billingModel (PREPAY_MANDATE | PREPAY_USER)
├── notifyEmails, warningThreshold, ...

MandateSubscription (neu)
├── stripeSubscriptionId  ← auf Subscription-Ebene
├── stripeItemIdUsers, stripeItemIdInstances
├── currentPeriodStart, currentPeriodEnd  ← Laufzeit-Tracking

C) Effective Usage (für Abrechnung, nicht eigenes Modell)

  • Aktuelle Anzahl aktive User mit Mandantenzugriff: COUNT(UserMandate WHERE mandateId = X AND enabled)
  • Aktuelle Anzahl aktive Feature-Instanzen des Mandanten: COUNT(FeatureInstance WHERE mandateId = X AND enabled)

Diese Werte werden bei jeder Mutation als Stripe Quantity synchronisiert → Abrechnung stets korrekt.


7. Getrennte Prüfzeitpunkte (Hot Path vs. Mutations)

Designprinzip

Subscription-Prüfungen müssen an zwei klar getrennten Stellen erfolgen — nicht alles in einem einzigen Gate bei jedem AI-Call:

Prüfzeitpunkt Was wird geprüft Warum getrennt
AI-Call (hot path, jeder Request) 1. Subscription aktiv? 2. Budget ausreichend? Schnell, gecacht; ändert sich selten
User-Add / Instance-Create (Mutation) 1. Kapazitäts-Cap (nur bei Plänen mit harten Limits, z. B. Trial) 2. Stripe-Quantity aktualisieren Nur bei strukturellen Änderungen
Periodischer Job (Konsistenz) Stripe-Quantity vs. DB-Count abgleichen Sicherheitsnetz für Drift

Kapazität wird bewusst NICHT im AI-Hot-Path geprüft, weil:

  • User- und Instanz-Zahlen ändern sich nicht zwischen AI-Calls
  • Das Zählen von DB-Records bei jedem AI-Call wäre teuer und sinnlos
  • Enforcement an den Mutations-Endpunkten ist vollständig und ausreichend

7a. AI-Call Gate (Hot Path)

In mainServiceAi._checkBillingBeforeAiCall:

  1. Subscription-Status prüfen (gecacht, siehe Caching unten)
    ACTIVE oder TRIALING? weiter. Sonst: blockieren.
  2. Budget prüfen (bestehende checkBalance-Logik)
    → ausreichend? weiter. Sonst: blockieren wie bisher.

Erweiterung von BillingCheckResult:

  • reason: bestehende (INSUFFICIENT_BALANCE) + neue (SUBSCRIPTION_INACTIVE, SUBSCRIPTION_PAYMENT_REQUIRED, SUBSCRIPTION_EXPIRED)
  • upgradeRequired: bool
  • subscriptionUiPath: Optional[str] — z. B. /billing?tab=subscription
  • userAction: bestehende (TOP_UP_SELF, CONTACT_MANDATE_ADMIN) + neue (UPGRADE_SUBSCRIPTION, REACTIVATE_SUBSCRIPTION, ADD_PAYMENT_METHOD)

Implementierung: BillingService.checkBalance ruft intern zuerst SubscriptionService.assertActive(mandateId) auf. Wenn Subscription nicht aktiv → sofort BillingCheckResult(allowed=False, reason=...) zurückgeben, ohne Budget-DB-Roundtrip.

Caching des Subscription-Status

Der Subscription-Status ändert sich selten (Plan-Wechsel, Zahlung fehlgeschlagen). Für den AI-Hot-Path:

  • In-Memory Cache mit TTL (z. B. 60 Sekunden)
  • Cache-Key: mandate:{mandateId}:subscription_status
  • Invalidierung: Stripe-Webhook-Handler und lokale Mutations (Plan-Wechsel, Kündigung, Trial-Ablauf) setzen den Cache zurück
  • Fallback: Bei Cache-Miss → DB-Query, Result cachen

7b. Mutations-Gate (User-Add, Instance-Create, Remove)

Bei User-Add / Instance-Create:

  1. Falls Plan harte Caps hat (z. B. Trial: maxUsers: 1, maxFeatureInstances: 3):
    Prüfen currentActive + 1 <= plan.maxUsers/maxFeatureInstances.
    Bei Überschreitung: HTTP 402 mit strukturiertem Error.

  2. Lokale DB aktualisieren (UserMandate / FeatureInstance).

  3. Stripe-Quantity synchronisieren (nur bei Plänen mit Stripe-Subscription):
    stripe.subscription_items.update(itemId, quantity=newActiveCount)
    → Proration wird automatisch erstellt.

Bei User-Remove / Instance-Deaktivieren:

  1. Lokale DB aktualisieren.
  2. Stripe-Quantity reduzieren → Gutschrift auf nächster Rechnung.

Error-Response bei Cap-Überschreitung (Trial):

{
    "error": "SUBSCRIPTION_USER_LIMIT",
    "currentCount": 1,
    "maxAllowed": 1,
    "message": "...",
    "userAction": "UPGRADE_SUBSCRIPTION",
    "subscriptionUiPath": "/billing?tab=subscription"
}

7c. Downgrade-Regeln

Wenn ein Mandant auf einen Plan mit harten Caps wechseln will (z. B. von Standard auf einen zukünftigen Budget-Plan):

Regel: Downgrade ist nur erlaubt, wenn effective <= new entitlement.

  • Bevor der Plan-Wechsel committed wird: prüfen currentActiveUsers <= newPlan.maxUsers AND currentActiveInstances <= newPlan.maxFeatureInstances.
  • Wenn nicht erfüllt: Fehler mit klarer Meldung.
  • Stripe-Subscription wird erst aktualisiert, wenn die lokale Prüfung bestanden ist.

7d. UI: Upgrade und Subscription-Management

Wenn reason eine Subscription-Kategorie ist:

  • API-Responses (HTTP 402 oder strukturiertes SSE-Error-Objekt) enthalten subscriptionUiPath und userAction (analog bestehendem TOP_UP_SELF / billingUiPath in useWorkspace.ts).
  • Frontend zeigt Call-to-Action und Navigation zur Subscription-/Billing-Seite.
  • Auf der Subscription-Seite: Plan-Auswahl (nur selectableByUser: true), aktuelle Nutzung (Users / Instanzen), Kosten-Vorschau, Stripe Checkout/Update-Flow.

8. Checks bei User-Zuordnung und Feature-Instanzen

Triggerpunkte (Gateway)

  • User zum Mandanten hinzufügen (interfaceDbApp.createUserMandate, routeInvitations bei Einladungsannahme):
    1. Cap-Check (nur bei Plänen mit maxUsers)
    2. DB Insert
    3. Stripe-Quantity-Sync
  • Feature-Instanz erstellen (routeAdminFeatures.create_feature_instance):
    1. Cap-Check (nur bei Plänen mit maxFeatureInstances)
    2. DB Insert
    3. Stripe-Quantity-Sync
  • User entfernen / Instanz deaktivieren:
    1. DB Update
    2. Stripe-Quantity-Sync (reduzieren → Gutschrift)

Was zählt als „aktiv"?

  • User: nur UserMandate-Einträge mit enabled = true. Einladungen zählen nicht.
  • Feature-Instanzen: nur Instanzen mit enabled = true. Entwürfe/deaktivierte zählen nicht.

Periodische Konsistenz

Job, der Stripe-Quantity vs. DB-Count vergleicht und bei Abweichung:

  1. Stripe-Quantity korrigieren (auf den DB-Count setzen)
  2. Warn-Mail an notifyEmails
  3. Log-Eintrag mit Details

Beispiel-Pläne

Plan planKey selectableByUser billingPeriod maxUsers maxInstances Preis pro Periode Bemerkung
Standard monatlich STANDARD_MONTHLY ja monthly unbegrenzt unbegrenzt CHF activeInstances * 400 + activeUsers * 200 Nutzungsbasiert, Stripe-Quantity = aktive Anzahl
Standard jährlich STANDARD_YEARLY ja yearly unbegrenzt unbegrenzt CHF activeInstances * 4'000 + activeUsers * 2'000 Gleich wie monatlich, Jahresdiscount möglich
Trial 7 Tage TRIAL_7D ja none 1 3 0 CHF Nach Ablauf → automatisch STANDARD_MONTHLY
Root ROOT nein none unbegrenzt unbegrenzt keine Stripe-Subscription Bootstrap; Default für Root-Mandant
Enterprise (zukünftig) ENTERPRISE_DEFAULT nein yearly per Vertrag per Vertrag individuell Default bei neuem Mandanten durch SysAdmin

Trial-Ende: automatischer Wechsel zu Standard

Wenn trialEndsAt erreicht ist:

  1. Trial-Subscription erhält Status EXPIRED.
  2. Automatisch wird eine neue STANDARD_MONTHLY-Subscription erstellt (der Plan aus successorPlanKey).
  3. Stripe-Subscription wird angelegt mit quantity = aktuelle aktive User / Instanzen.
  4. Erste Rechnung wird von Stripe erstellt und eingezogen.
  5. E-Mail an Billing-Kontakt: „Ihre Testphase ist abgelaufen. Ihr Mandant wurde automatisch auf den Standard-Plan umgestellt."

Voraussetzung: Beim Trial-Signup wird bereits ein Zahlungsmittel hinterlegt (Stripe Customer mit PaymentMethod). Falls kein Zahlungsmittel hinterlegt ist:

  • Stripe-Subscription wird trotzdem erstellt, erste Invoice schlägt fehl.
  • Subscription geht auf PAST_DUE → AI-Calls blockiert.
  • UI zeigt prominenten Hinweis: „Bitte hinterlegen Sie ein Zahlungsmittel, um den Standard-Plan zu aktivieren."
  • userAction: ADD_PAYMENT_METHOD

Abhängigkeiten und Reihenfolge (Implementierung)

  1. Datenmodell + DB für SubscriptionPlan (Katalog mit billingPeriod, successorPlanKey) und MandateSubscription (Instanzen mit currentPeriodStart/End) + stripeCustomerId auf BillingSettings.
  2. Bootstrap: Root-Plan und Root-Subscription für den Root-Mandanten automatisch erstellen.
  3. SubscriptionService mit assertActive(mandateId) (gecacht) und assertCapacity(mandateId, resourceType, delta) (nur für Pläne mit Caps).
  4. AI-Gate: BillingService.checkBalance ruft SubscriptionService.assertActive auf; erweiterte BillingCheckResult.
  5. Mutations-Hooks: In createUserMandate und create_feature_instance: Cap-Check + Stripe-Quantity-Sync. Analog bei Remove/Deaktivieren.
  6. Stripe: Customer-Erstellung bei erstem bezahltem Plan; Subscription-Erstellung mit zwei Items (User + Instance); Webhook-Handler für invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, customer.subscription.trial_will_end.
  7. Trial-Ende-Handler: Cron oder Stripe-Webhook customer.subscription.trial_will_end → automatischer Plan-Wechsel zu successorPlanKey.
  8. Downgrade-Prüfung in Plan-Wechsel-Endpunkt.
  9. Frontend: Subscription-Tab unter Billing, Plan-Auswahl, aktuelle Nutzung, Kosten-Vorschau, Stripe Checkout/Update-Flow, Deep-Links aus Fehler-Responses.
  10. E-Mails und Buchhaltungs-Payloads.

Geklärte fachliche Entscheide

  • Kündigung: Zugriff bis Ende der bezahlten Periode. Umsetzung via Stripe cancel_at_period_end: true — Subscription bleibt bis Periodenende ACTIVE, wechselt dann auf CANCELLED. Keine sofortige Sperre.

  • Feature-Instanz-Limit im Trial: maximal 3 Feature-Instanzen. Bestätigt.

  • Proration bei Seat-Änderungen: sofort nachverrechnet. Stripe proration_behavior: create_prorations (Default) — Teilperiode wird auf der nächsten Rechnung nachbelastet bzw. gutgeschrieben.

  • Jahresdiscount: Vorerst kein Discount für den Jahresplan. Jahrespreis = 12 × Monatspreis. Kann später als Incentive eingeführt werden.

Das Konzept ist damit implementierungsreif — keine offenen fachlichen Punkte.


Verwandte Dokumente

  • Billing-Konzept.md — LLM-Verbrauch, Prepaid, Transaktionen
  • wiki/implementation/Saas Multi Tenant Mandate/ — Mandanten-RBAC und UI-Hintergrund

Anhang: Relevante Code-Pfade (Referenz)

Bestehend (zu erweitern)

gateway/modules/serviceCenter/services/serviceBilling/mainServiceBilling.py  # checkBalance → + assertActive
gateway/modules/serviceCenter/services/serviceAi/mainServiceAi.py           # _checkBillingBeforeAiCall
gateway/modules/interfaces/interfaceDbBilling.py                            # checkBalance
gateway/modules/interfaces/interfaceDbApp.py                                # createUserMandate → + Cap-Check + Stripe-Quantity
gateway/modules/routes/routeAdminFeatures.py                                # create_feature_instance → + Cap-Check + Stripe-Quantity
gateway/modules/routes/routeBilling.py                                      # Stripe-Webhooks erweitern
gateway/modules/serviceCenter/registry.py                                   # subscription-Service registrieren
gateway/modules/datamodels/datamodelBilling.py                              # BillingSettings + stripeCustomerId
                                                                            # BillingCheckResult + neue reasons
frontend_nyla/src/pages/billing/                                            # Subscription-Tab
frontend_nyla/src/hooks/useBilling.ts                                       # Subscription-Daten laden
frontend_nyla/src/pages/views/workspace/useWorkspace.ts                     # subscriptionUiPath / userAction

Neu

gateway/modules/datamodels/datamodelSubscription.py                         # SubscriptionPlan, MandateSubscription
gateway/modules/interfaces/interfaceDbSubscription.py                       # CRUD + assertActive + assertCapacity
gateway/modules/serviceCenter/services/serviceSubscription/
    mainServiceSubscription.py                                              # Service mit Cache, Stripe-Sync, Trial-Handler
gateway/modules/routes/routeSubscription.py                                 # Plan-Auswahl, Wechsel, Status-Abfrage