"""套餐开通、续费和到期(m6-plan §1.2、§1.3,决策 M6-5): - subscription 加「scheduled」:续费到账时当前一期还没到期,新一期排队,到旧一期结束时由定时任务生效并发放额度; - activation_request 加 request_no(界面和转账备注用的申请单号,如 KT-7Q3M2X); - 通知加 subscription_expired(到期后只读)、activation_rejected(申请被退回); - billing_due(now):定时任务找到期、该生效、该提醒的订阅。worker 受行级安全策略约束,看不到别的企业, 这里照登录前查询的做法(data-model §2.3)用 bd_auth 所有的 SECURITY DEFINER 函数,只返回编号。 Revision ID: 0010 """ from __future__ import annotations from alembic import op revision = "0010" down_revision = "0009" def upgrade() -> None: op.get_bind().exec_driver_sql(""" ALTER TABLE subscription DROP CONSTRAINT subscription_status_check; ALTER TABLE subscription ADD CONSTRAINT subscription_status_check CHECK (status IN ('scheduled', 'active', 'expired', 'cancelled')); CREATE INDEX ON subscription (tenant_id, starts_at) WHERE status = 'scheduled'; ALTER TABLE activation_request ADD COLUMN request_no text UNIQUE; ALTER TABLE notification DROP CONSTRAINT notification_kind_check; ALTER TABLE notification ADD CONSTRAINT notification_kind_check CHECK (kind IN ('parse_succeeded', 'parse_failed', 'check_succeeded', 'check_failed', 'report_succeeded', 'report_failed', 'quota_80', 'quota_100', 'subscription_activated', 'subscription_expiring', 'subscription_expired', 'activation_rejected')); CREATE FUNCTION billing_due(p_now timestamptz) RETURNS TABLE (tenant_id uuid, subscription_id uuid, action text) LANGUAGE sql STABLE SECURITY DEFINER SET search_path = app AS $$ SELECT s.tenant_id, s.id, 'expire' FROM subscription s WHERE s.status = 'active' AND s.ends_at IS NOT NULL AND s.ends_at <= p_now UNION ALL SELECT s.tenant_id, s.id, 'start' FROM subscription s WHERE s.status = 'scheduled' AND s.starts_at <= p_now UNION ALL SELECT s.tenant_id, s.id, 'remind' FROM subscription s WHERE s.status = 'active' AND s.ends_at IS NOT NULL AND s.ends_at > p_now AND s.ends_at <= p_now + interval '30 days' $$; GRANT SELECT ON subscription TO bd_auth; ALTER FUNCTION billing_due(timestamptz) OWNER TO bd_auth; GRANT EXECUTE ON FUNCTION billing_due(timestamptz) TO bd_app; """) def downgrade() -> None: op.get_bind().exec_driver_sql(""" DROP FUNCTION billing_due(timestamptz); REVOKE SELECT ON subscription FROM bd_auth; ALTER TABLE notification DROP CONSTRAINT notification_kind_check; ALTER TABLE notification ADD CONSTRAINT notification_kind_check CHECK (kind IN ('parse_succeeded', 'parse_failed', 'check_succeeded', 'check_failed', 'report_succeeded', 'report_failed', 'quota_80', 'quota_100', 'subscription_activated', 'subscription_expiring')); ALTER TABLE activation_request DROP COLUMN request_no; DROP INDEX IF EXISTS subscription_tenant_id_starts_at_idx; ALTER TABLE subscription DROP CONSTRAINT subscription_status_check; ALTER TABLE subscription ADD CONSTRAINT subscription_status_check CHECK (status IN ('active', 'expired', 'cancelled')); """)