Files
X-Financial/server/alembic/versions/20260716_0016_commercial_metering.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

591 lines
25 KiB
Python

"""add tenant commercial contracts, entitlements, usage and cost ledgers
Revision ID: 20260716_0016
Revises: 20260716_0015
Create Date: 2026-07-16 23:10:00
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260716_0016"
down_revision: str | None = "20260716_0015"
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_0016 only supports PostgreSQL; "
f"refusing to mutate {dialect_name} without transactional constraint DDL"
)
def _require_empty_commercial_domain_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 (
"tenant_commercial_plans",
"tenant_subscriptions",
"commercial_entitlements",
"usage_meter_events",
"commercial_cost_events",
)
}
if any(counts.values()):
summary = ", ".join(f"{name}={count}" for name, count in counts.items())
raise RuntimeError(
"cannot downgrade commercial metering: contracts or immutable facts exist "
f"({summary})"
)
def _json_object_default() -> sa.TextClause:
return sa.text("'{}'::json")
def upgrade() -> None:
_require_postgresql()
op.create_table(
"tenant_commercial_plans",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=64), nullable=False),
sa.Column("plan_code", sa.String(length=80), nullable=False),
sa.Column("name", sa.String(length=160), nullable=False),
sa.Column("pricing_model", 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", sa.Numeric(20, 4), nullable=False),
sa.Column("included_seats", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"overage_enabled", sa.Boolean(), nullable=False, server_default=sa.text("false")
),
sa.Column("status", sa.String(length=16), nullable=False, server_default="draft"),
sa.Column("effective_from", sa.DateTime(timezone=True), nullable=False),
sa.Column("effective_to", sa.DateTime(timezone=True), nullable=True),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column(
"contract_terms_json",
sa.JSON(),
nullable=False,
server_default=_json_object_default(),
),
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.Column(
"updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.CheckConstraint(
"pricing_model IN ('subscription', 'usage', 'hybrid', 'pilot', 'custom')",
name="ck_tenant_commercial_plans_pricing_model",
),
sa.CheckConstraint(
"billing_interval IN ('monthly', 'quarterly', 'annual', 'contract')",
name="ck_tenant_commercial_plans_billing_interval",
),
sa.CheckConstraint(
"status IN ('draft', 'active', 'retired')",
name="ck_tenant_commercial_plans_status",
),
sa.CheckConstraint(
"base_fee >= 0 AND included_seats >= 0 AND version >= 1",
name="ck_tenant_commercial_plans_values",
),
sa.CheckConstraint(
"length(trim(plan_code)) > 0 AND length(trim(name)) > 0",
name="ck_tenant_commercial_plans_keys",
),
sa.CheckConstraint(
"length(trim(currency)) = 3", name="ck_tenant_commercial_plans_currency"
),
sa.CheckConstraint(
"effective_to IS NULL OR effective_to > effective_from",
name="ck_tenant_commercial_plans_effective_window",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"tenant_id", "id", name="uq_tenant_commercial_plans_tenant_id"
),
sa.UniqueConstraint(
"tenant_id",
"plan_code",
"version",
name="uq_tenant_commercial_plans_tenant_code_version",
),
)
op.create_index(
"uq_tenant_commercial_plans_active_code",
"tenant_commercial_plans",
["tenant_id", "plan_code"],
unique=True,
postgresql_where=sa.text("status = 'active'"),
)
op.create_index(
"ix_tenant_commercial_plans_tenant_status",
"tenant_commercial_plans",
["tenant_id", "status", "effective_from"],
)
op.create_table(
"tenant_subscriptions",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("tenant_id", sa.String(length=64), nullable=False),
sa.Column("subscription_key", sa.String(length=120), nullable=False),
sa.Column("plan_id", sa.String(length=36), nullable=False),
sa.Column("status", sa.String(length=20), nullable=False),
sa.Column("starts_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("ends_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("current_period_start", sa.DateTime(timezone=True), nullable=False),
sa.Column("current_period_end", sa.DateTime(timezone=True), nullable=False),
sa.Column("seats", sa.Integer(), nullable=False),
sa.Column("base_fee_snapshot", sa.Numeric(20, 4), nullable=False),
sa.Column("currency", sa.String(length=3), nullable=False),
sa.Column("billing_interval", sa.String(length=20), nullable=False),
sa.Column("auto_renew", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("external_provider", sa.String(length=60), nullable=True),
sa.Column("external_subscription_id", sa.String(length=160), nullable=True),
sa.Column("canceled_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column(
"metadata_json", sa.JSON(), nullable=False, server_default=_json_object_default()
),
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.Column(
"updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.CheckConstraint(
"status IN ('trialing', 'active', 'past_due', 'suspended', 'canceled', 'expired')",
name="ck_tenant_subscriptions_status",
),
sa.CheckConstraint(
"billing_interval IN ('monthly', 'quarterly', 'annual', 'contract')",
name="ck_tenant_subscriptions_billing_interval",
),
sa.CheckConstraint(
"seats > 0 AND base_fee_snapshot >= 0 AND version >= 1",
name="ck_tenant_subscriptions_values",
),
sa.CheckConstraint(
"length(trim(subscription_key)) > 0 AND length(trim(currency)) = 3",
name="ck_tenant_subscriptions_keys",
),
sa.CheckConstraint(
"current_period_end > current_period_start",
name="ck_tenant_subscriptions_period",
),
sa.CheckConstraint(
"ends_at IS NULL OR ends_at > starts_at",
name="ck_tenant_subscriptions_contract_window",
),
sa.CheckConstraint(
"(external_provider IS NULL AND external_subscription_id IS NULL) OR "
"(external_provider IS NOT NULL AND external_subscription_id IS NOT NULL)",
name="ck_tenant_subscriptions_external_pair",
),
sa.CheckConstraint(
"status != 'canceled' OR canceled_at IS NOT NULL",
name="ck_tenant_subscriptions_cancellation",
),
sa.ForeignKeyConstraint(
["tenant_id", "plan_id"],
["tenant_commercial_plans.tenant_id", "tenant_commercial_plans.id"],
name="fk_tenant_subscriptions_tenant_plan",
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("tenant_id", "id", name="uq_tenant_subscriptions_tenant_id"),
sa.UniqueConstraint(
"tenant_id", "subscription_key", name="uq_tenant_subscriptions_tenant_key"
),
sa.UniqueConstraint(
"tenant_id",
"external_provider",
"external_subscription_id",
name="uq_tenant_subscriptions_external_ref",
),
)
op.create_index(
"uq_tenant_subscriptions_current",
"tenant_subscriptions",
["tenant_id"],
unique=True,
postgresql_where=sa.text(
"status IN ('trialing', 'active', 'past_due', 'suspended')"
),
)
op.create_index(
"ix_tenant_subscriptions_tenant_status_period",
"tenant_subscriptions",
["tenant_id", "status", "current_period_end"],
)
op.create_table(
"commercial_entitlements",
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("entitlement_key", sa.String(length=120), nullable=False),
sa.Column("metric_key", sa.String(length=120), nullable=False),
sa.Column("entitlement_type", sa.String(length=20), nullable=False),
sa.Column("unit", sa.String(length=40), nullable=False),
sa.Column("included_quantity", sa.Numeric(20, 6), nullable=True),
sa.Column("hard_limit_quantity", sa.Numeric(20, 6), nullable=True),
sa.Column("reset_interval", sa.String(length=20), nullable=False),
sa.Column("overage_policy", sa.String(length=16), nullable=False),
sa.Column("status", sa.String(length=16), nullable=False),
sa.Column("effective_from", sa.DateTime(timezone=True), nullable=False),
sa.Column("effective_to", sa.DateTime(timezone=True), nullable=True),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.Column("config_json", sa.JSON(), nullable=False, server_default=_json_object_default()),
sa.Column(
"created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.Column(
"updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.CheckConstraint(
"entitlement_type IN ('feature', 'metered', 'unlimited')",
name="ck_commercial_entitlements_type",
),
sa.CheckConstraint(
"reset_interval IN ('none', 'monthly', 'quarterly', 'annual', 'contract')",
name="ck_commercial_entitlements_reset_interval",
),
sa.CheckConstraint(
"overage_policy IN ('block', 'allow', 'alert')",
name="ck_commercial_entitlements_overage_policy",
),
sa.CheckConstraint(
"status IN ('active', 'suspended', 'expired')",
name="ck_commercial_entitlements_status",
),
sa.CheckConstraint(
"version >= 1 AND (included_quantity IS NULL OR included_quantity >= 0) "
"AND (hard_limit_quantity IS NULL OR hard_limit_quantity >= 0)",
name="ck_commercial_entitlements_values",
),
sa.CheckConstraint(
"(entitlement_type = 'unlimited' AND included_quantity IS NULL "
"AND hard_limit_quantity IS NULL) OR "
"(entitlement_type = 'feature' AND included_quantity IN (0, 1) "
"AND (hard_limit_quantity IS NULL OR hard_limit_quantity IN (0, 1))) OR "
"(entitlement_type = 'metered' AND included_quantity IS NOT NULL "
"AND (hard_limit_quantity IS NULL OR hard_limit_quantity >= included_quantity))",
name="ck_commercial_entitlements_quota_shape",
),
sa.CheckConstraint(
"length(trim(entitlement_key)) > 0 AND length(trim(metric_key)) > 0 "
"AND length(trim(unit)) > 0",
name="ck_commercial_entitlements_keys",
),
sa.CheckConstraint(
"effective_to IS NULL OR effective_to > effective_from",
name="ck_commercial_entitlements_effective_window",
),
sa.ForeignKeyConstraint(
["tenant_id", "subscription_id"],
["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"],
name="fk_commercial_entitlements_tenant_subscription",
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"tenant_id", "id", name="uq_commercial_entitlements_tenant_id"
),
sa.UniqueConstraint(
"tenant_id",
"subscription_id",
"id",
name="uq_commercial_entitlements_tenant_subscription_id",
),
sa.UniqueConstraint(
"tenant_id",
"subscription_id",
"entitlement_key",
name="uq_commercial_entitlements_subscription_key",
),
)
op.create_index(
"ix_commercial_entitlements_subscription_status",
"commercial_entitlements",
["tenant_id", "subscription_id", "status"],
)
op.create_table(
"usage_meter_events",
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("entitlement_id", sa.String(length=36), nullable=False),
sa.Column("event_type", sa.String(length=16), nullable=False),
sa.Column("metric_key", sa.String(length=120), nullable=False),
sa.Column("quantity", sa.Numeric(20, 6), nullable=False),
sa.Column("unit", sa.String(length=40), nullable=False),
sa.Column("period_key", sa.String(length=32), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("source_system", sa.String(length=80), nullable=False),
sa.Column("idempotency_key", sa.String(length=160), nullable=False),
sa.Column("request_fingerprint", sa.String(length=80), nullable=False),
sa.Column("reversal_of_event_id", sa.String(length=36), nullable=True),
sa.Column("subject_type", sa.String(length=60), nullable=True),
sa.Column("subject_id", sa.String(length=160), nullable=True),
sa.Column("actor_type", sa.String(length=20), nullable=False),
sa.Column("actor_id", sa.String(length=120), nullable=False),
sa.Column("correlation_id", sa.String(length=120), nullable=True),
sa.Column("trace_id", sa.String(length=120), nullable=True),
sa.Column(
"metadata_json", sa.JSON(), nullable=False, server_default=_json_object_default()
),
sa.Column(
"recorded_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.CheckConstraint(
"event_type IN ('usage', 'credit', 'adjustment', 'reversal')",
name="ck_usage_meter_events_type",
),
sa.CheckConstraint(
"(event_type = 'usage' AND quantity > 0) OR "
"(event_type = 'credit' AND quantity < 0) OR "
"(event_type IN ('adjustment', 'reversal') AND quantity <> 0)",
name="ck_usage_meter_events_quantity",
),
sa.CheckConstraint(
"(event_type = 'reversal' AND reversal_of_event_id IS NOT NULL) OR "
"(event_type != 'reversal' AND reversal_of_event_id IS NULL)",
name="ck_usage_meter_events_reversal",
),
sa.CheckConstraint(
"(subject_type IS NULL AND subject_id IS NULL) OR "
"(subject_type IS NOT NULL AND subject_id IS NOT NULL)",
name="ck_usage_meter_events_subject_pair",
),
sa.CheckConstraint(
"actor_type IN ('system', 'user', 'integration', 'admin')",
name="ck_usage_meter_events_actor_type",
),
sa.CheckConstraint(
"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",
name="ck_usage_meter_events_keys",
),
sa.ForeignKeyConstraint(
["tenant_id", "subscription_id"],
["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"],
name="fk_usage_meter_events_tenant_subscription",
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["tenant_id", "subscription_id", "entitlement_id"],
[
"commercial_entitlements.tenant_id",
"commercial_entitlements.subscription_id",
"commercial_entitlements.id",
],
name="fk_usage_meter_events_tenant_entitlement",
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["tenant_id", "subscription_id", "entitlement_id", "reversal_of_event_id"],
[
"usage_meter_events.tenant_id",
"usage_meter_events.subscription_id",
"usage_meter_events.entitlement_id",
"usage_meter_events.id",
],
name="fk_usage_meter_events_tenant_reversal",
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("tenant_id", "id", name="uq_usage_meter_events_tenant_id"),
sa.UniqueConstraint(
"tenant_id",
"subscription_id",
"id",
name="uq_usage_meter_events_tenant_subscription_id",
),
sa.UniqueConstraint(
"tenant_id",
"subscription_id",
"entitlement_id",
"id",
name="uq_usage_meter_events_entitlement_id",
),
sa.UniqueConstraint(
"tenant_id",
"source_system",
"idempotency_key",
name="uq_usage_meter_events_source_request",
),
)
op.create_index(
"ix_usage_meter_events_quota_window",
"usage_meter_events",
["tenant_id", "subscription_id", "metric_key", "period_key", "occurred_at"],
)
op.create_index(
"ix_usage_meter_events_correlation",
"usage_meter_events",
["tenant_id", "correlation_id", "occurred_at"],
)
op.create_table(
"commercial_cost_events",
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=True),
sa.Column("usage_event_id", sa.String(length=36), nullable=True),
sa.Column("event_type", sa.String(length=16), nullable=False),
sa.Column("cost_category", sa.String(length=32), nullable=False),
sa.Column("quantity", sa.Numeric(20, 6), nullable=False),
sa.Column("unit", sa.String(length=40), nullable=False),
sa.Column("unit_cost", sa.Numeric(20, 8), nullable=False),
sa.Column("cost_amount", sa.Numeric(20, 4), nullable=False),
sa.Column("original_currency", sa.String(length=3), nullable=False),
sa.Column("reporting_amount", sa.Numeric(20, 4), nullable=False),
sa.Column("reporting_currency", sa.String(length=3), nullable=False),
sa.Column("fx_rate", sa.Numeric(20, 8), nullable=False),
sa.Column("provider", sa.String(length=120), nullable=True),
sa.Column("sku", sa.String(length=120), nullable=True),
sa.Column("model_name", sa.String(length=120), nullable=True),
sa.Column("allocation_key", sa.String(length=160), nullable=False),
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("source_system", sa.String(length=80), nullable=False),
sa.Column("idempotency_key", sa.String(length=160), nullable=False),
sa.Column("request_fingerprint", sa.String(length=80), nullable=False),
sa.Column("reversal_of_cost_event_id", sa.String(length=36), nullable=True),
sa.Column("correlation_id", sa.String(length=120), nullable=True),
sa.Column("trace_id", sa.String(length=120), nullable=True),
sa.Column(
"metadata_json", sa.JSON(), nullable=False, server_default=_json_object_default()
),
sa.Column(
"recorded_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
),
sa.CheckConstraint(
"event_type IN ('incurred', 'credit', 'adjustment', 'reversal')",
name="ck_commercial_cost_events_type",
),
sa.CheckConstraint(
"cost_category IN ('ai_inference', 'ocr', 'storage', 'connector', "
"'support', 'implementation', 'infrastructure', 'payment', 'other')",
name="ck_commercial_cost_events_category",
),
sa.CheckConstraint(
"quantity > 0 AND unit_cost >= 0 AND fx_rate > 0",
name="ck_commercial_cost_events_values",
),
sa.CheckConstraint(
"(event_type = 'incurred' AND cost_amount >= 0 AND reporting_amount >= 0) OR "
"(event_type = 'credit' AND cost_amount <= 0 AND reporting_amount <= 0) OR "
"(event_type IN ('adjustment', 'reversal') AND cost_amount <> 0 "
"AND reporting_amount <> 0)",
name="ck_commercial_cost_events_amount_direction",
),
sa.CheckConstraint(
"(event_type = 'reversal' AND reversal_of_cost_event_id IS NOT NULL) OR "
"(event_type != 'reversal' AND reversal_of_cost_event_id IS NULL)",
name="ck_commercial_cost_events_reversal",
),
sa.CheckConstraint(
"usage_event_id IS NULL OR subscription_id IS NOT NULL",
name="ck_commercial_cost_events_usage_pair",
),
sa.CheckConstraint(
"length(trim(unit)) > 0 AND length(trim(source_system)) > 0 "
"AND length(trim(idempotency_key)) > 0 "
"AND length(trim(request_fingerprint)) > 0 "
"AND length(trim(original_currency)) = 3 "
"AND length(trim(reporting_currency)) = 3",
name="ck_commercial_cost_events_keys",
),
sa.ForeignKeyConstraint(
["tenant_id", "subscription_id"],
["tenant_subscriptions.tenant_id", "tenant_subscriptions.id"],
name="fk_commercial_cost_events_tenant_subscription",
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["tenant_id", "subscription_id", "usage_event_id"],
[
"usage_meter_events.tenant_id",
"usage_meter_events.subscription_id",
"usage_meter_events.id",
],
name="fk_commercial_cost_events_tenant_usage",
ondelete="RESTRICT",
),
sa.ForeignKeyConstraint(
["tenant_id", "reversal_of_cost_event_id"],
["commercial_cost_events.tenant_id", "commercial_cost_events.id"],
name="fk_commercial_cost_events_tenant_reversal",
ondelete="RESTRICT",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"tenant_id", "id", name="uq_commercial_cost_events_tenant_id"
),
sa.UniqueConstraint(
"tenant_id",
"source_system",
"idempotency_key",
name="uq_commercial_cost_events_source_request",
),
)
op.create_index(
"ix_commercial_cost_events_tenant_period",
"commercial_cost_events",
["tenant_id", "occurred_at", "cost_category"],
)
op.create_index(
"ix_commercial_cost_events_subscription_period",
"commercial_cost_events",
["tenant_id", "subscription_id", "occurred_at"],
)
op.create_index(
"ix_commercial_cost_events_allocation",
"commercial_cost_events",
["tenant_id", "allocation_key", "occurred_at"],
)
op.execute(
"""
CREATE OR REPLACE FUNCTION prevent_commercial_events_mutation()
RETURNS trigger AS $$
BEGIN
RAISE EXCEPTION '% is append-only', TG_TABLE_NAME;
END;
$$ LANGUAGE plpgsql
"""
)
for table_name in ("usage_meter_events", "commercial_cost_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 downgrade() -> None:
_require_postgresql()
_require_empty_commercial_domain_for_downgrade()
for table_name in ("commercial_cost_events", "usage_meter_events"):
op.execute(f"DROP TRIGGER IF EXISTS trg_{table_name}_append_only ON {table_name}")
op.execute("DROP FUNCTION IF EXISTS prevent_commercial_events_mutation()")
op.drop_table("commercial_cost_events")
op.drop_table("usage_meter_events")
op.drop_table("commercial_entitlements")
op.drop_table("tenant_subscriptions")
op.drop_table("tenant_commercial_plans")