-- Migration: Billing tables for subscription management -- Supports: Free/Pro/Team plans, Alipay + WeChat Pay, usage quotas -- Plan definitions (Free/Pro/Team) CREATE TABLE IF NOT EXISTS billing_plans ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, description TEXT, price_cents INTEGER NOT NULL DEFAULT 0, currency TEXT NOT NULL DEFAULT 'CNY', interval TEXT NOT NULL DEFAULT 'month', features JSONB NOT NULL DEFAULT '{}', limits JSONB NOT NULL DEFAULT '{}', is_default BOOLEAN NOT NULL DEFAULT FALSE, sort_order INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_billing_plans_status ON billing_plans(status); -- Account subscriptions CREATE TABLE IF NOT EXISTS billing_subscriptions ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, plan_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', current_period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(), current_period_end TIMESTAMPTZ NOT NULL, trial_end TIMESTAMPTZ, canceled_at TIMESTAMPTZ, cancel_at_period_end BOOLEAN NOT NULL DEFAULT FALSE, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (plan_id) REFERENCES billing_plans(id) ); CREATE INDEX IF NOT EXISTS idx_billing_sub_account ON billing_subscriptions(account_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_sub_active ON billing_subscriptions(account_id) WHERE status IN ('trial', 'active', 'past_due'); -- Invoices CREATE TABLE IF NOT EXISTS billing_invoices ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, subscription_id TEXT, plan_id TEXT, amount_cents INTEGER NOT NULL, currency TEXT NOT NULL DEFAULT 'CNY', description TEXT, status TEXT NOT NULL DEFAULT 'pending', due_at TIMESTAMPTZ, paid_at TIMESTAMPTZ, voided_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (subscription_id) REFERENCES billing_subscriptions(id) ON DELETE SET NULL, FOREIGN KEY (plan_id) REFERENCES billing_plans(id) ); CREATE INDEX IF NOT EXISTS idx_billing_inv_account ON billing_invoices(account_id); CREATE INDEX IF NOT EXISTS idx_billing_inv_status ON billing_invoices(status); CREATE INDEX IF NOT EXISTS idx_billing_inv_time ON billing_invoices(created_at); -- Payment records (Alipay / WeChat Pay) CREATE TABLE IF NOT EXISTS billing_payments ( id TEXT PRIMARY KEY, invoice_id TEXT NOT NULL, account_id TEXT NOT NULL, amount_cents INTEGER NOT NULL, currency TEXT NOT NULL DEFAULT 'CNY', method TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', external_trade_no TEXT, paid_at TIMESTAMPTZ, refunded_at TIMESTAMPTZ, failure_reason TEXT, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (invoice_id) REFERENCES billing_invoices(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_billing_pay_invoice ON billing_payments(invoice_id); CREATE INDEX IF NOT EXISTS idx_billing_pay_account ON billing_payments(account_id); CREATE INDEX IF NOT EXISTS idx_billing_pay_trade_no ON billing_payments(external_trade_no); CREATE INDEX IF NOT EXISTS idx_billing_pay_status ON billing_payments(status); -- Monthly usage quotas (per account per billing period) CREATE TABLE IF NOT EXISTS billing_usage_quotas ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL, period_start TIMESTAMPTZ NOT NULL, period_end TIMESTAMPTZ NOT NULL, input_tokens BIGINT NOT NULL DEFAULT 0, output_tokens BIGINT NOT NULL DEFAULT 0, relay_requests INTEGER NOT NULL DEFAULT 0, hand_executions INTEGER NOT NULL DEFAULT 0, pipeline_runs INTEGER NOT NULL DEFAULT 0, max_input_tokens BIGINT, max_output_tokens BIGINT, max_relay_requests INTEGER, max_hand_executions INTEGER, max_pipeline_runs INTEGER, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, UNIQUE(account_id, period_start) ); CREATE INDEX IF NOT EXISTS idx_billing_usage_account ON billing_usage_quotas(account_id); CREATE INDEX IF NOT EXISTS idx_billing_usage_period ON billing_usage_quotas(period_start, period_end); -- Seed: default plans INSERT INTO billing_plans (id, name, display_name, description, price_cents, interval, features, limits, is_default, sort_order) VALUES ('plan-free', 'free', '免费版', '基础功能,适合个人体验', 0, 'month', '{"hands": ["browser", "collector", "researcher"], "chat_modes": ["flash", "thinking"], "pipelines": 3, "support": "community"}'::jsonb, '{"max_input_tokens_monthly": 500000, "max_output_tokens_monthly": 500000, "max_relay_requests_monthly": 100, "max_hand_executions_monthly": 20, "max_pipeline_runs_monthly": 5}'::jsonb, TRUE, 0), ('plan-pro', 'pro', '专业版', '全功能解锁,适合知识工作者', 4900, 'month', '{"hands": "all", "chat_modes": "all", "pipelines": -1, "support": "priority", "memory": true, "export": true}'::jsonb, '{"max_input_tokens_monthly": 5000000, "max_output_tokens_monthly": 5000000, "max_relay_requests_monthly": 2000, "max_hand_executions_monthly": 200, "max_pipeline_runs_monthly": 100}'::jsonb, FALSE, 1), ('plan-team', 'team', '团队版', '多席位协作,适合企业团队', 19900, 'month', '{"hands": "all", "chat_modes": "all", "pipelines": -1, "support": "dedicated", "memory": true, "export": true, "sharing": true, "admin": true}'::jsonb, '{"max_input_tokens_monthly": 50000000, "max_output_tokens_monthly": 50000000, "max_relay_requests_monthly": 20000, "max_hand_executions_monthly": 1000, "max_pipeline_runs_monthly": 500}'::jsonb, FALSE, 2) ON CONFLICT (name) DO NOTHING;