feat: add v9c subscription system database schema
Phase 1: Database Migration Complete Created migration infrastructure: - backend/migrations/v9c_subscription_system.sql (11 new tables) - backend/apply_v9c_migration.py (auto-migration runner) - Updated main.py startup event to apply migration New tables (Feature-Registry Pattern): 1. app_settings - Global configuration 2. tiers - Subscription tiers (free/basic/premium/selfhosted) 3. features - Feature registry (11 limitable features) 4. tier_limits - Tier x Feature matrix (44 initial limits) 5. user_feature_restrictions - Individual user overrides 6. user_feature_usage - Usage tracking with reset periods 7. coupons - Coupon management (single-use, period, Wellpass) 8. coupon_redemptions - Redemption history 9. access_grants - Time-limited access with pause/resume logic 10. user_activity_log - Activity tracking (JSONB details) 11. user_stats - Aggregated statistics Extended profiles table: - tier, trial_ends_at, email_verified, email_verify_token - invited_by, invitation_token Initial data inserted: - 4 tiers (free/basic/premium/selfhosted) - 11 features (weight, circumference, caliper, nutrition, activity, photos, ai_calls, ai_pipeline, export_*) - 44 tier_limits (complete Tier x Feature matrix) - App settings (trial duration, self-registration config) Migration auto-runs on container startup (similar to SQLite→PostgreSQL). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
26f8bcf86d
commit
2f302b26af
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}")
|
||||
# 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"}
|
||||
|
|
|
|||
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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
profile_id TEXT 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 TEXT, -- 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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
profile_id TEXT 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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
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 TEXT, -- 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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
coupon_id TEXT NOT NULL REFERENCES coupons(id) ON DELETE CASCADE,
|
||||
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
redeemed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
access_grant_id TEXT, -- 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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
profile_id TEXT 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 TEXT 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 TEXT, -- 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 TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
|
||||
profile_id TEXT 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 TEXT 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 TEXT 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