"""add transactional approval action idempotency ledger Revision ID: 20260716_0010 Revises: 20260716_0009 Create Date: 2026-07-16 16:10:00 """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "20260716_0010" down_revision: str | None = "20260716_0009" 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_0010 only supports PostgreSQL; " f"refusing to mutate {dialect_name} without transactional constraint DDL" ) def upgrade() -> None: _require_postgresql() op.create_table( "approval_action_ledgers", sa.Column("id", sa.String(length=36), nullable=False), sa.Column("tenant_id", sa.String(length=64), nullable=False), sa.Column("actor_id", sa.String(length=120), nullable=False), sa.Column("request_id", sa.String(length=120), nullable=False), sa.Column("claim_id", sa.String(length=36), nullable=False), sa.Column("action", sa.String(length=20), nullable=False), sa.Column("payload_fingerprint", sa.String(length=80), nullable=False), sa.Column("expected_status", sa.String(length=30), nullable=False), sa.Column("expected_approval_stage", sa.String(length=50), nullable=False), sa.Column("result_status", sa.String(length=30), nullable=True), sa.Column("result_approval_stage", sa.String(length=50), nullable=True), sa.Column("response_json", sa.JSON(), nullable=True), sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.CheckConstraint( "action IN ('approve', 'return', 'pay')", name="ck_approval_action_ledger_action", ), sa.CheckConstraint( "(completed_at IS NULL AND result_status IS NULL " "AND result_approval_stage IS NULL) OR " "(completed_at IS NOT NULL AND result_status IS NOT NULL " "AND result_approval_stage IS NOT NULL)", name="ck_approval_action_ledger_completion", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "tenant_id", "actor_id", "request_id", name="uq_approval_action_ledger_request", ), ) op.create_index( "ix_approval_action_ledger_claim_action", "approval_action_ledgers", ["tenant_id", "claim_id", "action"], unique=False, ) def downgrade() -> None: _require_postgresql() ledger_count = int( op.get_bind().scalar(sa.text("SELECT COUNT(*) FROM approval_action_ledgers")) or 0 ) if ledger_count: raise RuntimeError( "cannot downgrade approval action protocol: " f"approval_action_ledgers contains {ledger_count} audit record(s)" ) op.drop_index( "ix_approval_action_ledger_claim_action", table_name="approval_action_ledgers", ) op.drop_table("approval_action_ledgers")