725 lines
27 KiB
Python
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,
|
||
|
|
)
|