wiki/z-archive/concepts/Mandanten-Subscription-Konzept.md

474 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mandanten-Subscription & Kosten pro Mandant — Konzept
## Zweck und Abgrenzung
Dieses Dokument ergänzt [Billing-Konzept.md](./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](https://stripe.com/docs/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: bool``false` 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: str``ACTIVE | 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):**
```python
{
"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](./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
```