Files
X-Financial/server/alembic/versions/20260716_0021_commercial_billing_periods.py
caoxiaozhu 787bc3a481 feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
2026-07-17 14:14:08 +08:00

725 lines
27 KiB
Python

"""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,
)