26 KiB
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 vonaiund 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 Eintragsubscriptionnebenbillingerlaubt 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 (
ACTIVEoderTRIALING) für einen Zeitpunktt. - Pflichtfeld
startedAt. OptionalendedAt(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:
- Stripe erstellt automatisch eine Invoice für die neue Periode.
- Invoice wird eingezogen (Zahlungsmittel des Stripe Customer).
- Bei Erfolg: Subscription bleibt
ACTIVE,currentPeriodEndwird aktualisiert. - 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 aktiveUserMandatefür diesen Mandanten - Instance Item:
quantity= Anzahl aktiveFeatureInstancefü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 —
_checkBillingBeforeAiCallinmainServiceAi.pygibt 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.enabledbleibt für technische Admin-Sperren (z. B. Missbrauch).- Subscription-Status steuert die geschäftliche Nutzbarkeit. Beides muss
true/ACTIVEsein 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: bool—falsefür Root, zukünftige interne Pläne- Mehrsprachige Texte:
title: dict[str, str],description: dict[str, str](en/de/fr…) - Verrechnungsparameter:
currency: str(fixCHF)billingPeriod: str(monthly|yearly|none)pricePerUserCHF: float(Beispiel: 200.00 pro Periode)pricePerFeatureInstanceCHF: float(Beispiel: 400.00 pro Periode)autoRenew: bool(defaulttrue— 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, mitrecurring.interval)stripePriceIdInstances: Optional[str](Price für Instance Item, mitrecurring.interval)
B) MandateSubscription (Instanz am Mandanten)
id: str,mandateId: strplanKey: str— Referenz zum SubscriptionPlan- Snapshot der Plan-Parameter bei Aktivierung (für Rechnungshistorie):
snapshotPricePerUserCHF,snapshotPricePerInstanceCHF
- Status und Laufzeit:
status: str—ACTIVE | CANCELLED | EXPIRED | PAST_DUE | TRIALINGstartedAt: 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:
- Subscription-Status prüfen (gecacht, siehe Caching unten)
→ACTIVEoderTRIALING? weiter. Sonst: blockieren. - 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: boolsubscriptionUiPath: Optional[str]— z. B./billing?tab=subscriptionuserAction: 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:
-
Falls Plan harte Caps hat (z. B. Trial:
maxUsers: 1,maxFeatureInstances: 3):
PrüfencurrentActive + 1 <= plan.maxUsers/maxFeatureInstances.
Bei Überschreitung: HTTP 402 mit strukturiertem Error. -
Lokale DB aktualisieren (UserMandate / FeatureInstance).
-
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:
- Lokale DB aktualisieren.
- 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.maxUsersANDcurrentActiveInstances <= 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
subscriptionUiPathunduserAction(analog bestehendemTOP_UP_SELF/billingUiPathinuseWorkspace.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,routeInvitationsbei Einladungsannahme):- Cap-Check (nur bei Plänen mit
maxUsers) - DB Insert
- Stripe-Quantity-Sync
- Cap-Check (nur bei Plänen mit
- Feature-Instanz erstellen (
routeAdminFeatures.create_feature_instance):- Cap-Check (nur bei Plänen mit
maxFeatureInstances) - DB Insert
- Stripe-Quantity-Sync
- Cap-Check (nur bei Plänen mit
- User entfernen / Instanz deaktivieren:
- DB Update
- Stripe-Quantity-Sync (reduzieren → Gutschrift)
Was zählt als „aktiv"?
- User: nur
UserMandate-Einträge mitenabled = 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:
- Stripe-Quantity korrigieren (auf den DB-Count setzen)
- Warn-Mail an
notifyEmails - 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:
- Trial-Subscription erhält Status
EXPIRED. - Automatisch wird eine neue
STANDARD_MONTHLY-Subscription erstellt (der Plan aussuccessorPlanKey). - Stripe-Subscription wird angelegt mit
quantity= aktuelle aktive User / Instanzen. - Erste Rechnung wird von Stripe erstellt und eingezogen.
- 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)
- Datenmodell + DB für
SubscriptionPlan(Katalog mitbillingPeriod,successorPlanKey) undMandateSubscription(Instanzen mitcurrentPeriodStart/End) +stripeCustomerIdaufBillingSettings. - Bootstrap: Root-Plan und Root-Subscription für den Root-Mandanten automatisch erstellen.
- SubscriptionService mit
assertActive(mandateId)(gecacht) undassertCapacity(mandateId, resourceType, delta)(nur für Pläne mit Caps). - AI-Gate:
BillingService.checkBalanceruftSubscriptionService.assertActiveauf; erweiterteBillingCheckResult. - Mutations-Hooks: In
createUserMandateundcreate_feature_instance: Cap-Check + Stripe-Quantity-Sync. Analog bei Remove/Deaktivieren. - 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. - Trial-Ende-Handler: Cron oder Stripe-Webhook
customer.subscription.trial_will_end→ automatischer Plan-Wechsel zusuccessorPlanKey. - Downgrade-Prüfung in Plan-Wechsel-Endpunkt.
- Frontend: Subscription-Tab unter Billing, Plan-Auswahl, aktuelle Nutzung, Kosten-Vorschau, Stripe Checkout/Update-Flow, Deep-Links aus Fehler-Responses.
- 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 PeriodenendeACTIVE, wechselt dann aufCANCELLED. 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