From 2f302b26afe435285dda0595c93b72d730e52590 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 19 Mar 2026 12:42:43 +0100 Subject: [PATCH] feat: add v9c subscription system database schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/apply_v9c_migration.py | 117 ++++++ backend/main.py | 9 +- .../migrations/v9c_subscription_system.sql | 352 ++++++++++++++++++ 3 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 backend/apply_v9c_migration.py create mode 100644 backend/migrations/v9c_subscription_system.sql 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..dbd72d4 --- /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 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 +-- ============================================================================