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