feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
590
server/alembic/versions/20260716_0016_commercial_metering.py
Normal file
590
server/alembic/versions/20260716_0016_commercial_metering.py
Normal file
@@ -0,0 +1,590 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user