"""add marketplace tables: catalog_items, marketplace_listings, purchases, creator_balances Revision ID: 0013_add_marketplace_tables Revises: 0012_add_wallet_and_transactions Create Date: 2026-09-15 00:00:00.000000 docs/planning/economy.md US-3, US-4 (task 166836d55365). """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "0013_add_marketplace_tables" down_revision: Union[str, None] = "0012_add_wallet_and_transactions" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None ITEM_TYPES = ("asset", "avatar_item", "experience_pass") LISTING_STATUSES = ("active", "sold", "delisted", "removed_by_moderation") PURCHASE_STATUSES = ("pending_escrow", "completed", "refunded", "reversed") def upgrade() -> None: bind = op.get_bind() is_pg = bind.dialect.name == "postgresql" # Enums if is_pg: item_type_enum = postgresql.ENUM(*ITEM_TYPES, name="catalog_item_type", create_type=False) item_type_enum.create(bind, checkfirst=True) item_type_col = postgresql.ENUM(*ITEM_TYPES, name="catalog_item_type", create_type=False) listing_status_enum = postgresql.ENUM(*LISTING_STATUSES, name="marketplace_listing_status", create_type=False) listing_status_enum.create(bind, checkfirst=True) listing_status_col = postgresql.ENUM(*LISTING_STATUSES, name="marketplace_listing_status", create_type=False) purchase_status_enum = postgresql.ENUM(*PURCHASE_STATUSES, name="purchase_status", create_type=False) purchase_status_enum.create(bind, checkfirst=True) purchase_status_col = postgresql.ENUM(*PURCHASE_STATUSES, name="purchase_status", create_type=False) else: item_type_col = sa.Enum(*ITEM_TYPES, name="catalog_item_type") listing_status_col = sa.Enum(*LISTING_STATUSES, name="marketplace_listing_status") purchase_status_col = sa.Enum(*PURCHASE_STATUSES, name="purchase_status") # 1. catalog_items op.create_table( "catalog_items", sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("owner_id", sa.Integer(), nullable=False), sa.Column("asset_id", sa.Integer(), nullable=True), sa.Column("item_type", item_type_col, nullable=False), sa.Column("name", sa.String(length=100), nullable=False), sa.Column("description", sa.String(length=2000), nullable=True), sa.Column("metadata_json", postgresql.JSONB(astext_type=sa.Text()) if is_pg else sa.JSON(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.ForeignKeyConstraint(["owner_id"], ["users.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["asset_id"], ["assets.id"], ondelete="SET NULL"), sa.PrimaryKeyConstraint("id"), ) op.create_index(op.f("ix_catalog_items_owner_id"), "catalog_items", ["owner_id"], unique=False) op.create_index(op.f("ix_catalog_items_asset_id"), "catalog_items", ["asset_id"], unique=False) # 2. marketplace_listings op.create_table( "marketplace_listings", sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("catalog_item_id", sa.BigInteger(), nullable=False), sa.Column("seller_id", sa.Integer(), nullable=False), sa.Column("price", sa.BigInteger(), nullable=False), sa.Column("status", listing_status_col, server_default="active", nullable=False), 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("price > 0", name="check_listing_price_positive"), sa.ForeignKeyConstraint(["catalog_item_id"], ["catalog_items.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["seller_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), ) op.create_index(op.f("ix_marketplace_listings_catalog_item_id"), "marketplace_listings", ["catalog_item_id"], unique=False) op.create_index(op.f("ix_marketplace_listings_seller_id"), "marketplace_listings", ["seller_id"], unique=False) # 3. purchases op.create_table( "purchases", sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("listing_id", sa.BigInteger(), nullable=True), sa.Column("buyer_id", sa.Integer(), nullable=False), sa.Column("seller_id", sa.Integer(), nullable=False), sa.Column("price", sa.BigInteger(), nullable=False), sa.Column("platform_fee", sa.BigInteger(), nullable=False), sa.Column("seller_net", sa.BigInteger(), nullable=False), sa.Column("status", purchase_status_col, server_default="pending_escrow", nullable=False), sa.Column("escrow_hold_until", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.ForeignKeyConstraint(["listing_id"], ["marketplace_listings.id"], ondelete="SET NULL"), sa.ForeignKeyConstraint(["buyer_id"], ["users.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["seller_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), ) op.create_index(op.f("ix_purchases_listing_id"), "purchases", ["listing_id"], unique=False) op.create_index(op.f("ix_purchases_buyer_id"), "purchases", ["buyer_id"], unique=False) op.create_index(op.f("ix_purchases_seller_id"), "purchases", ["seller_id"], unique=False) # 4. creator_balances op.create_table( "creator_balances", sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), sa.Column("user_id", sa.Integer(), nullable=False), sa.Column("available", sa.BigInteger(), server_default="0", nullable=False), sa.Column("pending", sa.BigInteger(), server_default="0", nullable=False), sa.Column("lifetime_earned", sa.BigInteger(), server_default="0", nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.CheckConstraint("available >= 0", name="check_creator_available_non_negative"), sa.CheckConstraint("pending >= 0", name="check_creator_pending_non_negative"), sa.CheckConstraint("lifetime_earned >= 0", name="check_creator_lifetime_non_negative"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("user_id", name="uq_creator_balances_user_id"), ) op.create_index(op.f("ix_creator_balances_user_id"), "creator_balances", ["user_id"], unique=True) def downgrade() -> None: op.drop_index(op.f("ix_creator_balances_user_id"), table_name="creator_balances") op.drop_table("creator_balances") op.drop_index(op.f("ix_purchases_seller_id"), table_name="purchases") op.drop_index(op.f("ix_purchases_buyer_id"), table_name="purchases") op.drop_index(op.f("ix_purchases_listing_id"), table_name="purchases") op.drop_table("purchases") op.drop_index(op.f("ix_marketplace_listings_seller_id"), table_name="marketplace_listings") op.drop_index(op.f("ix_marketplace_listings_catalog_item_id"), table_name="marketplace_listings") op.drop_table("marketplace_listings") op.drop_index(op.f("ix_catalog_items_asset_id"), table_name="catalog_items") op.drop_index(op.f("ix_catalog_items_owner_id"), table_name="catalog_items") op.drop_table("catalog_items") bind = op.get_bind() if bind.dialect.name == "postgresql": sa.Enum(*PURCHASE_STATUSES, name="purchase_status").drop(bind, checkfirst=True) sa.Enum(*LISTING_STATUSES, name="marketplace_listing_status").drop(bind, checkfirst=True) sa.Enum(*ITEM_TYPES, name="catalog_item_type").drop(bind, checkfirst=True)