1059 lines
32 KiB
Markdown
1059 lines
32 KiB
Markdown
|
|
# Mitai Jinkendo - Membership & Subscription System (v9c)
|
|||
|
|
|
|||
|
|
**Version:** v9c-dev
|
|||
|
|
**Status:** Backend & Admin-UI komplett, Enforcement deaktiviert
|
|||
|
|
**Letzte Aktualisierung:** 20. März 2026
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Inhaltsverzeichnis
|
|||
|
|
|
|||
|
|
1. [Überblick](#überblick)
|
|||
|
|
2. [Architektur-Entscheidungen](#architektur-entscheidungen)
|
|||
|
|
3. [Datenbank-Schema](#datenbank-schema)
|
|||
|
|
4. [Backend-API](#backend-api)
|
|||
|
|
5. [Frontend-Komponenten](#frontend-komponenten)
|
|||
|
|
6. [Feature-Enforcement-System](#feature-enforcement-system)
|
|||
|
|
7. [Lessons Learned](#lessons-learned)
|
|||
|
|
8. [Roadmap](#roadmap)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Überblick
|
|||
|
|
|
|||
|
|
Das Mitai Jinkendo Membership-System (v9c) ist ein flexibles, tier-basiertes Subscription-System mit folgenden Kern-Features:
|
|||
|
|
|
|||
|
|
### Implementierte Features ✅
|
|||
|
|
|
|||
|
|
- **4 Tier-Stufen**: free, basic, premium, selfhosted
|
|||
|
|
- **Feature-Registry-Pattern**: Zentrale Definition aller limitierbaren Features
|
|||
|
|
- **Flexible Limit-Matrix**: Admin kann Tier × Feature Limits konfigurieren
|
|||
|
|
- **User-Override-System**: Individuelle Limits pro User
|
|||
|
|
- **Coupon-System**: 3 Typen (single_use, multi_use_period, gift)
|
|||
|
|
- **Coupon-Stacking**: Intelligente Pause/Resume-Logik bei temporären Zugriffen
|
|||
|
|
- **Access-Grants**: Zeitlich begrenzte Tier-Zugriffe mit Quelle-Tracking
|
|||
|
|
- **User-Activity-Log**: JSONB-basierte Aktivitätsverfolgung
|
|||
|
|
- **Admin-UI**: Vollständige Verwaltungsoberfläche für alle Aspekte
|
|||
|
|
|
|||
|
|
### Geplante Features 🔲
|
|||
|
|
|
|||
|
|
- Feature-Enforcement in Endpoints (needs redesign)
|
|||
|
|
- Selbst-Registrierung mit E-Mail-Verifizierung
|
|||
|
|
- Trial-System mit automatischem Downgrade
|
|||
|
|
- Bonus-System (Login-Streaks)
|
|||
|
|
- Stripe-Integration
|
|||
|
|
- Partner-Integration (Wellpass, Hansefit)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architektur-Entscheidungen
|
|||
|
|
|
|||
|
|
### 1. Feature-Registry-Pattern
|
|||
|
|
|
|||
|
|
**Entscheidung:** Alle limitierbaren Features werden zentral in einer `features` Tabelle definiert.
|
|||
|
|
|
|||
|
|
**Rationale:**
|
|||
|
|
- Neue Features können ohne Schema-Migration hinzugefügt werden
|
|||
|
|
- Metadaten (name, description, category, unit) sind direkt verfügbar
|
|||
|
|
- Konsistenz zwischen Backend-Checks und Frontend-Display
|
|||
|
|
- Admin-UI kann automatisch generiert werden
|
|||
|
|
|
|||
|
|
**Schema:**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE features (
|
|||
|
|
id TEXT PRIMARY KEY, -- 'ai_calls', 'data_export', etc.
|
|||
|
|
name TEXT NOT NULL, -- 'KI-Analysen', 'Daten exportieren'
|
|||
|
|
description TEXT,
|
|||
|
|
category TEXT, -- 'ai', 'export', 'data', 'integration'
|
|||
|
|
limit_type TEXT DEFAULT 'count', -- 'count' oder 'boolean'
|
|||
|
|
reset_period TEXT DEFAULT 'never', -- 'never', 'daily', 'monthly'
|
|||
|
|
default_limit INTEGER, -- NULL = unbegrenzt
|
|||
|
|
active BOOLEAN DEFAULT true,
|
|||
|
|
sort_order INTEGER DEFAULT 0,
|
|||
|
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated TIMESTAMP
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Beispiel-Features:**
|
|||
|
|
```sql
|
|||
|
|
-- Count-based mit monatlichem Reset
|
|||
|
|
('ai_calls', 'KI-Analysen', 'KI-Auswertungen pro Monat', 'ai', 'count', 'monthly', 0, true)
|
|||
|
|
|
|||
|
|
-- Boolean-Feature (an/aus)
|
|||
|
|
('ai_pipeline', 'KI-Pipeline', 'Vollständige Pipeline-Analyse', 'ai', 'boolean', 'never', 0, true)
|
|||
|
|
|
|||
|
|
-- Count-based ohne Reset (Gesamt-Limit)
|
|||
|
|
('weight_entries', 'Gewichtseinträge', 'Anzahl Gewichtsmessungen', 'data', 'count', 'never', NULL, true)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Tier-System
|
|||
|
|
|
|||
|
|
**Entscheidung:** Vereinfachte Tier-Tabelle ohne hart-codierte Limits.
|
|||
|
|
|
|||
|
|
**Rationale:**
|
|||
|
|
- Limits werden in separate `tier_limits` Tabelle ausgelagert
|
|||
|
|
- Tiers können dynamisch hinzugefügt werden
|
|||
|
|
- Pricing-Informationen zentral verwaltet
|
|||
|
|
- Flexibilität für zukünftige Tier-Erweiterungen
|
|||
|
|
|
|||
|
|
**Schema:**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE tiers (
|
|||
|
|
id TEXT PRIMARY KEY, -- 'free', 'basic', 'premium', 'selfhosted'
|
|||
|
|
slug TEXT UNIQUE NOT NULL,
|
|||
|
|
name TEXT NOT NULL, -- 'Free', 'Basic', 'Premium', 'Self-Hosted'
|
|||
|
|
description TEXT,
|
|||
|
|
price_monthly DECIMAL(10,2),
|
|||
|
|
price_yearly DECIMAL(10,2),
|
|||
|
|
sort_order INTEGER DEFAULT 0,
|
|||
|
|
active BOOLEAN DEFAULT true,
|
|||
|
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated TIMESTAMP
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Initial Tiers:**
|
|||
|
|
- **free**: Eingeschränkt (30 Daten-Einträge, 0 KI-Calls, kein Export)
|
|||
|
|
- **basic**: Kernfunktionen (unbegrenzte Daten, 3 KI-Calls/Monat, Export erlaubt)
|
|||
|
|
- **premium**: Alles unbegrenzt (inkl. KI-Pipeline, Connectoren)
|
|||
|
|
- **selfhosted**: Admin-Tier für selbst-gehostete Installationen
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. Zugriffs-Hierarchie
|
|||
|
|
|
|||
|
|
**Entscheidung:** Drei-stufige Priorität für effektive Tier-Ermittlung.
|
|||
|
|
|
|||
|
|
**Priorität (höchste zuerst):**
|
|||
|
|
1. **Admin-Override**: `profiles.tier_locked = true` → nutzt `profiles.tier`
|
|||
|
|
2. **Access-Grant**: Aktiver, nicht-pausierter Grant → nutzt `access_grants.granted_tier`
|
|||
|
|
3. **Trial**: `profiles.trial_ends_at > NOW()` → nutzt trial tier
|
|||
|
|
4. **Base Tier**: `profiles.tier`
|
|||
|
|
|
|||
|
|
**Implementierung:**
|
|||
|
|
```python
|
|||
|
|
def get_effective_tier(profile_id: str) -> str:
|
|||
|
|
"""Get effective tier considering all overrides."""
|
|||
|
|
with get_db() as conn:
|
|||
|
|
cur = get_cursor(conn)
|
|||
|
|
|
|||
|
|
# 1. Check if tier is locked by admin
|
|||
|
|
cur.execute("SELECT tier, tier_locked FROM profiles WHERE id = %s", (profile_id,))
|
|||
|
|
profile = cur.fetchone()
|
|||
|
|
if profile['tier_locked']:
|
|||
|
|
return profile['tier']
|
|||
|
|
|
|||
|
|
# 2. Check for active access grant
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT granted_tier FROM access_grants
|
|||
|
|
WHERE profile_id = %s
|
|||
|
|
AND is_active = true
|
|||
|
|
AND valid_from <= CURRENT_TIMESTAMP
|
|||
|
|
AND (valid_until IS NULL OR valid_until > CURRENT_TIMESTAMP)
|
|||
|
|
ORDER BY created DESC LIMIT 1
|
|||
|
|
""", (profile_id,))
|
|||
|
|
grant = cur.fetchone()
|
|||
|
|
if grant:
|
|||
|
|
return grant['granted_tier']
|
|||
|
|
|
|||
|
|
# 3. Check trial
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT tier FROM profiles
|
|||
|
|
WHERE id = %s AND trial_ends_at > CURRENT_TIMESTAMP
|
|||
|
|
""", (profile_id,))
|
|||
|
|
trial = cur.fetchone()
|
|||
|
|
if trial:
|
|||
|
|
return 'premium' # or configurable trial tier
|
|||
|
|
|
|||
|
|
# 4. Base tier
|
|||
|
|
return profile['tier']
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Rationale:**
|
|||
|
|
- Admin kann User dauerhaft einem Tier zuweisen (Support-Fälle)
|
|||
|
|
- Temporäre Zugriffe (Coupons, Wellpass) haben Vorrang vor Base-Tier
|
|||
|
|
- Trial-Logik ist transparent und automatisch
|
|||
|
|
- Base-Tier ist Fallback
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. Coupon-System
|
|||
|
|
|
|||
|
|
**Entscheidung:** 3 Coupon-Typen mit unterschiedlicher Stacking-Logik.
|
|||
|
|
|
|||
|
|
**Typen:**
|
|||
|
|
|
|||
|
|
#### 4.1 Single-Use Coupon
|
|||
|
|
- **Verwendung:** Einmalig einlösbar (z.B. Geschenk-Coupon)
|
|||
|
|
- **Verhalten:** Erstellt `access_grant` mit fester Laufzeit
|
|||
|
|
- **Stacking:** Zeitlich sequenziell (startet nach Ablauf vorheriger Grants)
|
|||
|
|
- **Beispiel:** "30 Tage Premium geschenkt"
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO coupons (code, type, grants_tier, duration_days, max_redemptions) VALUES
|
|||
|
|
('FRIEND-GIFT-ABC', 'single_use', 'premium', 30, 1);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 Multi-Use Period Coupon
|
|||
|
|
- **Verwendung:** Unbegrenzt einlösbar während Gültigkeitszeitraum
|
|||
|
|
- **Verhalten:** Pausiert andere Grants, reaktiviert sie nach Ablauf
|
|||
|
|
- **Stacking:** Override mit Pause/Resume
|
|||
|
|
- **Beispiel:** "Wellpass-Monatszugang März 2026"
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO coupons (code, type, grants_tier, valid_from, valid_until, max_redemptions) VALUES
|
|||
|
|
('WELLPASS-2026-03', 'multi_use_period', 'premium', '2026-03-01', '2026-03-31', NULL);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Stacking-Logik:**
|
|||
|
|
```python
|
|||
|
|
# User hat Single-Use Grant (20 Tage verbleibend)
|
|||
|
|
# User löst Wellpass-Coupon ein
|
|||
|
|
# → Single-Use Grant wird pausiert (is_active=false, paused_by=wellpass_grant_id)
|
|||
|
|
# → Nach Wellpass-Ablauf: Single-Use wird reaktiviert (noch 20 Tage)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.3 Gift Coupon
|
|||
|
|
- **Verwendung:** Vom System generiert als Bonus
|
|||
|
|
- **Verhalten:** Wie Single-Use, aber spezielles Tracking
|
|||
|
|
- **Beispiel:** "Login-Streak Belohnung"
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. User-Restrictions (Override-System)
|
|||
|
|
|
|||
|
|
**Entscheidung:** User-spezifische Overrides haben höchste Priorität.
|
|||
|
|
|
|||
|
|
**Schema:**
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE user_feature_restrictions (
|
|||
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|||
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|||
|
|
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
|||
|
|
limit_value INTEGER, -- NULL = unbegrenzt, überschreibt Tier-Limit
|
|||
|
|
enabled BOOLEAN DEFAULT true,
|
|||
|
|
reason TEXT, -- Warum wurde Override gesetzt?
|
|||
|
|
set_by UUID REFERENCES profiles(id), -- Welcher Admin?
|
|||
|
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated TIMESTAMP,
|
|||
|
|
UNIQUE(profile_id, feature_id)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Anwendungsfälle:**
|
|||
|
|
- Admin gewährt einzelnem User mehr/weniger Zugriff
|
|||
|
|
- Support-Fall: User bekommt temporär mehr KI-Calls
|
|||
|
|
- Beta-Tester: Zugriff auf experimentelle Features
|
|||
|
|
- Problem-User: Einschränkung bestimmter Features
|
|||
|
|
|
|||
|
|
**Beispiel:**
|
|||
|
|
```sql
|
|||
|
|
-- User bekommt 100 KI-Calls/Monat statt Tier-Standard
|
|||
|
|
INSERT INTO user_feature_restrictions (profile_id, feature_id, limit_value, reason, set_by)
|
|||
|
|
VALUES ('user-uuid', 'ai_calls', 100, 'Beta-Tester', 'admin-uuid');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Datenbank-Schema
|
|||
|
|
|
|||
|
|
### Übersicht aller v9c Tabellen
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
v9c Subscription System (11 neue Tabellen):
|
|||
|
|
├── app_settings - Globale App-Konfiguration
|
|||
|
|
├── tiers - Tier-Definitionen (free/basic/premium/selfhosted)
|
|||
|
|
├── features - Feature-Registry (zentrale Feature-Definition)
|
|||
|
|
├── tier_limits - Tier × Feature Matrix (Limits pro Tier)
|
|||
|
|
├── user_feature_restrictions - User-spezifische Overrides
|
|||
|
|
├── user_feature_usage - Usage-Tracking (für reset_period)
|
|||
|
|
├── coupons - Coupon-Verwaltung (3 Typen)
|
|||
|
|
├── coupon_redemptions - Einlösungs-Historie
|
|||
|
|
├── access_grants - Zeitlich begrenzte Zugriffe
|
|||
|
|
├── user_activity_log - Aktivitäts-Tracking (JSONB)
|
|||
|
|
└── user_stats - Aggregierte Statistiken
|
|||
|
|
|
|||
|
|
Erweiterte Tabellen:
|
|||
|
|
└── profiles - Neue Spalten: tier, tier_locked, trial_ends_at,
|
|||
|
|
email_verified, invited_by, contract_type
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Detaillierte Schema-Definitionen
|
|||
|
|
|
|||
|
|
#### app_settings
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE app_settings (
|
|||
|
|
key TEXT PRIMARY KEY,
|
|||
|
|
value TEXT,
|
|||
|
|
value_type TEXT DEFAULT 'string', -- 'string', 'integer', 'boolean', 'json'
|
|||
|
|
description TEXT,
|
|||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_by UUID REFERENCES profiles(id)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
-- Beispiel-Einstellungen:
|
|||
|
|
INSERT INTO app_settings (key, value, value_type, description) VALUES
|
|||
|
|
('trial_days', '14', 'integer', 'Anzahl Tage Trial-Zugang'),
|
|||
|
|
('trial_behavior', 'downgrade', 'string', 'downgrade|lock nach Trial-Ende'),
|
|||
|
|
('allow_registration', 'false', 'boolean', 'Selbst-Registrierung erlaubt?'),
|
|||
|
|
('default_tier_trial', 'premium', 'string', 'Tier während Trial'),
|
|||
|
|
('gift_coupons_per_month', '3', 'integer', 'Max Geschenk-Coupons pro User/Monat');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### tier_limits (Kern der Matrix)
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE tier_limits (
|
|||
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|||
|
|
tier_id TEXT NOT NULL REFERENCES tiers(id) ON DELETE CASCADE,
|
|||
|
|
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
|
|||
|
|
limit_value INTEGER, -- NULL = unbegrenzt, 0 = deaktiviert
|
|||
|
|
enabled BOOLEAN DEFAULT true,
|
|||
|
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated TIMESTAMP,
|
|||
|
|
UNIQUE(tier_id, feature_id)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
-- Beispiel Free Tier:
|
|||
|
|
INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
|
|||
|
|
('free', 'weight_entries', 30),
|
|||
|
|
('free', 'ai_calls', 0), -- Deaktiviert
|
|||
|
|
('free', 'data_export', 0); -- Deaktiviert
|
|||
|
|
|
|||
|
|
-- Beispiel Premium Tier:
|
|||
|
|
INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
|
|||
|
|
('premium', 'weight_entries', NULL), -- Unbegrenzt
|
|||
|
|
('premium', 'ai_calls', NULL), -- Unbegrenzt
|
|||
|
|
('premium', 'ai_pipeline', 1); -- Boolean: Aktiviert
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### access_grants (Zeitliche Zugriffe)
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE access_grants (
|
|||
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|||
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|||
|
|
granted_tier TEXT NOT NULL REFERENCES tiers(id),
|
|||
|
|
valid_from TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
valid_until TIMESTAMP, -- NULL = unbegrenzt
|
|||
|
|
source TEXT, -- 'coupon', 'admin_grant', 'trial'
|
|||
|
|
source_reference TEXT, -- Coupon-Code oder Admin-Notiz
|
|||
|
|
is_active BOOLEAN DEFAULT true, -- Kann pausiert werden
|
|||
|
|
paused_at TIMESTAMP,
|
|||
|
|
paused_by UUID REFERENCES access_grants(id), -- Welcher Grant hat pausiert?
|
|||
|
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
created_by UUID REFERENCES profiles(id)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
-- Index für Performance
|
|||
|
|
CREATE INDEX idx_access_grants_active ON access_grants(profile_id, is_active, valid_until DESC);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### user_activity_log
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE user_activity_log (
|
|||
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|||
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|||
|
|
activity_type TEXT NOT NULL, -- 'login', 'coupon_redeemed', 'tier_change', etc.
|
|||
|
|
details JSONB, -- Flexible Details
|
|||
|
|
ip_address TEXT,
|
|||
|
|
user_agent TEXT,
|
|||
|
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
-- Index für Abfragen
|
|||
|
|
CREATE INDEX idx_activity_log_profile ON user_activity_log(profile_id, created DESC);
|
|||
|
|
|
|||
|
|
-- Beispiel-Einträge:
|
|||
|
|
-- Login
|
|||
|
|
INSERT INTO user_activity_log (profile_id, activity_type, details) VALUES
|
|||
|
|
('uuid', 'login', '{"ip": "192.168.1.1", "device": "Chrome/Mac"}');
|
|||
|
|
|
|||
|
|
-- Coupon eingelöst
|
|||
|
|
INSERT INTO user_activity_log (profile_id, activity_type, details) VALUES
|
|||
|
|
('uuid', 'coupon_redeemed', '{"code": "FRIEND-GIFT-ABC", "tier": "premium", "days": 30}');
|
|||
|
|
|
|||
|
|
-- Tier-Änderung
|
|||
|
|
INSERT INTO user_activity_log (profile_id, activity_type, details) VALUES
|
|||
|
|
('uuid', 'tier_change', '{"from": "free", "to": "basic", "reason": "admin_grant"}');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Backend-API
|
|||
|
|
|
|||
|
|
### Router-Übersicht
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
v9c Backend-Router (7 neue):
|
|||
|
|
├── /api/subscription - User-facing: Abo-Status, Usage, Limits
|
|||
|
|
├── /api/coupons - User: redeem; Admin: CRUD
|
|||
|
|
├── /api/features - Admin: Feature-Registry CRUD + check-access Endpoint
|
|||
|
|
├── /api/tiers - Admin: Tier-Verwaltung CRUD
|
|||
|
|
├── /api/tier-limits - Admin: Matrix-Editor (Tier × Feature)
|
|||
|
|
├── /api/user-restrictions- Admin: User-Override-System
|
|||
|
|
└── /api/access-grants - Admin: Grant-Verwaltung (create, list, revoke)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Wichtige Endpoints
|
|||
|
|
|
|||
|
|
#### User-Facing
|
|||
|
|
|
|||
|
|
**GET /api/subscription/me**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"tier": "basic",
|
|||
|
|
"tier_source": "base", // 'base', 'trial', 'access_grant', 'admin_locked'
|
|||
|
|
"trial_ends_at": null,
|
|||
|
|
"access_grants": [
|
|||
|
|
{
|
|||
|
|
"granted_tier": "premium",
|
|||
|
|
"valid_until": "2026-04-15",
|
|||
|
|
"source": "coupon",
|
|||
|
|
"days_remaining": 25
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**GET /api/subscription/usage**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"ai_calls": {
|
|||
|
|
"limit": 3,
|
|||
|
|
"used": 2,
|
|||
|
|
"remaining": 1,
|
|||
|
|
"reset_at": "2026-04-01T00:00:00"
|
|||
|
|
},
|
|||
|
|
"data_export": {
|
|||
|
|
"limit": 5,
|
|||
|
|
"used": 0,
|
|||
|
|
"remaining": 5,
|
|||
|
|
"reset_at": "2026-04-01T00:00:00"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**POST /api/coupons/redeem**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": "FRIEND-GIFT-ABC"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
Response:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"success": true,
|
|||
|
|
"granted_tier": "premium",
|
|||
|
|
"valid_until": "2026-04-19",
|
|||
|
|
"message": "30 Tage Premium-Zugang aktiviert!"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Admin-Only
|
|||
|
|
|
|||
|
|
**GET /api/tier-limits**
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"tier_id": "free",
|
|||
|
|
"feature_id": "ai_calls",
|
|||
|
|
"limit_value": 0,
|
|||
|
|
"enabled": false
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"tier_id": "basic",
|
|||
|
|
"feature_id": "ai_calls",
|
|||
|
|
"limit_value": 3,
|
|||
|
|
"enabled": true
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**PUT /api/tier-limits**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"tier_id": "basic",
|
|||
|
|
"feature_id": "ai_calls",
|
|||
|
|
"limit_value": 5
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**POST /api/access-grants**
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"profile_id": "user-uuid",
|
|||
|
|
"granted_tier": "premium",
|
|||
|
|
"valid_until": "2026-12-31",
|
|||
|
|
"source": "admin_grant",
|
|||
|
|
"source_reference": "Support-Fall #123"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Frontend-Komponenten
|
|||
|
|
|
|||
|
|
### Admin-UI (vollständig implementiert)
|
|||
|
|
|
|||
|
|
#### 1. AdminFeaturesPage
|
|||
|
|
**Route:** `/admin/features`
|
|||
|
|
|
|||
|
|
**Funktionen:**
|
|||
|
|
- Feature-Liste (sortierbar, filterbar)
|
|||
|
|
- Neues Feature hinzufügen
|
|||
|
|
- Feature bearbeiten (Name, Beschreibung, Limits, Reset-Period)
|
|||
|
|
- Feature deaktivieren (soft-delete)
|
|||
|
|
|
|||
|
|
**UI-Elemente:**
|
|||
|
|
- Feature-Tabelle mit Spalten: Name, Kategorie, Limit-Typ, Reset-Period, Default-Limit
|
|||
|
|
- Modal für Feature-Bearbeitung
|
|||
|
|
- Kategorie-Filter (ai, export, data, integration)
|
|||
|
|
|
|||
|
|
#### 2. AdminTiersPage
|
|||
|
|
**Route:** `/admin/tiers`
|
|||
|
|
|
|||
|
|
**Funktionen:**
|
|||
|
|
- Tier-Liste mit CRUD
|
|||
|
|
- Pricing (monatlich/jährlich) konfigurierbar
|
|||
|
|
- Sort-Order für Anzeige-Reihenfolge
|
|||
|
|
- Tier aktivieren/deaktivieren
|
|||
|
|
|
|||
|
|
#### 3. AdminTierLimitsPage
|
|||
|
|
**Route:** `/admin/tier-limits`
|
|||
|
|
|
|||
|
|
**Funktionen:**
|
|||
|
|
- **Matrix-Editor**: Tiers (Spalten) × Features (Zeilen)
|
|||
|
|
- Responsive: Desktop = Tabelle, Mobile = Cards
|
|||
|
|
- Inline-Editing mit Auto-Save
|
|||
|
|
- Checkbox für Boolean-Features
|
|||
|
|
- Number-Input für Count-Features
|
|||
|
|
- NULL/∞ für unbegrenzt
|
|||
|
|
|
|||
|
|
**UI-Konzept:**
|
|||
|
|
```
|
|||
|
|
┌────────────────┬──────┬───────┬─────────┬────────────┐
|
|||
|
|
│ Feature │ Free │ Basic │ Premium │ Selfhosted │
|
|||
|
|
├────────────────┼──────┼───────┼─────────┼────────────┤
|
|||
|
|
│ Gewicht │ 30 │ ∞ │ ∞ │ ∞ │
|
|||
|
|
│ KI-Calls/Mon │ ☐ 0 │ ☑ 3 │ ☑ ∞ │ ☑ ∞ │
|
|||
|
|
│ KI-Pipeline │ ☐ │ ☐ │ ☑ │ ☑ │
|
|||
|
|
│ Export/Mon │ ☐ 0 │ ☑ 5 │ ☑ ∞ │ ☑ ∞ │
|
|||
|
|
└────────────────┴──────┴───────┴─────────┴────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4. AdminCouponsPage
|
|||
|
|
**Route:** `/admin/coupons`
|
|||
|
|
|
|||
|
|
**Funktionen:**
|
|||
|
|
- Coupon-Liste (Code, Typ, Tier, Gültigkeit, Einlösungen)
|
|||
|
|
- Neuer Coupon (3 Typen)
|
|||
|
|
- Auto-Generate Code (Button)
|
|||
|
|
- Redemption-Historie ansehen
|
|||
|
|
- Coupon deaktivieren
|
|||
|
|
|
|||
|
|
**Coupon-Typen-UI:**
|
|||
|
|
```
|
|||
|
|
[ Single-Use ] [ Multi-Use Period ] [ Gift ]
|
|||
|
|
|
|||
|
|
Single-Use:
|
|||
|
|
Code: [FRIEND-GIFT-___] [Generate]
|
|||
|
|
Tier: [Premium ▼]
|
|||
|
|
Dauer: [30] Tage
|
|||
|
|
Max Einlösungen: [1]
|
|||
|
|
|
|||
|
|
Multi-Use Period:
|
|||
|
|
Code: [WELLPASS-2026-__] [Generate]
|
|||
|
|
Tier: [Premium ▼]
|
|||
|
|
Gültig von: [2026-03-01]
|
|||
|
|
Gültig bis: [2026-03-31]
|
|||
|
|
Max Einlösungen: [∞]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5. AdminUserRestrictionsPage
|
|||
|
|
**Route:** `/admin/user-restrictions`
|
|||
|
|
|
|||
|
|
**Funktionen:**
|
|||
|
|
- User auswählen (Dropdown)
|
|||
|
|
- Aktueller Tier anzeigen
|
|||
|
|
- Feature-Liste mit:
|
|||
|
|
- Tier-Limit (readonly, grau)
|
|||
|
|
- Aktuelle Nutzung
|
|||
|
|
- Override-Eingabe (leer = Tier-Standard)
|
|||
|
|
- Save-Button pro Feature
|
|||
|
|
- "Alle Overrides entfernen" Button
|
|||
|
|
|
|||
|
|
**UI-Konzept:**
|
|||
|
|
```
|
|||
|
|
User: [Lars Stommer ▼] Tier: selfhosted
|
|||
|
|
|
|||
|
|
┌─────────────┬────────────┬─────────┬──────────┬────────┐
|
|||
|
|
│ Feature │ Tier-Limit │ Genutzt │ Override │ Aktion │
|
|||
|
|
├─────────────┼────────────┼─────────┼──────────┼────────┤
|
|||
|
|
│ KI-Calls │ ∞ │ 5/∞ │ [100] │ [Save] │
|
|||
|
|
│ Export │ ∞ │ 2/∞ │ [ ] │ [Save] │
|
|||
|
|
│ Gewicht │ ∞ │ 50/∞ │ [ ] │ [Save] │
|
|||
|
|
└─────────────┴────────────┴─────────┴──────────┴────────┘
|
|||
|
|
|
|||
|
|
[Alle Overrides entfernen]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6. SubscriptionPage (User-facing)
|
|||
|
|
**Route:** `/subscription`
|
|||
|
|
|
|||
|
|
**Funktionen:**
|
|||
|
|
- Aktueller Tier + Quelle
|
|||
|
|
- Tier-Badge mit Icon
|
|||
|
|
- Feature-Liste mit Limits
|
|||
|
|
- Usage-Progress-Bars
|
|||
|
|
- Coupon-Einlösung
|
|||
|
|
- Access-Grant-Historie
|
|||
|
|
|
|||
|
|
**UI-Konzept:**
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ Dein Abo: [🟢 PREMIUM] │
|
|||
|
|
│ Quelle: Coupon (noch 25 Tage) │
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
Features & Limits:
|
|||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|||
|
|
KI-Analysen 5/10
|
|||
|
|
[████████░░] 50% Reset: 1.4.2026
|
|||
|
|
|
|||
|
|
Daten-Export 2/5
|
|||
|
|
[████░░░░░░] 40% Reset: 1.4.2026
|
|||
|
|
|
|||
|
|
Gewichtseinträge 120/∞
|
|||
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|||
|
|
|
|||
|
|
Coupon einlösen:
|
|||
|
|
[_________________] [Einlösen]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Feature-Enforcement-System
|
|||
|
|
|
|||
|
|
### Status: ⚠️ DEAKTIVIERT (Rollback 20.03.2026)
|
|||
|
|
|
|||
|
|
**Versuch-Implementation:** Commits 3745ebd, cbad50a, cd4d912, 8415509
|
|||
|
|
**Rollback:** Commit 4fcde4a
|
|||
|
|
|
|||
|
|
### Was war geplant
|
|||
|
|
|
|||
|
|
**Backend:**
|
|||
|
|
```python
|
|||
|
|
# In jedem limitierten Endpoint
|
|||
|
|
def some_endpoint(session = Depends(require_auth)):
|
|||
|
|
pid = session['profile_id']
|
|||
|
|
|
|||
|
|
# 1. Feature-Check
|
|||
|
|
access = check_feature_access(pid, 'feature_slug')
|
|||
|
|
if not access['allowed']:
|
|||
|
|
if access['reason'] == 'feature_disabled':
|
|||
|
|
raise HTTPException(403, "Feature nicht verfügbar")
|
|||
|
|
elif access['reason'] == 'limit_exceeded':
|
|||
|
|
raise HTTPException(429, f"Limit erreicht ({access['limit']})")
|
|||
|
|
|
|||
|
|
# 2. Funktion ausführen
|
|||
|
|
result = do_something()
|
|||
|
|
|
|||
|
|
# 3. Usage inkrementieren
|
|||
|
|
increment_feature_usage(pid, 'feature_slug')
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Frontend:**
|
|||
|
|
```jsx
|
|||
|
|
import { FeatureGate, FeatureBadge } from '../components/FeatureGate'
|
|||
|
|
|
|||
|
|
function MyComponent() {
|
|||
|
|
return (
|
|||
|
|
<FeatureGate feature="ai_calls" showUpgradePrompt>
|
|||
|
|
<button onClick={runAnalysis}>
|
|||
|
|
KI-Analyse starten
|
|||
|
|
<FeatureBadge feature="ai_calls" />
|
|||
|
|
</button>
|
|||
|
|
</FeatureGate>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Was schief ging
|
|||
|
|
|
|||
|
|
1. **Frontend-Cache-Problem**
|
|||
|
|
- FeatureGate cached Feature-Status
|
|||
|
|
- Änderungen im Admin-Panel nicht sofort sichtbar
|
|||
|
|
- Optimistic rendering zeigte Features kurz an bevor Block
|
|||
|
|
|
|||
|
|
2. **Backend-Inkonsistenzen**
|
|||
|
|
- Feature-IDs stimmten nicht mit DB überein (data_export vs export_csv)
|
|||
|
|
- Manche Features fehlten komplett (csv_import)
|
|||
|
|
- increment_feature_usage() hatte silent failures
|
|||
|
|
|
|||
|
|
3. **Analyse-History zerstört**
|
|||
|
|
- DELETE vor INSERT entfernt, aber das war GEWOLLT im Original
|
|||
|
|
- Führte zu "keine Historisierung"-Beschwerde
|
|||
|
|
- War Missverständnis der Original-Logik
|
|||
|
|
|
|||
|
|
4. **Export-Buttons verschwunden**
|
|||
|
|
- FeatureGate blockierte sofort
|
|||
|
|
- Migration nicht gelaufen → Features existierten nicht
|
|||
|
|
- canExport-Flag wurde überschrieben
|
|||
|
|
|
|||
|
|
5. **Pipeline-Duplikate**
|
|||
|
|
- Filter ließ 'pipeline' Prompt durch
|
|||
|
|
- Scope-Bug: speicherte als 'gesamt' statt 'pipeline'
|
|||
|
|
|
|||
|
|
### Lessons Learned
|
|||
|
|
|
|||
|
|
1. **Nie ohne vollständiges Verständnis refactorn**
|
|||
|
|
- DELETE-Logik war absichtlich (1 Analyse pro Scope)
|
|||
|
|
- User wollte aber History → Requirements unklar
|
|||
|
|
|
|||
|
|
2. **Migrations müssen atomar laufen**
|
|||
|
|
- Features müssen VOR Enforcement existieren
|
|||
|
|
- Auto-Migration muss zuverlässig sein
|
|||
|
|
- Test-Daten für lokale Entwicklung
|
|||
|
|
|
|||
|
|
3. **Frontend-Gates brauchen Refresh-Mechanismus**
|
|||
|
|
- WebSocket oder Polling nach Admin-Änderungen
|
|||
|
|
- Oder: "Neu laden" Button prominent anzeigen
|
|||
|
|
- Optimistic rendering ist riskant
|
|||
|
|
|
|||
|
|
4. **Feature-IDs konsolidieren**
|
|||
|
|
- Ein Feature für Export, nicht drei (csv/json/zip)
|
|||
|
|
- Konsistent zwischen DB, Backend-Code und Frontend
|
|||
|
|
|
|||
|
|
5. **Inkrementelle Einführung**
|
|||
|
|
- Erst Backend-Checks als Logs (nicht blockierend)
|
|||
|
|
- Dann Frontend-Display (aber funktional)
|
|||
|
|
- Dann Enforcement aktivieren nach Tests
|
|||
|
|
|
|||
|
|
### Nächste Schritte für Re-Implementation
|
|||
|
|
|
|||
|
|
1. **Phase 1: Cleanup**
|
|||
|
|
- Feature-Definitionen konsolidieren
|
|||
|
|
- Migration auf Idempotenz prüfen
|
|||
|
|
- Test-Daten-Script erstellen
|
|||
|
|
|
|||
|
|
2. **Phase 2: Backend Non-Blocking**
|
|||
|
|
- Feature-Checks in Endpoints einbauen
|
|||
|
|
- Aber nur loggen, nicht blockieren
|
|||
|
|
- Monitoring: Wie oft würde blockiert?
|
|||
|
|
|
|||
|
|
3. **Phase 3: Frontend Display**
|
|||
|
|
- Usage-Counter anzeigen (ohne Gates)
|
|||
|
|
- Admin kann sehen was genutzt wird
|
|||
|
|
- Validierung gegen tatsächliche API-Calls
|
|||
|
|
|
|||
|
|
4. **Phase 4: Enforcement (opt-in)**
|
|||
|
|
- Per Feature-Flag aktivierbar
|
|||
|
|
- Erst für Admin-Accounts testen
|
|||
|
|
- Dann für Test-User
|
|||
|
|
- Dann Rollout
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Roadmap
|
|||
|
|
|
|||
|
|
### v9c - Fertigstellung (Q2 2026)
|
|||
|
|
|
|||
|
|
**Prio 1: Feature-Enforcement (Redesign)**
|
|||
|
|
- ✅ Backend-Checks als Log-Only implementieren
|
|||
|
|
- ✅ Frontend Usage-Display ohne Gates
|
|||
|
|
- ✅ Feature-ID Konsolidierung
|
|||
|
|
- ✅ Test-Suite für alle Limits
|
|||
|
|
- ⏳ Opt-in Enforcement per Feature-Flag
|
|||
|
|
- ⏳ Rollout-Plan mit Rollback-Option
|
|||
|
|
|
|||
|
|
**Prio 2: Registrierung & Trial**
|
|||
|
|
- Self-Registration mit E-Mail-Verifizierung
|
|||
|
|
- Automatischer Trial-Start (14 Tage Premium)
|
|||
|
|
- Trial-Countdown-Banner
|
|||
|
|
- Auto-Downgrade nach Trial-Ende
|
|||
|
|
|
|||
|
|
**Prio 3: App-Settings UI**
|
|||
|
|
- Admin-Panel für globale Konfiguration
|
|||
|
|
- Trial-Einstellungen (Dauer, Verhalten)
|
|||
|
|
- Registrierungs-Toggle
|
|||
|
|
- E-Mail-Template-Editor
|
|||
|
|
|
|||
|
|
### v9d - Monetarisierung (Q3 2026)
|
|||
|
|
|
|||
|
|
**Prio 1: Stripe-Integration**
|
|||
|
|
- Self-Service Upgrade (Premium)
|
|||
|
|
- Subscription-Management
|
|||
|
|
- Webhook-Handler
|
|||
|
|
- Rechnungs-E-Mails
|
|||
|
|
|
|||
|
|
**Prio 2: Bonus-System**
|
|||
|
|
- Login-Streak-Tracking
|
|||
|
|
- Punkte-System
|
|||
|
|
- Geschenk-Coupons (automatisch)
|
|||
|
|
- Achievements
|
|||
|
|
|
|||
|
|
**Prio 3: Fitness-Connectoren**
|
|||
|
|
- OAuth2-Framework
|
|||
|
|
- Strava-Connector
|
|||
|
|
- Withings-Connector (Waage)
|
|||
|
|
- Garmin-Connector
|
|||
|
|
|
|||
|
|
### v9e - Partner & Enterprise (Q4 2026)
|
|||
|
|
|
|||
|
|
**Prio 1: Partner-Integration**
|
|||
|
|
- Wellpass-Authentifizierung
|
|||
|
|
- Hansefit-Authentifizierung
|
|||
|
|
- Partner-Admin-UI
|
|||
|
|
- Usage-Reporting für Partner
|
|||
|
|
|
|||
|
|
**Prio 2: Enterprise Features**
|
|||
|
|
- Multi-Tenant-Support
|
|||
|
|
- White-Label-Option
|
|||
|
|
- SAML/SSO
|
|||
|
|
- API-Keys für Drittanbieter
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Technische Details
|
|||
|
|
|
|||
|
|
### Performance-Überlegungen
|
|||
|
|
|
|||
|
|
**check_feature_access() Caching:**
|
|||
|
|
- Cache auf Request-Level (nicht global)
|
|||
|
|
- Verhindert multiple DB-Calls pro Request
|
|||
|
|
- TTL: 5 Minuten für Frontend-Checks
|
|||
|
|
|
|||
|
|
**Database Indices:**
|
|||
|
|
```sql
|
|||
|
|
-- Kritische Indices für Performance
|
|||
|
|
CREATE INDEX idx_tier_limits_lookup ON tier_limits(tier_id, feature_id);
|
|||
|
|
CREATE INDEX idx_user_restrictions_lookup ON user_feature_restrictions(profile_id, feature_id);
|
|||
|
|
CREATE INDEX idx_feature_usage_lookup ON user_feature_usage(profile_id, feature_id, reset_at);
|
|||
|
|
CREATE INDEX idx_access_grants_active ON access_grants(profile_id, is_active, valid_until DESC);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Security-Considerations
|
|||
|
|
|
|||
|
|
**Coupon-Code-Generation:**
|
|||
|
|
- Kryptografisch sicherer Zufallsgenerator
|
|||
|
|
- 12 Zeichen: XXXXX-YYYYY-ZZ
|
|||
|
|
- Kollisions-Check vor INSERT
|
|||
|
|
|
|||
|
|
**Access-Grant-Validation:**
|
|||
|
|
- Zeitstempel-Checks auf DB-Ebene (PostgreSQL TIMESTAMP)
|
|||
|
|
- Keine Client-side Validierung für Enforcement
|
|||
|
|
- is_active Flag für sofortigen Widerruf
|
|||
|
|
|
|||
|
|
**User-Restrictions:**
|
|||
|
|
- Nur Admins können setzen
|
|||
|
|
- Audit-Log (set_by, reason)
|
|||
|
|
- Kann nicht via User-API manipuliert werden
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Testing-Strategie
|
|||
|
|
|
|||
|
|
### Unit-Tests (Backend)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def test_check_feature_access_user_override():
|
|||
|
|
# Setup: User mit Free-Tier + Override für ai_calls=100
|
|||
|
|
profile_id = create_test_profile(tier='free')
|
|||
|
|
set_user_restriction(profile_id, 'ai_calls', 100)
|
|||
|
|
|
|||
|
|
# Test: Override hat Vorrang vor Tier-Limit
|
|||
|
|
access = check_feature_access(profile_id, 'ai_calls')
|
|||
|
|
assert access['allowed'] == True
|
|||
|
|
assert access['limit'] == 100
|
|||
|
|
|
|||
|
|
def test_coupon_stacking_pause_resume():
|
|||
|
|
# Setup: Single-Use Grant aktiv
|
|||
|
|
profile_id = create_test_profile()
|
|||
|
|
grant1 = create_access_grant(profile_id, 'premium', days=30)
|
|||
|
|
|
|||
|
|
# Test: Multi-Use Coupon pausiert Single-Use
|
|||
|
|
redeem_coupon(profile_id, 'WELLPASS-CODE')
|
|||
|
|
grant1_reloaded = get_access_grant(grant1.id)
|
|||
|
|
assert grant1_reloaded['is_active'] == False
|
|||
|
|
|
|||
|
|
# Test: Nach Wellpass-Ablauf wird Single-Use reaktiviert
|
|||
|
|
expire_wellpass_grant(profile_id)
|
|||
|
|
grant1_reloaded = get_access_grant(grant1.id)
|
|||
|
|
assert grant1_reloaded['is_active'] == True
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Integration-Tests (API)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Scenario: Free User versucht KI-Analyse
|
|||
|
|
curl -X POST /api/insights/run/gesamt \
|
|||
|
|
-H "X-Auth-Token: $TOKEN" \
|
|||
|
|
-H "X-Profile-Id: $FREE_USER_ID"
|
|||
|
|
|
|||
|
|
# Expected: HTTP 403 "KI nicht verfügbar"
|
|||
|
|
|
|||
|
|
# Scenario: User löst Coupon ein
|
|||
|
|
curl -X POST /api/coupons/redeem \
|
|||
|
|
-H "X-Auth-Token: $TOKEN" \
|
|||
|
|
-d '{"code": "FRIEND-GIFT-ABC"}'
|
|||
|
|
|
|||
|
|
# Expected: HTTP 200 + Access-Grant erstellt
|
|||
|
|
|
|||
|
|
# Scenario: User mit Premium-Grant kann KI nutzen
|
|||
|
|
curl -X POST /api/insights/run/gesamt \
|
|||
|
|
-H "X-Auth-Token: $TOKEN" \
|
|||
|
|
-H "X-Profile-Id: $FREE_USER_ID"
|
|||
|
|
|
|||
|
|
# Expected: HTTP 200 + Analyse-Ergebnis
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Deployment-Notes
|
|||
|
|
|
|||
|
|
### Migration-Reihenfolge
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. Backup
|
|||
|
|
pg_dump mitai_prod > backup_before_v9c.sql
|
|||
|
|
|
|||
|
|
# 2. v9c Schema-Migration
|
|||
|
|
psql mitai_prod < backend/migrations/v9c_subscription_system.sql
|
|||
|
|
|
|||
|
|
# 3. Feature-Fixes (falls nötig)
|
|||
|
|
psql mitai_prod < backend/migrations/v9c_fix_features.sql
|
|||
|
|
|
|||
|
|
# 4. Backend neu starten (Auto-Migration läuft)
|
|||
|
|
docker compose restart backend
|
|||
|
|
|
|||
|
|
# 5. Verifizieren
|
|||
|
|
docker logs mitai-api | grep "v9c Migration"
|
|||
|
|
# Expected: "✅ Migration completed successfully!"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Rollback-Plan
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- Emergency Rollback v9c
|
|||
|
|
DROP TABLE IF EXISTS user_stats CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS user_activity_log CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS coupon_redemptions CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS coupons CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS access_grants CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS user_feature_usage CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS user_feature_restrictions CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS tier_limits CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS features CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS tiers CASCADE;
|
|||
|
|
DROP TABLE IF EXISTS app_settings CASCADE;
|
|||
|
|
|
|||
|
|
-- Profiles-Spalten entfernen
|
|||
|
|
ALTER TABLE profiles
|
|||
|
|
DROP COLUMN tier,
|
|||
|
|
DROP COLUMN tier_locked,
|
|||
|
|
DROP COLUMN trial_ends_at,
|
|||
|
|
DROP COLUMN email_verified,
|
|||
|
|
DROP COLUMN email_verify_token,
|
|||
|
|
DROP COLUMN invited_by,
|
|||
|
|
DROP COLUMN contract_type,
|
|||
|
|
DROP COLUMN contract_valid_until,
|
|||
|
|
DROP COLUMN stripe_customer_id;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Support & Troubleshooting
|
|||
|
|
|
|||
|
|
### Häufige Probleme
|
|||
|
|
|
|||
|
|
**Problem: User kann Feature nicht nutzen**
|
|||
|
|
```sql
|
|||
|
|
-- Diagnose: Effektiven Tier prüfen
|
|||
|
|
SELECT tier, tier_locked, trial_ends_at FROM profiles WHERE id = 'user-uuid';
|
|||
|
|
|
|||
|
|
-- Diagnose: Access-Grants prüfen
|
|||
|
|
SELECT * FROM access_grants
|
|||
|
|
WHERE profile_id = 'user-uuid'
|
|||
|
|
AND is_active = true
|
|||
|
|
ORDER BY created DESC;
|
|||
|
|
|
|||
|
|
-- Diagnose: Feature-Limit prüfen
|
|||
|
|
SELECT tl.* FROM tier_limits tl
|
|||
|
|
WHERE tier_id = (SELECT tier FROM profiles WHERE id = 'user-uuid')
|
|||
|
|
AND feature_id = 'ai_calls';
|
|||
|
|
|
|||
|
|
-- Diagnose: User-Override prüfen
|
|||
|
|
SELECT * FROM user_feature_restrictions
|
|||
|
|
WHERE profile_id = 'user-uuid' AND feature_id = 'ai_calls';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Problem: Coupon lässt sich nicht einlösen**
|
|||
|
|
```sql
|
|||
|
|
-- Diagnose: Coupon gültig?
|
|||
|
|
SELECT * FROM coupons WHERE code = 'COUPON-CODE';
|
|||
|
|
|
|||
|
|
-- Check: Bereits eingelöst?
|
|||
|
|
SELECT * FROM coupon_redemptions
|
|||
|
|
WHERE coupon_id = (SELECT id FROM coupons WHERE code = 'COUPON-CODE')
|
|||
|
|
AND profile_id = 'user-uuid';
|
|||
|
|
|
|||
|
|
-- Check: Max Einlösungen erreicht?
|
|||
|
|
SELECT c.max_redemptions, COUNT(cr.*) as current_redemptions
|
|||
|
|
FROM coupons c
|
|||
|
|
LEFT JOIN coupon_redemptions cr ON cr.coupon_id = c.id
|
|||
|
|
WHERE c.code = 'COUPON-CODE'
|
|||
|
|
GROUP BY c.id, c.max_redemptions;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Admin-Tools
|
|||
|
|
|
|||
|
|
**User-Tier manuell ändern:**
|
|||
|
|
```sql
|
|||
|
|
-- Tier setzen und locken
|
|||
|
|
UPDATE profiles SET tier = 'premium', tier_locked = true WHERE id = 'user-uuid';
|
|||
|
|
|
|||
|
|
-- Access-Grant manuell erstellen
|
|||
|
|
INSERT INTO access_grants (profile_id, granted_tier, valid_until, source, source_reference)
|
|||
|
|
VALUES ('user-uuid', 'premium', '2026-12-31', 'admin_grant', 'Support-Fall #123');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Feature-Usage zurücksetzen:**
|
|||
|
|
```sql
|
|||
|
|
-- Alle Usage für User zurücksetzen
|
|||
|
|
DELETE FROM user_feature_usage WHERE profile_id = 'user-uuid';
|
|||
|
|
|
|||
|
|
-- Nur bestimmtes Feature zurücksetzen
|
|||
|
|
DELETE FROM user_feature_usage
|
|||
|
|
WHERE profile_id = 'user-uuid' AND feature_id = 'ai_calls';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Anhang
|
|||
|
|
|
|||
|
|
### Glossar
|
|||
|
|
|
|||
|
|
- **Tier**: Subscription-Stufe (free, basic, premium, selfhosted)
|
|||
|
|
- **Feature**: Limitierbare Funktionalität (ai_calls, data_export, etc.)
|
|||
|
|
- **Limit**: Maximale Anzahl Nutzungen (count) oder an/aus (boolean)
|
|||
|
|
- **Access-Grant**: Zeitlich begrenzte Tier-Berechtigung
|
|||
|
|
- **Coupon**: Einlösbarer Code für Tier-Zugang
|
|||
|
|
- **Override**: User-spezifische Abweichung vom Tier-Limit
|
|||
|
|
- **Reset-Period**: Zeitraum nach dem Limit zurückgesetzt wird (never, daily, monthly)
|
|||
|
|
|
|||
|
|
### Kontakt & Fragen
|
|||
|
|
|
|||
|
|
- **Repository**: http://192.168.2.144:3000/Lars/mitai-jinkendo
|
|||
|
|
- **Dokumentation**: `/docs/` im Repository
|
|||
|
|
- **Issues**: Gitea Issues oder direkt an Lars
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Letzte Aktualisierung:** 20. März 2026
|
|||
|
|
**Autor:** Lars Stommer + Claude Opus 4.6
|
|||
|
|
**Version:** v9c-dev
|