Updates: - CLAUDE.md: Reflect current v9c-dev status (enforcement disabled, history working) - CLAUDE.md: Document simple AI limit system currently active - CLAUDE.md: Update implementation status (admin UI complete, enforcement rolled back) New Documentation: - docs/MEMBERSHIP_SYSTEM.md: Complete v9c architecture documentation - Design decisions and rationale - Complete database schema (11 tables) - Backend API overview (7 routers, 30+ endpoints) - Frontend components (6 admin pages) - Feature enforcement rollback analysis - Lessons learned and next steps - Testing strategy - Deployment notes - Troubleshooting guide The new doc provides complete reference for: - Feature-Registry-Pattern implementation - Tier system architecture - Coupon system (3 types with stacking logic) - User-Override system - Access-Grant mechanics - What went wrong with enforcement attempt - Roadmap for v9d/v9e Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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
|