"""add apple_iap_transactions table and update points_ledger constraints Revision ID: 20260427_0001 Revises: 20260417_0001 Create Date: 2026-04-27 12:00:00 Changes: 1. Create apple_iap_transactions table for Apple IAP payment tracking 2. Update points_ledger check constraints: - Remove 'grant' from change_type (merged into 'adjust') - Add 'purchase' and 'refund' to change_type - Add 'payment' to biz_type - Update biz_binding constraint for new types - Update direction_by_change_type constraint - Add metadata shape constraints for purchase/refund - Update adjust metadata constraint (ticket_id -> reason) 3. Update points_audit_ledger check constraints similarly """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision: str = "20260427_0001" down_revision: Union[str, Sequence[str], None] = "20260417_0001" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "apple_iap_transactions", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("product_code", sa.String(32), nullable=False), sa.Column("app_store_product_id", sa.String(128), nullable=False), sa.Column("transaction_id", sa.String(64), nullable=False), sa.Column("original_transaction_id", sa.String(64), nullable=True), sa.Column("web_order_line_item_id", sa.String(64), nullable=True), sa.Column("environment", sa.String(16), nullable=False), sa.Column("bundle_id", sa.String(128), nullable=False), sa.Column("app_account_token", postgresql.UUID(as_uuid=True), nullable=True), sa.Column("purchase_date", sa.Text, nullable=False), sa.Column("revocation_date", sa.Text, nullable=True), sa.Column("status", sa.String(24), nullable=False), sa.Column("credits", sa.BigInteger, nullable=False), sa.Column("currency", sa.String(8), nullable=True), sa.Column("price_milliunits", sa.BigInteger, nullable=True), sa.Column("ledger_event_id", sa.String(64), nullable=True), sa.Column("signed_transaction_info", sa.Text, nullable=False), sa.Column( "apple_payload", postgresql.JSONB(), nullable=False, server_default=sa.text("'{}'::jsonb"), ), sa.Column("failure_code", sa.String(64), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.CheckConstraint( "environment in ('Sandbox', 'Production')", name="ck_apple_iap_transactions_environment", ), sa.CheckConstraint( "status in ('received', 'verified', 'granted', 'failed', 'refunded', 'refunded_insufficient', 'revoked')", name="ck_apple_iap_transactions_status", ), sa.UniqueConstraint("transaction_id", name="uq_apple_iap_transactions_transaction_id"), sa.UniqueConstraint("ledger_event_id", name="uq_apple_iap_transactions_ledger_event_id"), ) op.create_index( "ix_apple_iap_transactions_user_created_at", "apple_iap_transactions", ["user_id", sa.text("created_at DESC")], ) op.create_index( "ix_apple_iap_transactions_status_updated_at", "apple_iap_transactions", ["status", sa.text("updated_at DESC")], ) op.execute("ALTER TABLE apple_iap_transactions ENABLE ROW LEVEL SECURITY") op.execute( "CREATE POLICY anon_select_apple_iap_transactions ON apple_iap_transactions " "FOR SELECT TO anon USING (false)" ) op.execute( "CREATE POLICY anon_insert_apple_iap_transactions ON apple_iap_transactions " "FOR INSERT TO anon WITH CHECK (true)" ) op.execute( "CREATE POLICY anon_update_apple_iap_transactions ON apple_iap_transactions " "FOR UPDATE TO anon USING (false)" ) op.execute( "CREATE POLICY anon_delete_apple_iap_transactions ON apple_iap_transactions " "FOR DELETE TO anon USING (false)" ) op.execute( "CREATE POLICY authenticated_select_apple_iap_transactions ON apple_iap_transactions " "FOR SELECT TO authenticated USING (false)" ) op.execute( "CREATE POLICY authenticated_insert_apple_iap_transactions ON apple_iap_transactions " "FOR INSERT TO authenticated WITH CHECK (true)" ) op.execute( "CREATE POLICY authenticated_update_apple_iap_transactions ON apple_iap_transactions " "FOR UPDATE TO authenticated USING (false)" ) op.execute( "CREATE POLICY authenticated_delete_apple_iap_transactions ON apple_iap_transactions " "FOR DELETE TO authenticated USING (false)" ) op.drop_constraint("ck_points_ledger_change_type", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_change_type", "points_ledger", "change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')", ) op.drop_constraint("ck_points_ledger_biz_type", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_biz_type", "points_ledger", "biz_type is null or biz_type in ('chat', 'payment')", ) op.drop_constraint("ck_points_ledger_biz_binding", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_biz_binding", "points_ledger", "((change_type in ('register', 'adjust') and biz_type is null and biz_id is null) or " "(change_type = 'consume' and biz_type = 'chat' and biz_id is not null) or " "(change_type in ('purchase', 'refund') and biz_type = 'payment' and biz_id is not null))", ) op.drop_constraint("ck_points_ledger_direction_by_change_type", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_direction_by_change_type", "points_ledger", "((change_type in ('register', 'purchase') and direction = 1) or " "(change_type in ('consume', 'refund') and direction = -1) or " "(change_type = 'adjust' and direction in (1, -1)))", ) op.drop_constraint("ck_points_ledger_metadata_adjust_shape", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_metadata_adjust_shape", "points_ledger", "(change_type <> 'adjust' or (" "(metadata ? 'ext') and (metadata->'ext' ? 'reason') and " "coalesce(metadata #>> '{ext,reason}', '') <> ''))", ) op.create_check_constraint( "ck_points_ledger_metadata_payment_shape", "points_ledger", "(change_type not in ('purchase', 'refund') or (" "(metadata ? 'ext') and (metadata->'ext' ? 'source') and (metadata->'ext' ? 'platform') and " "(metadata->'ext' ? 'product_code') and (metadata->'ext' ? 'transaction_id') and " "coalesce(metadata #>> '{ext,source}', '') <> '' and " "coalesce(metadata #>> '{ext,platform}', '') <> '' and " "coalesce(metadata #>> '{ext,product_code}', '') <> '' and " "coalesce(metadata #>> '{ext,transaction_id}', '') <> ''))", ) op.create_check_constraint( "ck_points_ledger_metadata_refund_shape", "points_ledger", "(change_type <> 'refund' or (" "(metadata ? 'ext') and (metadata->'ext' ? 'original_event_id') and " "coalesce(metadata #>> '{ext,original_event_id}', '') <> ''))", ) op.drop_constraint("ck_points_audit_ledger_change_type", "points_audit_ledger", type_="check") op.create_check_constraint( "ck_points_audit_ledger_change_type", "points_audit_ledger", "change_type in ('register', 'consume', 'adjust', 'purchase', 'refund')", ) op.drop_constraint("ck_points_audit_ledger_biz_type", "points_audit_ledger", type_="check") op.create_check_constraint( "ck_points_audit_ledger_biz_type", "points_audit_ledger", "biz_type is null or biz_type in ('chat', 'payment')", ) def downgrade() -> None: op.drop_constraint("ck_points_audit_ledger_biz_type", "points_audit_ledger", type_="check") op.create_check_constraint( "ck_points_audit_ledger_biz_type", "points_audit_ledger", "biz_type is null or biz_type = 'chat'", ) op.drop_constraint("ck_points_audit_ledger_change_type", "points_audit_ledger", type_="check") op.create_check_constraint( "ck_points_audit_ledger_change_type", "points_audit_ledger", "change_type in ('register', 'consume', 'grant', 'adjust')", ) op.drop_constraint("ck_points_ledger_metadata_refund_shape", "points_ledger", type_="check") op.drop_constraint("ck_points_ledger_metadata_payment_shape", "points_ledger", type_="check") op.drop_constraint("ck_points_ledger_metadata_adjust_shape", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_metadata_adjust_shape", "points_ledger", "(change_type <> 'adjust' or (" "(metadata ? 'ext') and (metadata->'ext' ? 'ticket_id') and " "coalesce(metadata #>> '{ext,ticket_id}', '') <> ''))", ) op.drop_constraint("ck_points_ledger_direction_by_change_type", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_direction_by_change_type", "points_ledger", "((change_type in ('register', 'grant') and direction = 1) or " "(change_type = 'consume' and direction = -1) or " "(change_type = 'adjust' and direction in (1, -1)))", ) op.drop_constraint("ck_points_ledger_biz_binding", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_biz_binding", "points_ledger", "((change_type = 'register' and biz_type is null and biz_id is null) or " "(change_type in ('consume', 'grant', 'adjust') and biz_type = 'chat' and biz_id is not null))", ) op.drop_constraint("ck_points_ledger_biz_type", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_biz_type", "points_ledger", "biz_type is null or biz_type = 'chat'", ) op.drop_constraint("ck_points_ledger_change_type", "points_ledger", type_="check") op.create_check_constraint( "ck_points_ledger_change_type", "points_ledger", "change_type in ('register', 'consume', 'grant', 'adjust')", ) op.drop_index("ix_apple_iap_transactions_status_updated_at", table_name="apple_iap_transactions") op.drop_index("ix_apple_iap_transactions_user_created_at", table_name="apple_iap_transactions") op.execute("DROP POLICY IF EXISTS authenticated_delete_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS authenticated_update_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS authenticated_insert_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS authenticated_select_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS anon_delete_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS anon_update_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS anon_insert_apple_iap_transactions ON apple_iap_transactions") op.execute("DROP POLICY IF EXISTS anon_select_apple_iap_transactions ON apple_iap_transactions") op.execute("ALTER TABLE apple_iap_transactions DISABLE ROW LEVEL SECURITY") op.drop_table("apple_iap_transactions")