"""add immutable commercial billing periods and administration audit Revision ID: 20260716_0021 Revises: 20260716_0020 Create Date: 2026-07-16 22:45:00 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "20260716_0021" down_revision: str | None = "20260716_0020" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def _require_postgresql() -> None: dialect_name = op.get_bind().dialect.name if dialect_name != "postgresql": raise RuntimeError( "20260716_0021 only supports PostgreSQL; " f"refusing to mutate {dialect_name} without transactional constraint DDL" ) def _require_empty_billing_lifecycle_for_downgrade() -> None: bind = op.get_bind() counts = { table_name: int(bind.scalar(sa.text(f"SELECT COUNT(*) FROM {table_name}")) or 0) for table_name in ("commercial_admin_events", "commercial_billing_periods") } if any(counts.values()): summary = ", ".join(f"{name}={count}" for name, count in counts.items()) raise RuntimeError( "cannot downgrade commercial billing lifecycle: immutable periods or audit " f"facts exist ({summary})" ) def _json_object_default() -> sa.TextClause: return sa.text("'{}'::json") def upgrade() -> None: _require_postgresql() _create_billing_periods() _create_admin_events() _backfill_current_periods_and_audit() _bind_usage_to_periods() _bind_costs_to_periods() _bind_reservations_to_periods() _create_period_overlap_guard() _create_append_only_triggers() def _create_billing_periods() -> None: op.create_table( "commercial_billing_periods", sa.Column("id", sa.String(length=36), nullable=False), sa.Column("tenant_id", sa.String(length=64), nullable=False), sa.Column("subscription_id", sa.String(length=36), nullable=False), sa.Column("plan_id", sa.String(length=36), nullable=False), sa.Column("period_sequence", sa.Integer(), nullable=False), sa.Column("period_key", sa.String(length=64), nullable=False), sa.Column("status", sa.String(length=16), nullable=False, server_default="issued"), sa.Column("period_start", sa.DateTime(timezone=True), nullable=False), sa.Column("period_end", sa.DateTime(timezone=True), nullable=False), sa.Column("subscription_status_snapshot", sa.String(length=20), nullable=False), sa.Column("plan_code_snapshot", sa.String(length=80), nullable=False), sa.Column("plan_version_snapshot", sa.Integer(), nullable=False), sa.Column("pricing_model_snapshot", sa.String(length=24), nullable=False), sa.Column("billing_interval", sa.String(length=20), nullable=False), sa.Column("currency", sa.String(length=3), nullable=False), sa.Column("base_fee_snapshot", sa.Numeric(20, 4), nullable=False), sa.Column("seats_snapshot", sa.Integer(), nullable=False), sa.Column("source", sa.String(length=32), nullable=False), sa.Column("idempotency_key", sa.String(length=160), nullable=False), sa.Column("created_by", sa.String(length=120), nullable=False), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.CheckConstraint( "status = 'issued'", name="ck_commercial_billing_periods_status", ), sa.CheckConstraint( "subscription_status_snapshot IN ('trialing', 'active', 'past_due', " "'suspended', 'canceled', 'expired')", name="ck_commercial_billing_periods_subscription_status", ), sa.CheckConstraint( "pricing_model_snapshot IN ('subscription', 'usage', 'hybrid', 'pilot', 'custom')", name="ck_commercial_billing_periods_pricing_model", ), sa.CheckConstraint( "billing_interval IN ('monthly', 'quarterly', 'annual', 'contract')", name="ck_commercial_billing_periods_interval", ), sa.CheckConstraint( "source IN ('subscription_created', 'auto_renew', 'migration_backfill')", name="ck_commercial_billing_periods_source", ), sa.CheckConstraint( "period_sequence >= 1 AND period_end > period_start " "AND plan_version_snapshot >= 1 AND base_fee_snapshot >= 0 " "AND seats_snapshot > 0", name="ck_commercial_billing_periods_values", ), sa.CheckConstraint( "length(trim(period_key)) > 0 AND length(trim(plan_code_snapshot)) > 0 " "AND length(trim(currency)) = 3 AND length(trim(idempotency_key)) > 0 " "AND length(trim(created_by)) > 0", name="ck_commercial_billing_periods_keys", ), sa.ForeignKeyConstraint( ["tenant_id", "subscription_id"], ["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"], name="fk_commercial_billing_periods_tenant_subscription", ondelete="RESTRICT", ), sa.ForeignKeyConstraint( ["tenant_id", "plan_id"], ["tenant_commercial_plans.tenant_id", "tenant_commercial_plans.id"], name="fk_commercial_billing_periods_tenant_plan", ondelete="RESTRICT", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("tenant_id", "id", name="uq_commercial_billing_periods_tenant_id"), sa.UniqueConstraint( "tenant_id", "subscription_id", "id", name="uq_commercial_billing_periods_tenant_subscription_id", ), sa.UniqueConstraint( "tenant_id", "subscription_id", "period_sequence", name="uq_commercial_billing_periods_subscription_sequence", ), sa.UniqueConstraint( "tenant_id", "subscription_id", "period_key", name="uq_commercial_billing_periods_subscription_key", ), sa.UniqueConstraint( "tenant_id", "subscription_id", "period_start", name="uq_commercial_billing_periods_subscription_start", ), sa.UniqueConstraint( "tenant_id", "subscription_id", "idempotency_key", name="uq_commercial_billing_periods_subscription_request", ), ) op.create_index( "ix_commercial_billing_periods_tenant_window", "commercial_billing_periods", ["tenant_id", "period_start", "period_end"], ) op.create_index( "ix_commercial_billing_periods_subscription_window", "commercial_billing_periods", ["tenant_id", "subscription_id", "period_start", "period_end"], ) def _create_admin_events() -> None: op.create_table( "commercial_admin_events", sa.Column("id", sa.String(length=36), nullable=False), sa.Column("tenant_id", sa.String(length=64), nullable=False), sa.Column("actor_type", sa.String(length=16), nullable=False), sa.Column("actor_id", sa.String(length=120), nullable=False), sa.Column("request_id", sa.String(length=120), nullable=False), sa.Column("reason", sa.Text(), nullable=False), sa.Column("action", sa.String(length=48), nullable=False), sa.Column("resource_type", sa.String(length=24), nullable=False), sa.Column("resource_id", sa.String(length=36), nullable=False), sa.Column("resource_version", sa.Integer(), nullable=False), sa.Column("before_json", sa.JSON(), nullable=False, server_default=_json_object_default()), sa.Column("after_json", sa.JSON(), nullable=False, server_default=_json_object_default()), sa.Column( "occurred_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.CheckConstraint( "actor_type IN ('user', 'system', 'migration')", name="ck_commercial_admin_events_actor_type", ), sa.CheckConstraint( "action IN ('plan_created', 'plan_activated', 'plan_retired', " "'subscription_created', 'subscription_activated', " "'subscription_transitioned', 'entitlement_created', " "'entitlement_updated', 'entitlement_activated', " "'billing_period_created', 'subscription_rolled_over', " "'legacy_state_imported')", name="ck_commercial_admin_events_action", ), sa.CheckConstraint( "resource_type IN ('plan', 'subscription', 'entitlement', 'billing_period')", name="ck_commercial_admin_events_resource_type", ), sa.CheckConstraint( "resource_version >= 1", name="ck_commercial_admin_events_resource_version", ), sa.CheckConstraint( "length(trim(actor_id)) > 0 AND length(trim(request_id)) > 0 " "AND length(trim(reason)) > 0 AND length(trim(resource_id)) > 0", name="ck_commercial_admin_events_required_text", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("tenant_id", "id", name="uq_commercial_admin_events_tenant_id"), sa.UniqueConstraint( "tenant_id", "request_id", "action", "resource_type", "resource_id", name="uq_commercial_admin_events_request_resource", ), ) op.create_index( "ix_commercial_admin_events_tenant_time", "commercial_admin_events", ["tenant_id", "occurred_at", "id"], ) op.create_index( "ix_commercial_admin_events_tenant_resource", "commercial_admin_events", ["tenant_id", "resource_type", "resource_id", "occurred_at"], ) op.create_index( "ix_commercial_admin_events_tenant_request", "commercial_admin_events", ["tenant_id", "request_id"], ) def _backfill_current_periods_and_audit() -> None: op.execute( """ INSERT INTO commercial_billing_periods ( id, tenant_id, subscription_id, plan_id, period_sequence, period_key, status, period_start, period_end, subscription_status_snapshot, plan_code_snapshot, plan_version_snapshot, pricing_model_snapshot, billing_interval, currency, base_fee_snapshot, seats_snapshot, source, idempotency_key, created_by ) SELECT substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-' || substr(digest, 13, 4) || '-' || substr(digest, 17, 4) || '-' || substr(digest, 21, 12), subscription.tenant_id, subscription.id, subscription.plan_id, 1, 'bp-' || substr(digest, 1, 24), 'issued', subscription.current_period_start, subscription.current_period_end, subscription.status, plan.plan_code, plan.version, plan.pricing_model, subscription.billing_interval, subscription.currency, subscription.base_fee_snapshot, subscription.seats, 'migration_backfill', 'migration-0021:' || subscription.id, 'migration:20260716_0021' FROM tenant_subscriptions AS subscription JOIN tenant_commercial_plans AS plan ON plan.tenant_id = subscription.tenant_id AND plan.id = subscription.plan_id CROSS JOIN LATERAL ( SELECT md5( subscription.tenant_id || ':' || subscription.id || ':' || subscription.current_period_start::text || ':' || subscription.current_period_end::text ) AS digest ) AS identity """ ) for resource_type, table_name, version_column in ( ("plan", "tenant_commercial_plans", "version"), ("subscription", "tenant_subscriptions", "version"), ("entitlement", "commercial_entitlements", "version"), ("billing_period", "commercial_billing_periods", "period_sequence"), ): op.execute( f""" INSERT INTO commercial_admin_events ( id, tenant_id, actor_type, actor_id, request_id, reason, action, resource_type, resource_id, resource_version, before_json, after_json ) SELECT substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-' || substr(digest, 13, 4) || '-' || substr(digest, 17, 4) || '-' || substr(digest, 21, 12), resource.tenant_id, 'migration', 'migration:20260716_0021', 'migration:0021:{resource_type}:' || resource.id, '0021 建立商业管理审计基线,不推断迁移前操作人或原始请求。', 'legacy_state_imported', '{resource_type}', resource.id, resource.{version_column}, '{{}}'::json, json_build_object('id', resource.id, 'version', resource.{version_column}) FROM {table_name} AS resource CROSS JOIN LATERAL ( SELECT md5( resource.tenant_id || ':audit:{resource_type}:' || resource.id ) AS digest ) AS identity """ ) def _bind_usage_to_periods() -> None: op.add_column( "usage_meter_events", sa.Column("billing_period_id", sa.String(length=36), nullable=True), ) op.add_column( "usage_meter_events", sa.Column("quota_period_key", sa.String(length=64), nullable=True), ) op.alter_column( "usage_meter_events", "period_key", existing_type=sa.String(length=32), type_=sa.String(length=64), existing_nullable=False, ) op.execute( """ UPDATE usage_meter_events AS usage SET billing_period_id = period.id, quota_period_key = usage.period_key, period_key = period.period_key FROM commercial_billing_periods AS period WHERE period.tenant_id = usage.tenant_id AND period.subscription_id = usage.subscription_id AND usage.occurred_at >= period.period_start AND usage.occurred_at < period.period_end """ ) _fail_if_unbound( "usage_meter_events", "billing_period_id IS NULL OR quota_period_key IS NULL", "existing usage facts do not map to one immutable current billing period", ) op.alter_column( "usage_meter_events", "billing_period_id", existing_type=sa.String(36), nullable=False ) op.alter_column( "usage_meter_events", "quota_period_key", existing_type=sa.String(64), nullable=False ) op.drop_constraint("ck_usage_meter_events_keys", "usage_meter_events", type_="check") op.create_check_constraint( "ck_usage_meter_events_keys", "usage_meter_events", "length(trim(metric_key)) > 0 AND length(trim(unit)) > 0 " "AND length(trim(period_key)) > 0 AND length(trim(source_system)) > 0 " "AND length(trim(quota_period_key)) > 0 " "AND length(trim(idempotency_key)) > 0 " "AND length(trim(request_fingerprint)) > 0", ) op.create_foreign_key( "fk_usage_meter_events_tenant_billing_period", "usage_meter_events", "commercial_billing_periods", ["tenant_id", "subscription_id", "billing_period_id"], ["tenant_id", "subscription_id", "id"], ondelete="RESTRICT", ) op.drop_index("ix_usage_meter_events_quota_window", table_name="usage_meter_events") op.create_index( "ix_usage_meter_events_quota_window", "usage_meter_events", ["tenant_id", "subscription_id", "metric_key", "quota_period_key", "occurred_at"], ) op.create_index( "ix_usage_meter_events_billing_period", "usage_meter_events", ["tenant_id", "billing_period_id", "occurred_at"], ) def _bind_costs_to_periods() -> None: op.add_column( "commercial_cost_events", sa.Column("billing_period_id", sa.String(length=36), nullable=True), ) op.execute( """ UPDATE commercial_cost_events AS cost SET billing_period_id = usage.billing_period_id FROM usage_meter_events AS usage WHERE usage.tenant_id = cost.tenant_id AND usage.subscription_id = cost.subscription_id AND usage.id = cost.usage_event_id """ ) op.execute( """ UPDATE commercial_cost_events AS cost SET billing_period_id = period.id FROM commercial_billing_periods AS period WHERE cost.subscription_id IS NOT NULL AND cost.billing_period_id IS NULL AND period.tenant_id = cost.tenant_id AND period.subscription_id = cost.subscription_id AND cost.occurred_at >= period.period_start AND cost.occurred_at < period.period_end """ ) _fail_if_unbound( "commercial_cost_events", "subscription_id IS NOT NULL AND billing_period_id IS NULL", "existing subscription cost facts do not map to one immutable billing period", ) op.create_check_constraint( "ck_commercial_cost_events_billing_period_pair", "commercial_cost_events", "(subscription_id IS NULL AND billing_period_id IS NULL) OR " "(subscription_id IS NOT NULL AND billing_period_id IS NOT NULL)", ) op.create_foreign_key( "fk_commercial_cost_events_tenant_billing_period", "commercial_cost_events", "commercial_billing_periods", ["tenant_id", "subscription_id", "billing_period_id"], ["tenant_id", "subscription_id", "id"], ondelete="RESTRICT", ) op.drop_index( "ix_commercial_cost_events_subscription_period", table_name="commercial_cost_events", ) op.create_index( "ix_commercial_cost_events_subscription_period", "commercial_cost_events", ["tenant_id", "subscription_id", "billing_period_id", "occurred_at"], ) def _bind_reservations_to_periods() -> None: op.add_column( "commercial_runtime_reservations", sa.Column("billing_period_id", sa.String(length=36), nullable=True), ) op.add_column( "commercial_runtime_reservations", sa.Column("quota_period_key", sa.String(length=64), nullable=True), ) op.alter_column( "commercial_runtime_reservations", "period_key", existing_type=sa.String(length=32), type_=sa.String(length=64), existing_nullable=False, ) op.execute( """ UPDATE commercial_runtime_reservations AS reservation SET billing_period_id = period.id, quota_period_key = reservation.period_key, period_key = period.period_key FROM commercial_billing_periods AS period WHERE period.tenant_id = reservation.tenant_id AND period.subscription_id = reservation.subscription_id AND reservation.created_at >= period.period_start AND reservation.created_at < period.period_end """ ) _fail_if_unbound( "commercial_runtime_reservations", "billing_period_id IS NULL OR quota_period_key IS NULL", "existing runtime reservations do not map to one immutable billing period", ) op.alter_column( "commercial_runtime_reservations", "billing_period_id", existing_type=sa.String(36), nullable=False, ) op.alter_column( "commercial_runtime_reservations", "quota_period_key", existing_type=sa.String(64), nullable=False, ) op.drop_constraint( "ck_commercial_runtime_reservations_keys", "commercial_runtime_reservations", type_="check", ) op.create_check_constraint( "ck_commercial_runtime_reservations_keys", "commercial_runtime_reservations", "length(trim(run_id)) > 0 AND length(trim(tool_call_id)) > 0 " "AND length(trim(tool_type)) > 0 AND length(trim(tool_name)) > 0 " "AND length(trim(period_key)) > 0 AND length(trim(quota_period_key)) > 0 " "AND length(trim(request_fingerprint)) > 0", ) op.create_foreign_key( "fk_commercial_runtime_reservations_tenant_billing_period", "commercial_runtime_reservations", "commercial_billing_periods", ["tenant_id", "subscription_id", "billing_period_id"], ["tenant_id", "subscription_id", "id"], ondelete="RESTRICT", ) op.drop_index( "ix_commercial_runtime_reservations_quota", table_name="commercial_runtime_reservations", ) op.create_index( "ix_commercial_runtime_reservations_quota", "commercial_runtime_reservations", ["tenant_id", "subscription_id", "entitlement_id", "quota_period_key", "status"], ) op.create_index( "ix_commercial_runtime_reservations_billing_period", "commercial_runtime_reservations", ["tenant_id", "billing_period_id", "status"], ) def _fail_if_unbound(table_name: str, predicate: str, message: str) -> None: op.execute( f""" DO $$ BEGIN IF EXISTS (SELECT 1 FROM {table_name} WHERE {predicate}) THEN RAISE EXCEPTION '{message}'; END IF; END; $$ """ ) def _create_append_only_triggers() -> None: for table_name in ("commercial_billing_periods", "commercial_admin_events"): op.execute( f""" CREATE TRIGGER trg_{table_name}_append_only BEFORE UPDATE OR DELETE ON {table_name} FOR EACH ROW EXECUTE FUNCTION prevent_commercial_events_mutation() """ ) def _create_period_overlap_guard() -> None: op.execute( """ CREATE FUNCTION prevent_commercial_billing_period_overlap() RETURNS trigger AS $$ BEGIN PERFORM pg_advisory_xact_lock( hashtextextended(NEW.tenant_id || ':' || NEW.subscription_id, 0) ); IF EXISTS ( SELECT 1 FROM commercial_billing_periods AS existing WHERE existing.tenant_id = NEW.tenant_id AND existing.subscription_id = NEW.subscription_id AND tstzrange( existing.period_start, existing.period_end, '[)' ) && tstzrange(NEW.period_start, NEW.period_end, '[)') ) THEN RAISE EXCEPTION 'commercial billing period overlaps an issued period'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql """ ) op.execute( """ CREATE TRIGGER trg_commercial_billing_periods_no_overlap BEFORE INSERT ON commercial_billing_periods FOR EACH ROW EXECUTE FUNCTION prevent_commercial_billing_period_overlap() """ ) def downgrade() -> None: _require_postgresql() _require_empty_billing_lifecycle_for_downgrade() _unbind_reservations() _unbind_costs() _unbind_usage() op.execute( "DROP TRIGGER IF EXISTS trg_commercial_billing_periods_no_overlap " "ON commercial_billing_periods" ) for table_name in ("commercial_admin_events", "commercial_billing_periods"): op.execute(f"DROP TRIGGER IF EXISTS trg_{table_name}_append_only ON {table_name}") op.drop_table("commercial_admin_events") op.drop_table("commercial_billing_periods") op.execute("DROP FUNCTION IF EXISTS prevent_commercial_billing_period_overlap()") def _unbind_reservations() -> None: op.drop_index( "ix_commercial_runtime_reservations_billing_period", table_name="commercial_runtime_reservations", ) op.drop_index( "ix_commercial_runtime_reservations_quota", table_name="commercial_runtime_reservations", ) op.create_index( "ix_commercial_runtime_reservations_quota", "commercial_runtime_reservations", ["tenant_id", "subscription_id", "entitlement_id", "period_key", "status"], ) op.drop_constraint( "fk_commercial_runtime_reservations_tenant_billing_period", "commercial_runtime_reservations", type_="foreignkey", ) op.drop_constraint( "ck_commercial_runtime_reservations_keys", "commercial_runtime_reservations", type_="check", ) op.create_check_constraint( "ck_commercial_runtime_reservations_keys", "commercial_runtime_reservations", "length(trim(run_id)) > 0 AND length(trim(tool_call_id)) > 0 " "AND length(trim(tool_type)) > 0 AND length(trim(tool_name)) > 0 " "AND length(trim(period_key)) > 0 " "AND length(trim(request_fingerprint)) > 0", ) op.drop_column("commercial_runtime_reservations", "quota_period_key") op.drop_column("commercial_runtime_reservations", "billing_period_id") op.alter_column( "commercial_runtime_reservations", "period_key", existing_type=sa.String(length=64), type_=sa.String(length=32), existing_nullable=False, ) def _unbind_costs() -> None: op.drop_index( "ix_commercial_cost_events_subscription_period", table_name="commercial_cost_events", ) op.create_index( "ix_commercial_cost_events_subscription_period", "commercial_cost_events", ["tenant_id", "subscription_id", "occurred_at"], ) op.drop_constraint( "fk_commercial_cost_events_tenant_billing_period", "commercial_cost_events", type_="foreignkey", ) op.drop_constraint( "ck_commercial_cost_events_billing_period_pair", "commercial_cost_events", type_="check", ) op.drop_column("commercial_cost_events", "billing_period_id") def _unbind_usage() -> None: op.drop_index("ix_usage_meter_events_billing_period", table_name="usage_meter_events") op.drop_index("ix_usage_meter_events_quota_window", table_name="usage_meter_events") op.create_index( "ix_usage_meter_events_quota_window", "usage_meter_events", ["tenant_id", "subscription_id", "metric_key", "period_key", "occurred_at"], ) op.drop_constraint( "fk_usage_meter_events_tenant_billing_period", "usage_meter_events", type_="foreignkey", ) op.drop_constraint("ck_usage_meter_events_keys", "usage_meter_events", type_="check") op.create_check_constraint( "ck_usage_meter_events_keys", "usage_meter_events", "length(trim(metric_key)) > 0 AND length(trim(unit)) > 0 " "AND length(trim(period_key)) > 0 AND length(trim(source_system)) > 0 " "AND length(trim(idempotency_key)) > 0 " "AND length(trim(request_fingerprint)) > 0", ) op.drop_column("usage_meter_events", "quota_period_key") op.drop_column("usage_meter_events", "billing_period_id") op.alter_column( "usage_meter_events", "period_key", existing_type=sa.String(length=64), type_=sa.String(length=32), existing_nullable=False, )