Files
modbison/backend/alembic/versions/0013_add_marketplace_tables.py
T

147 lines
7.9 KiB
Python

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