"""Add shop system with coins, items, inventory, certification Revision ID: 023_add_shop_system Revises: 022_add_notification_settings Create Date: 2025-01-05 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy import inspect # revision identifiers, used by Alembic. revision: str = '023_add_shop_system' down_revision: Union[str, None] = '022_add_notification_settings' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def column_exists(table_name: str, column_name: str) -> bool: bind = op.get_bind() inspector = inspect(bind) columns = [col['name'] for col in inspector.get_columns(table_name)] return column_name in columns def table_exists(table_name: str) -> bool: bind = op.get_bind() inspector = inspect(bind) return table_name in inspector.get_table_names() def upgrade() -> None: # === 1. Создаём таблицу shop_items === if not table_exists('shop_items'): op.create_table( 'shop_items', sa.Column('id', sa.Integer(), primary_key=True), sa.Column('item_type', sa.String(30), nullable=False, index=True), sa.Column('code', sa.String(50), nullable=False, unique=True, index=True), sa.Column('name', sa.String(100), nullable=False), sa.Column('description', sa.Text(), nullable=True), sa.Column('price', sa.Integer(), nullable=False), sa.Column('rarity', sa.String(20), nullable=False, server_default='common'), sa.Column('asset_data', sa.JSON(), nullable=True), sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'), sa.Column('available_from', sa.DateTime(), nullable=True), sa.Column('available_until', sa.DateTime(), nullable=True), sa.Column('stock_limit', sa.Integer(), nullable=True), sa.Column('stock_remaining', sa.Integer(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), ) # === 2. Создаём таблицу user_inventory === if not table_exists('user_inventory'): op.create_table( 'user_inventory', sa.Column('id', sa.Integer(), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True), sa.Column('item_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='CASCADE'), nullable=False, index=True), sa.Column('quantity', sa.Integer(), nullable=False, server_default='1'), sa.Column('equipped', sa.Boolean(), nullable=False, server_default='false'), sa.Column('purchased_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), sa.Column('expires_at', sa.DateTime(), nullable=True), ) # === 3. Создаём таблицу coin_transactions === if not table_exists('coin_transactions'): op.create_table( 'coin_transactions', sa.Column('id', sa.Integer(), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True), sa.Column('amount', sa.Integer(), nullable=False), sa.Column('transaction_type', sa.String(30), nullable=False), sa.Column('reference_type', sa.String(30), nullable=True), sa.Column('reference_id', sa.Integer(), nullable=True), sa.Column('description', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), ) # === 4. Создаём таблицу consumable_usages === if not table_exists('consumable_usages'): op.create_table( 'consumable_usages', sa.Column('id', sa.Integer(), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True), sa.Column('item_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='CASCADE'), nullable=False), sa.Column('marathon_id', sa.Integer(), sa.ForeignKey('marathons.id', ondelete='CASCADE'), nullable=True), sa.Column('assignment_id', sa.Integer(), sa.ForeignKey('assignments.id', ondelete='CASCADE'), nullable=True), sa.Column('used_at', sa.DateTime(), nullable=False, server_default=sa.func.now()), sa.Column('effect_data', sa.JSON(), nullable=True), ) # === 5. Добавляем поля в users === # coins_balance - баланс монет if not column_exists('users', 'coins_balance'): op.add_column('users', sa.Column('coins_balance', sa.Integer(), nullable=False, server_default='0')) # equipped_frame_id - экипированная рамка if not column_exists('users', 'equipped_frame_id'): op.add_column('users', sa.Column('equipped_frame_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True)) # equipped_title_id - экипированный титул if not column_exists('users', 'equipped_title_id'): op.add_column('users', sa.Column('equipped_title_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True)) # equipped_name_color_id - экипированный цвет ника if not column_exists('users', 'equipped_name_color_id'): op.add_column('users', sa.Column('equipped_name_color_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True)) # equipped_background_id - экипированный фон if not column_exists('users', 'equipped_background_id'): op.add_column('users', sa.Column('equipped_background_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True)) # === 6. Добавляем поля сертификации в marathons === # certification_status - статус сертификации if not column_exists('marathons', 'certification_status'): op.add_column('marathons', sa.Column('certification_status', sa.String(20), nullable=False, server_default='none')) # certification_requested_at - когда подана заявка if not column_exists('marathons', 'certification_requested_at'): op.add_column('marathons', sa.Column('certification_requested_at', sa.DateTime(), nullable=True)) # certified_at - когда сертифицирован if not column_exists('marathons', 'certified_at'): op.add_column('marathons', sa.Column('certified_at', sa.DateTime(), nullable=True)) # certified_by_id - кем сертифицирован if not column_exists('marathons', 'certified_by_id'): op.add_column('marathons', sa.Column('certified_by_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True)) # certification_rejection_reason - причина отказа if not column_exists('marathons', 'certification_rejection_reason'): op.add_column('marathons', sa.Column('certification_rejection_reason', sa.Text(), nullable=True)) # === 7. Добавляем настройки consumables в marathons === # allow_skips - разрешены ли скипы if not column_exists('marathons', 'allow_skips'): op.add_column('marathons', sa.Column('allow_skips', sa.Boolean(), nullable=False, server_default='true')) # max_skips_per_participant - лимит скипов на участника if not column_exists('marathons', 'max_skips_per_participant'): op.add_column('marathons', sa.Column('max_skips_per_participant', sa.Integer(), nullable=True)) # allow_consumables - разрешены ли расходуемые if not column_exists('marathons', 'allow_consumables'): op.add_column('marathons', sa.Column('allow_consumables', sa.Boolean(), nullable=False, server_default='true')) # === 8. Добавляем поля в participants === # coins_earned - заработано монет в марафоне if not column_exists('participants', 'coins_earned'): op.add_column('participants', sa.Column('coins_earned', sa.Integer(), nullable=False, server_default='0')) # skips_used - использовано скипов if not column_exists('participants', 'skips_used'): op.add_column('participants', sa.Column('skips_used', sa.Integer(), nullable=False, server_default='0')) # active_boost_multiplier - активный множитель буста if not column_exists('participants', 'active_boost_multiplier'): op.add_column('participants', sa.Column('active_boost_multiplier', sa.Float(), nullable=True)) # active_boost_expires_at - когда истекает буст if not column_exists('participants', 'active_boost_expires_at'): op.add_column('participants', sa.Column('active_boost_expires_at', sa.DateTime(), nullable=True)) # has_shield - есть ли активный щит if not column_exists('participants', 'has_shield'): op.add_column('participants', sa.Column('has_shield', sa.Boolean(), nullable=False, server_default='false')) def downgrade() -> None: # === Удаляем поля из participants === if column_exists('participants', 'has_shield'): op.drop_column('participants', 'has_shield') if column_exists('participants', 'active_boost_expires_at'): op.drop_column('participants', 'active_boost_expires_at') if column_exists('participants', 'active_boost_multiplier'): op.drop_column('participants', 'active_boost_multiplier') if column_exists('participants', 'skips_used'): op.drop_column('participants', 'skips_used') if column_exists('participants', 'coins_earned'): op.drop_column('participants', 'coins_earned') # === Удаляем поля consumables из marathons === if column_exists('marathons', 'allow_consumables'): op.drop_column('marathons', 'allow_consumables') if column_exists('marathons', 'max_skips_per_participant'): op.drop_column('marathons', 'max_skips_per_participant') if column_exists('marathons', 'allow_skips'): op.drop_column('marathons', 'allow_skips') # === Удаляем поля сертификации из marathons === if column_exists('marathons', 'certification_rejection_reason'): op.drop_column('marathons', 'certification_rejection_reason') if column_exists('marathons', 'certified_by_id'): op.drop_column('marathons', 'certified_by_id') if column_exists('marathons', 'certified_at'): op.drop_column('marathons', 'certified_at') if column_exists('marathons', 'certification_requested_at'): op.drop_column('marathons', 'certification_requested_at') if column_exists('marathons', 'certification_status'): op.drop_column('marathons', 'certification_status') # === Удаляем поля из users === if column_exists('users', 'equipped_background_id'): op.drop_column('users', 'equipped_background_id') if column_exists('users', 'equipped_name_color_id'): op.drop_column('users', 'equipped_name_color_id') if column_exists('users', 'equipped_title_id'): op.drop_column('users', 'equipped_title_id') if column_exists('users', 'equipped_frame_id'): op.drop_column('users', 'equipped_frame_id') if column_exists('users', 'coins_balance'): op.drop_column('users', 'coins_balance') # === Удаляем таблицы === if table_exists('consumable_usages'): op.drop_table('consumable_usages') if table_exists('coin_transactions'): op.drop_table('coin_transactions') if table_exists('user_inventory'): op.drop_table('user_inventory') if table_exists('shop_items'): op.drop_table('shop_items')