subscription concept
This commit is contained in:
parent
4c7cc4069a
commit
c52e28d2eb
1 changed files with 474 additions and 0 deletions
474
concepts/Mandanten-Subscription-Konzept.md
Normal file
474
concepts/Mandanten-Subscription-Konzept.md
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# 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
|
||||
```
|
||||
Loading…
Reference in a new issue