diff --git a/CLAUDE.md b/CLAUDE.md index 06fbe4b..db7f837 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -96,19 +96,468 @@ mitai-jinkendo/ - ✅ Automatische SQLite→PostgreSQL Migration bei Container-Start - ✅ **Modulare Backend-Architektur**: 14 Router-Module, main.py von 1878→75 Zeilen (-96%) -### Was in v9c kommt: -- 🔲 Selbst-Registrierung mit E-Mail-Bestätigung -- 🔲 Freemium Tier-System (free/basic/premium/selfhosted) -- 🔲 14-Tage Trial automatisch -- 🔲 Einladungslinks für Beta-Nutzer -- 🔲 Admin kann Tiers manuell setzen +### Was in v9c kommt: Subscription & Coupon Management System +**Core Features:** +- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung (Pflicht) +- 🔲 Flexibles Tier-System (free/basic/premium/selfhosted) - Admin-editierbar +- 🔲 Trial-System (Dauer konfigurierbar, auto-start nach E-Mail-Verifikation) +- 🔲 **Coupon-System** (2 Typen): + - Single-Use Coupons (Geschenke, zeitlich begrenzt) + - Multi-Use Period Coupons (z.B. Wellpass, monatlich erneuerbar) +- 🔲 Coupon-Stacking-Logik (Pause + Resume bei Wellpass-Override) +- 🔲 Access-Grant-System (zeitlich begrenzte Zugriffe mit Quelle-Tracking) +- 🔲 User-Activity-Log (Login, Password-Änderungen, Coupon-Einlösungen, etc.) +- 🔲 User-Stats (Login-Streaks, Nutzungsstatistiken) +- 🔲 Individuelle User-Restrictions (Admin kann Limits pro User setzen) +- 🔲 App-Settings (globale Konfiguration durch Admin) +- 🔲 Erweiterte Admin-User-Verwaltung (Activity-Log, Stats, Access-Historie) + +**E-Mail Templates (v9c):** +- 🔲 Registrierung + E-Mail-Verifizierung +- 🔲 Einladungslink +- 🔲 Passwort-Reset (bereits vorhanden) + +**Spätere Features (v9d/v9e):** +- 🔲 Bonus-System (Login-Streaks → Punkte → Geschenk-Coupons) +- 🔲 Trial-Reminder-E-Mails (3 Tage vor Ablauf) +- 🔲 Monatliches Nutzungs-Summary per E-Mail +- 🔲 Self-Service Upgrade (Stripe-Integration) +- 🔲 Partner-Verwaltung (Wellpass, Hansefit, etc.) +- 🔲 Admin-Benachrichtigungen (neue Registrierungen, etc.) ### Was in v9d kommt: +- 🔲 Bonus-System & Gamification (Streaks, Achievements) +- 🔲 Stripe-Integration (Self-Service Upgrade, Subscriptions) - 🔲 OAuth2-Grundgerüst für Fitness-Connectoren - 🔲 Strava Connector - 🔲 Withings Connector (Waage) - 🔲 Garmin Connector +--- + +## v9c Architektur-Details: Subscription & Coupon System + +### Datenbank-Schema (Neue Tabellen) + +#### **app_settings** - Globale Konfiguration +```sql +key, value, value_type, description, updated_at, updated_by + +Beispiele: +- trial_days: 14 +- trial_behavior: 'downgrade' | 'lock' +- allow_registration: true/false +- default_tier_trial: 'premium' +- gift_coupons_per_month: 3 +``` + +#### **tiers** - Tier-Konfiguration (vereinfacht) +```sql +id, slug, name, description, sort_order, active + +Initial Tiers: +- free, basic, premium, selfhosted + +Limits sind jetzt in tier_limits Tabelle (siehe unten)! +``` + +#### **features** - Feature-Registry (alle limitierbaren Features) +```sql +id, slug, name, category, description, unit +default_limit (NULL = unbegrenzt) +reset_period ('monthly' | 'daily' | 'never') +visible_in_admin, sort_order, active + +Initial Features: +- weight_entries: Gewichtseinträge, default: 30, never +- circumference_entries: Umfangsmessungen, default: 30, never +- caliper_entries: Caliper-Messungen, default: 30, never +- nutrition_entries: Ernährungseinträge, default: 30, never +- activity_entries: Aktivitäten, default: 30, never +- photos: Progress-Fotos, default: 5, never +- ai_calls: KI-Analysen, default: 0, monthly +- ai_pipeline: KI-Pipeline, default: 0, monthly +- csv_import: CSV-Importe, default: 0, monthly +- data_export: Daten-Exporte, default: 0, monthly +- fitness_connectors: Fitness-Connectoren, default: 0, never + +Neue Features einfach per INSERT hinzufügen - kein Schema-Change! +``` + +#### **tier_limits** - Limits pro Tier + Feature +```sql +id, tier_slug, feature_slug, limit_value, enabled + +Beispiel Free Tier: +- ('free', 'weight_entries', 30, true) +- ('free', 'ai_calls', 0, false) -- KI deaktiviert +- ('free', 'data_export', 0, false) + +Beispiel Premium: +- ('premium', 'weight_entries', NULL, true) -- unbegrenzt +- ('premium', 'ai_calls', NULL, true) -- unbegrenzt + +Admin kann in UI Matrix bearbeiten: Tier x Feature +``` + +#### **user_feature_restrictions** - Individuelle User-Limits +```sql +id, profile_id, feature_slug, limit_value, enabled +reason, set_by (admin_id) + +Überschreibt Tier-Limits für spezifische User. +Admin kann jeden User individuell einschränken oder erweitern. +``` + +#### **user_feature_usage** - Nutzungs-Tracking +```sql +id, profile_id, feature_slug, period_start, usage_count, last_used + +Für Features mit reset_period (z.B. ai_calls monthly). +Wird automatisch zurückgesetzt am Monatsanfang. +``` + +#### **coupons** - Coupon-Verwaltung +```sql +code, type ('single_use' | 'multi_use_period' | 'gift') +valid_from, valid_until, grants_tier, duration_days +max_redemptions, current_redemptions +created_by, created_for, notes, active + +Beispiel Single-Use: + Code: FRIEND-GIFT-XYZ, 30 Tage Premium, max 1x + +Beispiel Multi-Use Period: + Code: WELLPASS-2026-03, gültig 01.03-31.03, unbegrenzte Einlösungen +``` + +#### **coupon_redemptions** - Einlösungs-Historie +```sql +coupon_id, profile_id, redeemed_at, access_grant_id +UNIQUE(coupon_id, profile_id) - User kann denselben Coupon nur 1x einlösen +``` + +#### **access_grants** - Zeitlich begrenzte Zugriffe +```sql +profile_id, granted_tier, valid_from, valid_until +source ('coupon' | 'admin_grant' | 'trial') +active (false wenn pausiert durch Wellpass-Override) +paused_at, paused_by (access_grant_id das pausiert hat) + +Stacking-Logik: +- Multi-Use Period Coupon (Wellpass): pausiert andere grants +- Single-Use Coupon: stackt zeitlich (Resume nach Ablauf) +``` + +#### **user_activity_log** - Aktivitäts-Tracking +```sql +profile_id, activity_type, details (JSONB), ip_address, user_agent, created + +Activity Types: +- login, password_change, email_change, coupon_redeemed +- tier_change, export, ai_analysis, registration +``` + +#### **user_stats** - Aggregierte Statistiken +```sql +profile_id, first_login, last_login, total_logins +current_streak_days, longest_streak_days, last_streak_date +total_weight_entries, total_ai_analyses, total_exports +bonus_points (später), gift_coupons_available (später) +``` + +#### **profiles** - Erweiterte Spalten +```sql +tier, tier_locked (Admin kann Tier festnageln) +trial_ends_at, email_verified, email_verify_token +invited_by, contract_type, contract_valid_until +stripe_customer_id (vorbereitet für v9d) +``` + +--- + +### Backend-Erweiterungen + +#### Neue Router (v9c): +``` +routers/tiers.py - Tier-Verwaltung (List, Edit, Create) +routers/features.py - Feature-Registry (List, Add, Edit, Delete) ⭐ NEU +routers/tier_limits.py - Tier-Limits-Matrix (Admin bearbeitet Tier x Feature) ⭐ NEU +routers/coupons.py - Coupon-System (Redeem, Admin CRUD) +routers/access_grants.py - Zugriffs-Verwaltung (Current, Grant, Revoke) +routers/user_admin.py - Erweiterte User-Verwaltung (Activity, Stats, Feature-Restrictions) +routers/settings.py - App-Einstellungen (Admin) +routers/registration.py - Registrierung + E-Mail-Verifizierung +``` + +#### Neue Middleware: +```python +check_feature_access(profile_id, feature_slug, action='use') + """ + Zentrale Feature-Access-Prüfung. + Hierarchie: + 1. User-Restriction (höchste Priorität) + 2. Tier-Limit + 3. Feature-Default + + Returns: {'allowed': bool, 'limit': int, 'used': int, 'remaining': int, 'reason': str} + """ + +increment_feature_usage(profile_id, feature_slug) + """ + Inkrementiert Nutzungszähler. + Berücksichtigt reset_period (monthly, daily, never). + """ + +log_activity(profile_id, activity_type, details=None) + """ + Loggt User-Aktivitäten in user_activity_log. + """ +``` + +#### Hintergrund-Tasks (Cron): +```python +check_expired_access() # Täglich 00:00 - Trial/Coupon-Ablauf prüfen +reset_monthly_limits() # 1. jeden Monats - AI-Calls zurücksetzen +update_user_streaks() # Täglich 23:59 - Login-Streaks aktualisieren +``` + +--- + +### Zugriffs-Hierarchie + +``` +Effektiver Tier wird ermittelt durch (Priorität absteigend): +1. Admin-Override (tier_locked=true) → nutzt profiles.tier +2. Aktiver access_grant (nicht pausiert, valid_until > now) +3. Trial (trial_ends_at > now) +4. Base tier (profiles.tier) + +Wellpass-Override-Logik: +- User hat Single-Use Coupon (20 Tage verbleibend) +- User löst Wellpass-Coupon ein (gültig bis 31.03) +- Single-Use access_grant wird pausiert (active=false, paused_by=wellpass_grant_id) +- Nach Wellpass-Ablauf: Single-Use wird reaktiviert (noch 20 Tage) +``` + +--- + +### Tier-Limits & Feature-Gates + +**Daten-Sichtbarkeit bei Downgrade:** +- Frontend: Buttons/Features ausblenden (Export, KI, Import) +- Backend: API limitiert Rückgabe (z.B. nur letzte 30 Gewichtseinträge bei free) +- Daten bleiben erhalten, werden nur versteckt +- Bei Upgrade wieder sichtbar + +**Feature-Checks:** +```python +# Beispiel: Gewicht-Eintrag erstellen +@check_feature_limit('weight', 'create') +def create_weight_entry(): + # Prüft: Hat User max_weight_entries erreicht? + # Falls ja: HTTPException 403 "Limit erreicht - Upgrade erforderlich" +``` + +--- + +### Frontend-Erweiterungen + +#### Neue Seiten: +``` +RegisterPage.jsx - Registrierung (Name, E-Mail, Passwort) +VerifyEmailPage.jsx - E-Mail-Verifizierung (Token aus URL) +RedeemCouponPage.jsx - Coupon-Eingabe (oder Modal) +AdminCouponsPage.jsx - Coupon-Verwaltung (Admin) +AdminTiersPage.jsx - Tier-Verwaltung (CRUD) (Admin) +AdminFeaturesPage.jsx - Feature-Registry (List, Add, Edit) ⭐ NEU +AdminTierLimitsPage.jsx - Tier x Feature Matrix (bearbeiten) ⭐ NEU +AdminUserRestrictionsPage.jsx - User-spezifische Limits (bearbeiten) ⭐ NEU +AdminSettingsPage.jsx - App-Einstellungen (Admin) +``` + +#### Neue Komponenten: +```jsx + // Tier-Anzeige mit Icon +... // Feature-basierte Sichtbarkeit ⭐ GEÄNDERT + // "Trial endet in 5 Tagen" Banner + // Coupon-Eingabefeld + // User-Activity-Log + // "5/10 verwendet" Anzeige ⭐ NEU + // Matrix-Editor ⭐ NEU + // Login-Streak (später) +``` + +#### Erweiterte Admin-Seiten: +``` +AdminUsersPage.jsx erweitert um: +- Activity-Log Button → zeigt user_activity_log +- Stats Button → zeigt user_stats +- Access-Grants Button → zeigt aktive/abgelaufene Zugriffe +- Feature-Restrictions Button → individuelle Feature-Limits setzen ⭐ GEÄNDERT +- Grant Access Button → manuell Tier-Zugriff gewähren +- Usage-Overview → zeigt user_feature_usage für alle Features ⭐ NEU +``` + +#### Admin-Interface-Details: + +**AdminFeaturesPage.jsx** - Feature-Registry verwalten +```jsx +// Alle Features auflisten + neue hinzufügen + + {features.map(f => ( + + {f.name} + {f.category} + {f.unit} + {f.reset_period} + {f.default_limit ?? '∞'} + + + + + + ))} + + +``` + +**AdminTierLimitsPage.jsx** - Matrix-Editor +```jsx +// Matrix-View: Tiers (Spalten) x Features (Zeilen) + + + + Feature + Free + Basic + Premium + Selfhosted + + + + + Gewichtseinträge + + + + + + + KI-Analysen/Monat + 0 + + ∞ + ∞ + + + +``` + +**AdminUserRestrictionsPage.jsx** - Individuelle User-Limits +```jsx + + + + + {features.map(f => ( + + {f.name} + {getTierLimit(user.tier, f.slug)} + {getUsage(user.id, f.slug)} + + + + + + ))} + +``` + +--- + +### E-Mail Templates (v9c) + +**1. Registrierung + E-Mail-Verifizierung:** +``` +Betreff: Willkommen bei Mitai Jinkendo - E-Mail bestätigen + +Hallo {name}, + +vielen Dank für deine Registrierung bei Mitai Jinkendo! + +Bitte bestätige deine E-Mail-Adresse: +{app_url}/verify-email?token={token} + +Nach der Bestätigung startet dein 14-Tage Premium Trial automatisch. + +Viel Erfolg bei deinem Training! +Dein Mitai Jinkendo Team +``` + +**2. Einladungslink (Admin):** +``` +Betreff: Du wurdest zu Mitai Jinkendo eingeladen + +Hallo, + +{admin_name} hat dich zu Mitai Jinkendo eingeladen! + +Registriere dich jetzt: +{app_url}/register?invite={token} + +Du erhältst {tier} Zugriff. + +Dein Mitai Jinkendo Team +``` + +--- + +### Migrations-Reihenfolge (v9c) + +``` +Phase 1 - DB Schema: +1. app_settings Tabelle + Initialdaten +2. tiers Tabelle + 4 Standard-Tiers +3. coupons Tabelle +4. coupon_redemptions Tabelle +5. access_grants Tabelle +6. user_activity_log Tabelle +7. user_stats Tabelle +8. user_restrictions Tabelle +9. profiles Spalten erweitern +10. Bestehende Profile migrieren (Lars → tier='selfhosted', email_verified=true) + +Phase 2 - Backend: +11. Tier-System Router + Middleware +12. Registrierungs-Flow +13. Coupon-System +14. Access-Grant-Logik +15. Activity-Logging +16. Erweiterte Admin-Endpoints + +Phase 3 - Frontend: +17. Registrierungs-Seiten +18. Tier-System UI-Komponenten +19. Coupon-Eingabe +20. Erweiterte Admin-Panels +21. Feature-Gates in bestehende Seiten einbauen + +Phase 4 - Cron-Jobs: +22. Expired-Access-Checker +23. Monthly-Reset +24. Streak-Updater + +Phase 5 - Testing & Deployment: +25. Dev-Testing +26. Prod-Deployment +``` + +--- + ## Deployment ### Infrastruktur diff --git a/backend/apply_v9c_migration.py b/backend/apply_v9c_migration.py new file mode 100644 index 0000000..4a3bba5 --- /dev/null +++ b/backend/apply_v9c_migration.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Apply v9c Subscription System Migration + +This script checks if v9c migration is needed and applies it. +Run automatically on container startup via main.py startup event. +""" +import os +import psycopg2 +from psycopg2.extras import RealDictCursor + + +def get_db_connection(): + """Get PostgreSQL connection.""" + return psycopg2.connect( + host=os.getenv("DB_HOST", "postgres"), + port=int(os.getenv("DB_PORT", 5432)), + database=os.getenv("DB_NAME", "mitai_prod"), + user=os.getenv("DB_USER", "mitai_prod"), + password=os.getenv("DB_PASSWORD", ""), + cursor_factory=RealDictCursor + ) + + +def migration_needed(conn): + """Check if v9c migration is needed.""" + cur = conn.cursor() + + # Check if tiers table exists + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'tiers' + ) + """) + tiers_exists = cur.fetchone()['exists'] + + # Check if features table exists + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'features' + ) + """) + features_exists = cur.fetchone()['exists'] + + cur.close() + + # Migration needed if either table is missing + return not (tiers_exists and features_exists) + + +def apply_migration(): + """Apply v9c migration if needed.""" + print("[v9c Migration] Checking if migration is needed...") + + try: + conn = get_db_connection() + + if not migration_needed(conn): + print("[v9c Migration] Already applied, skipping.") + conn.close() + return + + print("[v9c Migration] Applying subscription system migration...") + + # Read migration SQL + migration_path = os.path.join( + os.path.dirname(__file__), + "migrations", + "v9c_subscription_system.sql" + ) + + with open(migration_path, 'r', encoding='utf-8') as f: + migration_sql = f.read() + + # Execute migration + cur = conn.cursor() + cur.execute(migration_sql) + conn.commit() + cur.close() + conn.close() + + print("[v9c Migration] ✅ Migration completed successfully!") + + # Verify tables created + conn = get_db_connection() + cur = conn.cursor() + cur.execute(""" + SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name IN ('tiers', 'features', 'tier_limits', 'access_grants', 'coupons') + ORDER BY table_name + """) + tables = [r['table_name'] for r in cur.fetchall()] + print(f"[v9c Migration] Created tables: {', '.join(tables)}") + + # Verify initial data + cur.execute("SELECT COUNT(*) as count FROM tiers") + tier_count = cur.fetchone()['count'] + cur.execute("SELECT COUNT(*) as count FROM features") + feature_count = cur.fetchone()['count'] + cur.execute("SELECT COUNT(*) as count FROM tier_limits") + limit_count = cur.fetchone()['count'] + + print(f"[v9c Migration] Initial data: {tier_count} tiers, {feature_count} features, {limit_count} tier limits") + + cur.close() + conn.close() + + except Exception as e: + print(f"[v9c Migration] ❌ Error: {e}") + raise + + +if __name__ == "__main__": + apply_migration() diff --git a/backend/main.py b/backend/main.py index 7d7249a..88204b3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -51,6 +51,13 @@ async def startup_event(): print(f"⚠️ init_db() failed (non-fatal): {e}") # Don't crash on startup - can be created manually + # Apply v9c migration if needed + try: + from apply_v9c_migration import apply_migration + apply_migration() + except Exception as e: + print(f"⚠️ v9c migration failed (non-fatal): {e}") + # ── Register Routers ────────────────────────────────────────────────────────── app.include_router(auth.router) # /api/auth/* app.include_router(profiles.router) # /api/profiles/*, /api/profile @@ -71,4 +78,4 @@ app.include_router(importdata.router) # /api/import/* @app.get("/") def root(): """API health check.""" - return {"status": "ok", "service": "mitai-jinkendo", "version": "v9b"} + return {"status": "ok", "service": "mitai-jinkendo", "version": "v9c-dev"} diff --git a/backend/migrations/v9c_subscription_system.sql b/backend/migrations/v9c_subscription_system.sql new file mode 100644 index 0000000..c5ff0fe --- /dev/null +++ b/backend/migrations/v9c_subscription_system.sql @@ -0,0 +1,352 @@ +-- ============================================================================ +-- Mitai Jinkendo v9c: Subscription & Coupon System Migration +-- ============================================================================ +-- Created: 2026-03-19 +-- Purpose: Add flexible tier system with Feature-Registry Pattern +-- +-- Tables added: +-- 1. app_settings - Global configuration +-- 2. tiers - Subscription tiers (simplified) +-- 3. features - Feature registry (all limitable features) +-- 4. tier_limits - Tier x Feature matrix +-- 5. user_feature_restrictions - Individual user overrides +-- 6. user_feature_usage - Usage tracking +-- 7. coupons - Coupon management +-- 8. coupon_redemptions - Redemption history +-- 9. access_grants - Time-limited access grants +-- 10. user_activity_log - Activity tracking +-- 11. user_stats - Aggregated statistics +-- +-- Feature-Registry Pattern: +-- Instead of hardcoded columns (max_weight_entries, max_ai_calls), +-- all limits are defined in features table and configured via tier_limits. +-- This allows adding new limitable features without schema changes. +-- ============================================================================ + +-- ============================================================================ +-- 1. app_settings - Global configuration +-- ============================================================================ +CREATE TABLE IF NOT EXISTS app_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + description TEXT, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- 2. tiers - Subscription tiers (simplified) +-- ============================================================================ +CREATE TABLE IF NOT EXISTS tiers ( + id TEXT PRIMARY KEY, -- 'free', 'basic', 'premium', 'selfhosted' + name TEXT NOT NULL, -- Display name + description TEXT, -- Marketing description + price_monthly_cents INTEGER, -- NULL for free/selfhosted + price_yearly_cents INTEGER, -- NULL for free/selfhosted + stripe_price_id_monthly TEXT, -- Stripe Price ID (for v9d) + stripe_price_id_yearly TEXT, -- Stripe Price ID (for v9d) + active BOOLEAN DEFAULT true, -- Can new users subscribe? + sort_order INTEGER DEFAULT 0, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- 3. features - Feature registry (all limitable features) +-- ============================================================================ +CREATE TABLE IF NOT EXISTS features ( + id TEXT PRIMARY KEY, -- 'weight_entries', 'ai_calls', 'photos', etc. + name TEXT NOT NULL, -- Display name + description TEXT, -- What is this feature? + category TEXT, -- 'data', 'ai', 'export', 'integration' + limit_type TEXT DEFAULT 'count', -- 'count', 'boolean', 'quota' + reset_period TEXT DEFAULT 'never', -- 'never', 'monthly', 'daily' + default_limit INTEGER, -- Fallback if no tier_limit defined + active BOOLEAN DEFAULT true, -- Is this feature currently used? + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- 4. tier_limits - Tier x Feature matrix +-- ============================================================================ +CREATE TABLE IF NOT EXISTS 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 = unlimited, 0 = disabled + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(tier_id, feature_id) +); + +-- ============================================================================ +-- 5. user_feature_restrictions - Individual user overrides +-- ============================================================================ +CREATE TABLE IF NOT EXISTS 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 = unlimited, 0 = disabled + reason TEXT, -- Why was this override applied? + created_by UUID, -- Admin profile_id + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(profile_id, feature_id) +); + +-- ============================================================================ +-- 6. user_feature_usage - Usage tracking +-- ============================================================================ +CREATE TABLE IF NOT EXISTS user_feature_usage ( + 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, + usage_count INTEGER DEFAULT 0, + reset_at TIMESTAMP, -- When does this counter reset? + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(profile_id, feature_id) +); + +-- ============================================================================ +-- 7. coupons - Coupon management +-- ============================================================================ +CREATE TABLE IF NOT EXISTS coupons ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + code TEXT UNIQUE NOT NULL, + type TEXT NOT NULL, -- 'single_use', 'period', 'wellpass' + tier_id TEXT REFERENCES tiers(id) ON DELETE SET NULL, + duration_days INTEGER, -- For period/wellpass coupons + max_redemptions INTEGER, -- NULL = unlimited + redemption_count INTEGER DEFAULT 0, + valid_from TIMESTAMP, + valid_until TIMESTAMP, + active BOOLEAN DEFAULT true, + created_by UUID, -- Admin profile_id + description TEXT, -- Internal note + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- 8. coupon_redemptions - Redemption history +-- ============================================================================ +CREATE TABLE IF NOT EXISTS coupon_redemptions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + coupon_id UUID NOT NULL REFERENCES coupons(id) ON DELETE CASCADE, + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + redeemed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + access_grant_id UUID, -- FK to access_grants (created as result) + UNIQUE(coupon_id, profile_id) -- One redemption per user per coupon +); + +-- ============================================================================ +-- 9. access_grants - Time-limited access grants +-- ============================================================================ +CREATE TABLE IF NOT EXISTS access_grants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + tier_id TEXT NOT NULL REFERENCES tiers(id) ON DELETE CASCADE, + granted_by TEXT, -- 'coupon', 'admin', 'trial', 'subscription' + coupon_id UUID REFERENCES coupons(id) ON DELETE SET NULL, + valid_from TIMESTAMP NOT NULL, + valid_until TIMESTAMP NOT NULL, + is_active BOOLEAN DEFAULT true, -- Can be paused by Wellpass logic + paused_by UUID, -- access_grant.id that paused this + paused_at TIMESTAMP, -- When was it paused? + remaining_days INTEGER, -- Days left when paused (for resume) + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- 10. user_activity_log - Activity tracking +-- ============================================================================ +CREATE TABLE IF NOT EXISTS user_activity_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + action TEXT NOT NULL, -- 'login', 'logout', 'coupon_redeemed', 'tier_changed' + details JSONB, -- Flexible metadata + ip_address TEXT, + user_agent TEXT, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_activity_log_profile ON user_activity_log(profile_id, created DESC); +CREATE INDEX IF NOT EXISTS idx_activity_log_action ON user_activity_log(action, created DESC); + +-- ============================================================================ +-- 11. user_stats - Aggregated statistics +-- ============================================================================ +CREATE TABLE IF NOT EXISTS user_stats ( + profile_id UUID PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE, + last_login TIMESTAMP, + login_count INTEGER DEFAULT 0, + weight_entries_count INTEGER DEFAULT 0, + ai_calls_count INTEGER DEFAULT 0, + photos_count INTEGER DEFAULT 0, + total_data_points INTEGER DEFAULT 0, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================================================ +-- Extend profiles table with subscription fields +-- ============================================================================ +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS tier TEXT DEFAULT 'free'; +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS trial_ends_at TIMESTAMP; +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT false; +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email_verify_token TEXT; +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS invited_by UUID REFERENCES profiles(id) ON DELETE SET NULL; +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS invitation_token TEXT; + +-- ============================================================================ +-- Insert initial data +-- ============================================================================ + +-- App settings +INSERT INTO app_settings (key, value, description) VALUES + ('trial_duration_days', '14', 'Default trial duration for new registrations'), + ('post_trial_tier', 'free', 'Tier after trial expires (free/disabled)'), + ('require_email_verification', 'true', 'Require email verification before activation'), + ('self_registration_enabled', 'true', 'Allow self-registration') +ON CONFLICT (key) DO NOTHING; + +-- Tiers +INSERT INTO tiers (id, name, description, price_monthly_cents, price_yearly_cents, active, sort_order) VALUES + ('free', 'Free', 'Eingeschränkte Basis-Funktionen', NULL, NULL, true, 1), + ('basic', 'Basic', 'Kernfunktionen ohne KI', 499, 4990, true, 2), + ('premium', 'Premium', 'Alle Features inkl. KI und Connectoren', 999, 9990, true, 3), + ('selfhosted', 'Self-Hosted', 'Unbegrenzt (für Heimserver)', NULL, NULL, false, 4) +ON CONFLICT (id) DO NOTHING; + +-- Features (11 initial features) +INSERT INTO features (id, name, description, category, limit_type, reset_period, default_limit, active) VALUES + ('weight_entries', 'Gewichtseinträge', 'Anzahl Gewichtsmessungen', 'data', 'count', 'never', NULL, true), + ('circumference_entries', 'Umfangs-Einträge', 'Anzahl Umfangsmessungen', 'data', 'count', 'never', NULL, true), + ('caliper_entries', 'Caliper-Einträge', 'Anzahl Hautfaltenmessungen', 'data', 'count', 'never', NULL, true), + ('nutrition_entries', 'Ernährungs-Einträge', 'Anzahl Ernährungslogs', 'data', 'count', 'never', NULL, true), + ('activity_entries', 'Aktivitäts-Einträge', 'Anzahl Trainings/Aktivitäten', 'data', 'count', 'never', NULL, true), + ('photos', 'Progress-Fotos', 'Anzahl hochgeladene Fotos', 'data', 'count', 'never', NULL, true), + ('ai_calls', 'KI-Analysen', 'KI-Auswertungen pro Monat', 'ai', 'count', 'monthly', 0, true), + ('ai_pipeline', 'KI-Pipeline', 'Vollständige Pipeline-Analyse', 'ai', 'boolean', 'never', 0, true), + ('export_csv', 'CSV-Export', 'Daten als CSV exportieren', 'export', 'boolean', 'never', 0, true), + ('export_json', 'JSON-Export', 'Daten als JSON exportieren', 'export', 'boolean', 'never', 0, true), + ('export_zip', 'ZIP-Export', 'Vollständiger Backup-Export', 'export', 'boolean', 'never', 0, true) +ON CONFLICT (id) DO NOTHING; + +-- Tier x Feature Matrix (tier_limits) +-- Format: (tier, feature, limit) - NULL = unlimited, 0 = disabled + +-- FREE tier (sehr eingeschränkt) +INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES + ('free', 'weight_entries', 30), + ('free', 'circumference_entries', 10), + ('free', 'caliper_entries', 10), + ('free', 'nutrition_entries', 30), + ('free', 'activity_entries', 30), + ('free', 'photos', 5), + ('free', 'ai_calls', 0), -- Keine KI + ('free', 'ai_pipeline', 0), -- Keine Pipeline + ('free', 'export_csv', 0), -- Kein Export + ('free', 'export_json', 0), + ('free', 'export_zip', 0) +ON CONFLICT (tier_id, feature_id) DO NOTHING; + +-- BASIC tier (Kernfunktionen) +INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES + ('basic', 'weight_entries', NULL), -- Unbegrenzt + ('basic', 'circumference_entries', NULL), + ('basic', 'caliper_entries', NULL), + ('basic', 'nutrition_entries', NULL), + ('basic', 'activity_entries', NULL), + ('basic', 'photos', 50), + ('basic', 'ai_calls', 3), -- 3 KI-Calls/Monat + ('basic', 'ai_pipeline', 0), -- Keine Pipeline + ('basic', 'export_csv', 1), -- Export erlaubt + ('basic', 'export_json', 1), + ('basic', 'export_zip', 1) +ON CONFLICT (tier_id, feature_id) DO NOTHING; + +-- PREMIUM tier (alles unbegrenzt) +INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES + ('premium', 'weight_entries', NULL), + ('premium', 'circumference_entries', NULL), + ('premium', 'caliper_entries', NULL), + ('premium', 'nutrition_entries', NULL), + ('premium', 'activity_entries', NULL), + ('premium', 'photos', NULL), + ('premium', 'ai_calls', NULL), -- Unbegrenzt KI + ('premium', 'ai_pipeline', 1), -- Pipeline erlaubt + ('premium', 'export_csv', 1), + ('premium', 'export_json', 1), + ('premium', 'export_zip', 1) +ON CONFLICT (tier_id, feature_id) DO NOTHING; + +-- SELFHOSTED tier (alles unbegrenzt) +INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES + ('selfhosted', 'weight_entries', NULL), + ('selfhosted', 'circumference_entries', NULL), + ('selfhosted', 'caliper_entries', NULL), + ('selfhosted', 'nutrition_entries', NULL), + ('selfhosted', 'activity_entries', NULL), + ('selfhosted', 'photos', NULL), + ('selfhosted', 'ai_calls', NULL), + ('selfhosted', 'ai_pipeline', 1), + ('selfhosted', 'export_csv', 1), + ('selfhosted', 'export_json', 1), + ('selfhosted', 'export_zip', 1) +ON CONFLICT (tier_id, feature_id) DO NOTHING; + +-- ============================================================================ +-- Migrate existing profiles +-- ============================================================================ + +-- Lars' Profile → selfhosted tier with email verified +UPDATE profiles +SET + tier = 'selfhosted', + email_verified = true +WHERE + email = 'lars@stommer.com' + OR role = 'admin'; + +-- Other existing profiles → free tier, unverified +UPDATE profiles +SET + tier = 'free', + email_verified = false +WHERE + tier IS NULL + OR tier = ''; + +-- Initialize user_stats for existing profiles +INSERT INTO user_stats (profile_id, weight_entries_count, photos_count) +SELECT + p.id, + (SELECT COUNT(*) FROM weight_log WHERE profile_id = p.id), + (SELECT COUNT(*) FROM photos WHERE profile_id = p.id) +FROM profiles p +ON CONFLICT (profile_id) DO NOTHING; + +-- ============================================================================ +-- Create indexes for performance +-- ============================================================================ +CREATE INDEX IF NOT EXISTS idx_tier_limits_tier ON tier_limits(tier_id); +CREATE INDEX IF NOT EXISTS idx_tier_limits_feature ON tier_limits(feature_id); +CREATE INDEX IF NOT EXISTS idx_user_restrictions_profile ON user_feature_restrictions(profile_id); +CREATE INDEX IF NOT EXISTS idx_user_usage_profile ON user_feature_usage(profile_id); +CREATE INDEX IF NOT EXISTS idx_access_grants_profile ON access_grants(profile_id, valid_until DESC); +CREATE INDEX IF NOT EXISTS idx_access_grants_active ON access_grants(profile_id, is_active, valid_until DESC); +CREATE INDEX IF NOT EXISTS idx_coupons_code ON coupons(code); +CREATE INDEX IF NOT EXISTS idx_coupon_redemptions_profile ON coupon_redemptions(profile_id); + +-- ============================================================================ +-- Migration complete +-- ============================================================================ +-- Run this migration with: +-- psql -h localhost -U mitai_prod -d mitai_prod < backend/migrations/v9c_subscription_system.sql +-- +-- Or via Docker: +-- docker exec -i mitai-postgres psql -U mitai_prod -d mitai_prod < backend/migrations/v9c_subscription_system.sql +-- ============================================================================