Merge pull request '9c datatables' (#5) from develop into main
Reviewed-on: #5
This commit is contained in:
commit
9387670a7b
461
CLAUDE.md
461
CLAUDE.md
|
|
@ -96,19 +96,468 @@ mitai-jinkendo/
|
||||||
- ✅ Automatische SQLite→PostgreSQL Migration bei Container-Start
|
- ✅ Automatische SQLite→PostgreSQL Migration bei Container-Start
|
||||||
- ✅ **Modulare Backend-Architektur**: 14 Router-Module, main.py von 1878→75 Zeilen (-96%)
|
- ✅ **Modulare Backend-Architektur**: 14 Router-Module, main.py von 1878→75 Zeilen (-96%)
|
||||||
|
|
||||||
### Was in v9c kommt:
|
### Was in v9c kommt: Subscription & Coupon Management System
|
||||||
- 🔲 Selbst-Registrierung mit E-Mail-Bestätigung
|
**Core Features:**
|
||||||
- 🔲 Freemium Tier-System (free/basic/premium/selfhosted)
|
- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung (Pflicht)
|
||||||
- 🔲 14-Tage Trial automatisch
|
- 🔲 Flexibles Tier-System (free/basic/premium/selfhosted) - Admin-editierbar
|
||||||
- 🔲 Einladungslinks für Beta-Nutzer
|
- 🔲 Trial-System (Dauer konfigurierbar, auto-start nach E-Mail-Verifikation)
|
||||||
- 🔲 Admin kann Tiers manuell setzen
|
- 🔲 **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:
|
### Was in v9d kommt:
|
||||||
|
- 🔲 Bonus-System & Gamification (Streaks, Achievements)
|
||||||
|
- 🔲 Stripe-Integration (Self-Service Upgrade, Subscriptions)
|
||||||
- 🔲 OAuth2-Grundgerüst für Fitness-Connectoren
|
- 🔲 OAuth2-Grundgerüst für Fitness-Connectoren
|
||||||
- 🔲 Strava Connector
|
- 🔲 Strava Connector
|
||||||
- 🔲 Withings Connector (Waage)
|
- 🔲 Withings Connector (Waage)
|
||||||
- 🔲 Garmin Connector
|
- 🔲 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
|
||||||
|
<TierBadge tier="premium" /> // Tier-Anzeige mit Icon
|
||||||
|
<FeatureGate feature="ai_calls">...</> // Feature-basierte Sichtbarkeit ⭐ GEÄNDERT
|
||||||
|
<AccessStatus /> // "Trial endet in 5 Tagen" Banner
|
||||||
|
<CouponInput onRedeem={...} /> // Coupon-Eingabefeld
|
||||||
|
<ActivityTimeline activities={...} /> // User-Activity-Log
|
||||||
|
<FeatureLimitBadge feature="ai_calls" /> // "5/10 verwendet" Anzeige ⭐ NEU
|
||||||
|
<TierLimitsMatrix tiers={...} features={...}/> // Matrix-Editor ⭐ NEU
|
||||||
|
<StreakCounter days={7} /> // 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
|
||||||
|
<FeatureList>
|
||||||
|
{features.map(f => (
|
||||||
|
<FeatureRow>
|
||||||
|
<Name>{f.name}</Name>
|
||||||
|
<Category>{f.category}</Category>
|
||||||
|
<Unit>{f.unit}</Unit>
|
||||||
|
<ResetPeriod>{f.reset_period}</ResetPeriod>
|
||||||
|
<DefaultLimit>{f.default_limit ?? '∞'}</DefaultLimit>
|
||||||
|
<Actions>
|
||||||
|
<EditButton />
|
||||||
|
<DeleteButton />
|
||||||
|
</Actions>
|
||||||
|
</FeatureRow>
|
||||||
|
))}
|
||||||
|
</FeatureList>
|
||||||
|
<AddFeatureButton />
|
||||||
|
```
|
||||||
|
|
||||||
|
**AdminTierLimitsPage.jsx** - Matrix-Editor
|
||||||
|
```jsx
|
||||||
|
// Matrix-View: Tiers (Spalten) x Features (Zeilen)
|
||||||
|
<TierLimitsMatrix>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Feature</th>
|
||||||
|
<th>Free</th>
|
||||||
|
<th>Basic</th>
|
||||||
|
<th>Premium</th>
|
||||||
|
<th>Selfhosted</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Gewichtseinträge</td>
|
||||||
|
<td><Input value="30" /></td>
|
||||||
|
<td><Input value="" placeholder="∞" /></td>
|
||||||
|
<td><Input value="" placeholder="∞" /></td>
|
||||||
|
<td><Input value="" placeholder="∞" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>KI-Analysen/Monat</td>
|
||||||
|
<td><Checkbox disabled /> 0</td>
|
||||||
|
<td><Checkbox checked /> <Input value="10" /></td>
|
||||||
|
<td><Checkbox checked /> ∞</td>
|
||||||
|
<td><Checkbox checked /> ∞</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</TierLimitsMatrix>
|
||||||
|
```
|
||||||
|
|
||||||
|
**AdminUserRestrictionsPage.jsx** - Individuelle User-Limits
|
||||||
|
```jsx
|
||||||
|
<UserSelect onChange={loadUser} />
|
||||||
|
|
||||||
|
<CurrentTier badge={user.tier} />
|
||||||
|
<CurrentUsage>
|
||||||
|
{features.map(f => (
|
||||||
|
<FeatureUsageRow key={f.slug}>
|
||||||
|
<Name>{f.name}</Name>
|
||||||
|
<TierLimit>{getTierLimit(user.tier, f.slug)}</TierLimit>
|
||||||
|
<CurrentUsage>{getUsage(user.id, f.slug)}</CurrentUsage>
|
||||||
|
<Override>
|
||||||
|
<Input
|
||||||
|
value={getUserRestriction(user.id, f.slug)}
|
||||||
|
placeholder="Tier-Standard"
|
||||||
|
/>
|
||||||
|
</Override>
|
||||||
|
<SaveButton />
|
||||||
|
</FeatureUsageRow>
|
||||||
|
))}
|
||||||
|
</CurrentUsage>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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
|
## Deployment
|
||||||
|
|
||||||
### Infrastruktur
|
### Infrastruktur
|
||||||
|
|
|
||||||
117
backend/apply_v9c_migration.py
Normal file
117
backend/apply_v9c_migration.py
Normal file
|
|
@ -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()
|
||||||
|
|
@ -51,6 +51,13 @@ async def startup_event():
|
||||||
print(f"⚠️ init_db() failed (non-fatal): {e}")
|
print(f"⚠️ init_db() failed (non-fatal): {e}")
|
||||||
# Don't crash on startup - can be created manually
|
# 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 ──────────────────────────────────────────────────────────
|
# ── Register Routers ──────────────────────────────────────────────────────────
|
||||||
app.include_router(auth.router) # /api/auth/*
|
app.include_router(auth.router) # /api/auth/*
|
||||||
app.include_router(profiles.router) # /api/profiles/*, /api/profile
|
app.include_router(profiles.router) # /api/profiles/*, /api/profile
|
||||||
|
|
@ -71,4 +78,4 @@ app.include_router(importdata.router) # /api/import/*
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
"""API health check."""
|
"""API health check."""
|
||||||
return {"status": "ok", "service": "mitai-jinkendo", "version": "v9b"}
|
return {"status": "ok", "service": "mitai-jinkendo", "version": "v9c-dev"}
|
||||||
|
|
|
||||||
352
backend/migrations/v9c_subscription_system.sql
Normal file
352
backend/migrations/v9c_subscription_system.sql
Normal file
|
|
@ -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
|
||||||
|
-- ============================================================================
|
||||||
Loading…
Reference in New Issue
Block a user