diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index 1a2ecafc..d186298d 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -74,6 +74,104 @@ async def check_column_exists(table_name: str, column_name: str) -> bool: logger.error(f"Ошибка проверки существования колонки {column_name}: {e}") return False + +async def check_constraint_exists(table_name: str, constraint_name: str) -> bool: + try: + async with engine.begin() as conn: + db_type = await get_database_type() + + if db_type == "postgresql": + result = await conn.execute( + text( + """ + SELECT 1 + FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = :table_name + AND constraint_name = :constraint_name + """ + ), + {"table_name": table_name, "constraint_name": constraint_name}, + ) + return result.fetchone() is not None + + if db_type == "mysql": + result = await conn.execute( + text( + """ + SELECT 1 + FROM information_schema.table_constraints + WHERE table_schema = DATABASE() + AND table_name = :table_name + AND constraint_name = :constraint_name + """ + ), + {"table_name": table_name, "constraint_name": constraint_name}, + ) + return result.fetchone() is not None + + if db_type == "sqlite": + result = await conn.execute(text(f"PRAGMA foreign_key_list({table_name})")) + rows = result.fetchall() + return any(row[5] == constraint_name for row in rows) + + return False + + except Exception as e: + logger.error( + f"Ошибка проверки существования ограничения {constraint_name} для {table_name}: {e}" + ) + return False + + +async def check_index_exists(table_name: str, index_name: str) -> bool: + try: + async with engine.begin() as conn: + db_type = await get_database_type() + + if db_type == "postgresql": + result = await conn.execute( + text( + """ + SELECT 1 + FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = :table_name + AND indexname = :index_name + """ + ), + {"table_name": table_name, "index_name": index_name}, + ) + return result.fetchone() is not None + + if db_type == "mysql": + result = await conn.execute( + text( + """ + SELECT 1 + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = :table_name + AND index_name = :index_name + """ + ), + {"table_name": table_name, "index_name": index_name}, + ) + return result.fetchone() is not None + + if db_type == "sqlite": + result = await conn.execute(text(f"PRAGMA index_list({table_name})")) + rows = result.fetchall() + return any(row[1] == index_name for row in rows) + + return False + + except Exception as e: + logger.error( + f"Ошибка проверки существования индекса {index_name} для {table_name}: {e}" + ) + return False + async def create_cryptobot_payments_table(): table_exists = await check_table_exists('cryptobot_payments') if table_exists: @@ -248,6 +346,276 @@ async def create_user_messages_table(): logger.error(f"Ошибка создания таблицы user_messages: {e}") return False + +async def ensure_promo_groups_setup(): + logger.info("=== НАСТРОЙКА ПРОМО ГРУПП ===") + + try: + promo_table_exists = await check_table_exists("promo_groups") + + async with engine.begin() as conn: + db_type = await get_database_type() + + if not promo_table_exists: + if db_type == "sqlite": + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS promo_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(255) NOT NULL, + server_discount_percent INTEGER NOT NULL DEFAULT 0, + traffic_discount_percent INTEGER NOT NULL DEFAULT 0, + device_discount_percent INTEGER NOT NULL DEFAULT 0, + is_default BOOLEAN NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + ) + await conn.execute( + text( + "CREATE UNIQUE INDEX IF NOT EXISTS uq_promo_groups_name ON promo_groups(name)" + ) + ) + elif db_type == "postgresql": + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS promo_groups ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + server_discount_percent INTEGER NOT NULL DEFAULT 0, + traffic_discount_percent INTEGER NOT NULL DEFAULT 0, + device_discount_percent INTEGER NOT NULL DEFAULT 0, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_promo_groups_name UNIQUE (name) + ) + """ + ) + ) + elif db_type == "mysql": + await conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS promo_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + server_discount_percent INT NOT NULL DEFAULT 0, + traffic_discount_percent INT NOT NULL DEFAULT 0, + device_discount_percent INT NOT NULL DEFAULT 0, + is_default TINYINT(1) NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_promo_groups_name (name) + ) ENGINE=InnoDB + """ + ) + ) + else: + logger.error(f"Неподдерживаемый тип БД для promo_groups: {db_type}") + return False + + logger.info("Создана таблица promo_groups") + + if db_type == "postgresql" and not await check_constraint_exists( + "promo_groups", "uq_promo_groups_name" + ): + try: + await conn.execute( + text( + "ALTER TABLE promo_groups ADD CONSTRAINT uq_promo_groups_name UNIQUE (name)" + ) + ) + except Exception as e: + logger.warning( + f"Не удалось добавить уникальное ограничение uq_promo_groups_name: {e}" + ) + + column_exists = await check_column_exists("users", "promo_group_id") + + if not column_exists: + if db_type == "sqlite": + await conn.execute(text("ALTER TABLE users ADD COLUMN promo_group_id INTEGER")) + elif db_type == "postgresql": + await conn.execute(text("ALTER TABLE users ADD COLUMN promo_group_id INTEGER")) + elif db_type == "mysql": + await conn.execute(text("ALTER TABLE users ADD COLUMN promo_group_id INT")) + else: + logger.error(f"Неподдерживаемый тип БД для promo_group_id: {db_type}") + return False + + logger.info("Добавлена колонка users.promo_group_id") + + index_exists = await check_index_exists("users", "ix_users_promo_group_id") + + if not index_exists: + try: + if db_type == "sqlite": + await conn.execute( + text("CREATE INDEX IF NOT EXISTS ix_users_promo_group_id ON users(promo_group_id)") + ) + elif db_type == "postgresql": + await conn.execute( + text("CREATE INDEX IF NOT EXISTS ix_users_promo_group_id ON users(promo_group_id)") + ) + elif db_type == "mysql": + await conn.execute( + text("CREATE INDEX ix_users_promo_group_id ON users(promo_group_id)") + ) + logger.info("Создан индекс ix_users_promo_group_id") + except Exception as e: + logger.warning(f"Не удалось создать индекс ix_users_promo_group_id: {e}") + + default_group_name = "Базовый юзер" + default_group_id = None + + result = await conn.execute( + text( + "SELECT id, is_default FROM promo_groups WHERE name = :name LIMIT 1" + ), + {"name": default_group_name}, + ) + row = result.fetchone() + + if row: + default_group_id = row[0] + if not row[1]: + await conn.execute( + text( + "UPDATE promo_groups SET is_default = :is_default WHERE id = :group_id" + ), + {"is_default": True, "group_id": default_group_id}, + ) + else: + result = await conn.execute( + text( + "SELECT id FROM promo_groups WHERE is_default = :is_default LIMIT 1" + ), + {"is_default": True}, + ) + existing_default = result.fetchone() + + if existing_default: + default_group_id = existing_default[0] + else: + await conn.execute( + text( + """ + INSERT INTO promo_groups ( + name, + server_discount_percent, + traffic_discount_percent, + device_discount_percent, + is_default + ) VALUES (:name, 0, 0, 0, :is_default) + """ + ), + {"name": default_group_name, "is_default": True}, + ) + + result = await conn.execute( + text( + "SELECT id FROM promo_groups WHERE name = :name LIMIT 1" + ), + {"name": default_group_name}, + ) + row = result.fetchone() + default_group_id = row[0] if row else None + + if default_group_id is None: + logger.error("Не удалось определить идентификатор базовой промо-группы") + return False + + await conn.execute( + text( + """ + UPDATE users + SET promo_group_id = :group_id + WHERE promo_group_id IS NULL + """ + ), + {"group_id": default_group_id}, + ) + + if db_type == "postgresql": + constraint_exists = await check_constraint_exists( + "users", "fk_users_promo_group_id_promo_groups" + ) + if not constraint_exists: + try: + await conn.execute( + text( + """ + ALTER TABLE users + ADD CONSTRAINT fk_users_promo_group_id_promo_groups + FOREIGN KEY (promo_group_id) + REFERENCES promo_groups(id) + ON DELETE RESTRICT + """ + ) + ) + logger.info("Добавлен внешний ключ users -> promo_groups") + except Exception as e: + logger.warning( + f"Не удалось добавить внешний ключ users.promo_group_id: {e}" + ) + + try: + await conn.execute( + text( + "ALTER TABLE users ALTER COLUMN promo_group_id SET NOT NULL" + ) + ) + except Exception as e: + logger.warning( + f"Не удалось сделать users.promo_group_id NOT NULL: {e}" + ) + + elif db_type == "mysql": + constraint_exists = await check_constraint_exists( + "users", "fk_users_promo_group_id_promo_groups" + ) + if not constraint_exists: + try: + await conn.execute( + text( + """ + ALTER TABLE users + ADD CONSTRAINT fk_users_promo_group_id_promo_groups + FOREIGN KEY (promo_group_id) + REFERENCES promo_groups(id) + ON DELETE RESTRICT + """ + ) + ) + logger.info("Добавлен внешний ключ users -> promo_groups") + except Exception as e: + logger.warning( + f"Не удалось добавить внешний ключ users.promo_group_id: {e}" + ) + + try: + await conn.execute( + text( + "ALTER TABLE users MODIFY promo_group_id INT NOT NULL" + ) + ) + except Exception as e: + logger.warning( + f"Не удалось сделать users.promo_group_id NOT NULL: {e}" + ) + + logger.info("✅ Промо группы настроены") + return True + + except Exception as e: + logger.error(f"Ошибка настройки промо групп: {e}") + return False + async def add_welcome_text_is_enabled_column(): column_exists = await check_column_exists('welcome_texts', 'is_enabled') if column_exists: @@ -688,7 +1056,14 @@ async def run_universal_migration(): logger.info("✅ Медиа поля в broadcast_history готовы") else: logger.warning("⚠️ Проблемы с добавлением медиа полей") - + + logger.info("=== НАСТРОЙКА ПРОМО ГРУПП ===") + promo_groups_ready = await ensure_promo_groups_setup() + if promo_groups_ready: + logger.info("✅ Промо группы готовы") + else: + logger.warning("⚠️ Проблемы с настройкой промо групп") + logger.info("=== ОБНОВЛЕНИЕ ВНЕШНИХ КЛЮЧЕЙ ===") fk_updated = await fix_foreign_keys_for_user_deletion() if fk_updated: @@ -756,10 +1131,12 @@ async def check_migration_status(): "cryptobot_table": False, "user_messages_table": False, "welcome_texts_table": False, - "welcome_texts_is_enabled_column": False, - "broadcast_history_media_fields": False, + "welcome_texts_is_enabled_column": False, + "broadcast_history_media_fields": False, "subscription_duplicates": False, - "subscription_conversions_table": False + "subscription_conversions_table": False, + "promo_groups_table": False, + "users_promo_group_column": False } status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup') @@ -768,8 +1145,10 @@ async def check_migration_status(): status["user_messages_table"] = await check_table_exists('user_messages') status["welcome_texts_table"] = await check_table_exists('welcome_texts') status["subscription_conversions_table"] = await check_table_exists('subscription_conversions') - + status["promo_groups_table"] = await check_table_exists('promo_groups') + status["welcome_texts_is_enabled_column"] = await check_column_exists('welcome_texts', 'is_enabled') + status["users_promo_group_column"] = await check_column_exists('users', 'promo_group_id') media_fields_exist = ( await check_column_exists('broadcast_history', 'has_media') and @@ -797,9 +1176,11 @@ async def check_migration_status(): "user_messages_table": "Таблица пользовательских сообщений", "welcome_texts_table": "Таблица приветственных текстов", "welcome_texts_is_enabled_column": "Поле is_enabled в welcome_texts", - "broadcast_history_media_fields": "Медиа поля в broadcast_history", + "broadcast_history_media_fields": "Медиа поля в broadcast_history", "subscription_conversions_table": "Таблица конверсий подписок", - "subscription_duplicates": "Отсутствие дубликатов подписок" + "subscription_duplicates": "Отсутствие дубликатов подписок", + "promo_groups_table": "Таблица промо-групп", + "users_promo_group_column": "Колонка promo_group_id у пользователей" } for check_key, check_status in status.items(): diff --git a/migrations/alembic/versions/1f5f3a3f5a4d_add_promo_groups_and_user_fk.py b/migrations/alembic/versions/1f5f3a3f5a4d_add_promo_groups_and_user_fk.py new file mode 100644 index 00000000..b16ac8c1 --- /dev/null +++ b/migrations/alembic/versions/1f5f3a3f5a4d_add_promo_groups_and_user_fk.py @@ -0,0 +1,224 @@ +"""add promo groups table and link users""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +PROMO_GROUPS_TABLE = "promo_groups" +USERS_TABLE = "users" +PROMO_GROUP_COLUMN = "promo_group_id" +PROMO_GROUP_INDEX = "ix_users_promo_group_id" +PROMO_GROUP_FK = "fk_users_promo_group_id_promo_groups" +DEFAULT_PROMO_GROUP_NAME = "Базовый юзер" + + +def _table_exists(inspector: sa.Inspector, table_name: str) -> bool: + return table_name in inspector.get_table_names() + + +def _column_exists(inspector: sa.Inspector, table_name: str, column_name: str) -> bool: + return any(col["name"] == column_name for col in inspector.get_columns(table_name)) + + +def _index_exists(inspector: sa.Inspector, table_name: str, index_name: str) -> bool: + return any(index["name"] == index_name for index in inspector.get_indexes(table_name)) + + +def _foreign_key_exists(inspector: sa.Inspector, table_name: str, fk_name: str) -> bool: + return any(fk["name"] == fk_name for fk in inspector.get_foreign_keys(table_name)) + +revision: str = "1f5f3a3f5a4d" +down_revision: Union[str, None] = "cbd1be472f3d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if not _table_exists(inspector, PROMO_GROUPS_TABLE): + op.create_table( + PROMO_GROUPS_TABLE, + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column( + "server_discount_percent", + sa.Integer(), + nullable=False, + server_default=sa.text("0"), + ), + sa.Column( + "traffic_discount_percent", + sa.Integer(), + nullable=False, + server_default=sa.text("0"), + ), + sa.Column( + "device_discount_percent", + sa.Integer(), + nullable=False, + server_default=sa.text("0"), + ), + sa.Column( + "is_default", + sa.Boolean(), + nullable=False, + server_default=sa.text("false"), + ), + sa.Column( + "created_at", + sa.DateTime(), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column( + "updated_at", + sa.DateTime(), + nullable=False, + server_default=sa.func.now(), + ), + sa.UniqueConstraint("name", name="uq_promo_groups_name"), + ) + inspector = sa.inspect(bind) + + if not _column_exists(inspector, USERS_TABLE, PROMO_GROUP_COLUMN): + op.add_column( + USERS_TABLE, + sa.Column(PROMO_GROUP_COLUMN, sa.Integer(), nullable=True), + ) + inspector = sa.inspect(bind) + + if _column_exists(inspector, USERS_TABLE, PROMO_GROUP_COLUMN): + if not _index_exists(inspector, USERS_TABLE, PROMO_GROUP_INDEX): + op.create_index(PROMO_GROUP_INDEX, USERS_TABLE, [PROMO_GROUP_COLUMN]) + + inspector = sa.inspect(bind) + if not _foreign_key_exists(inspector, USERS_TABLE, PROMO_GROUP_FK): + op.create_foreign_key( + PROMO_GROUP_FK, + USERS_TABLE, + PROMO_GROUPS_TABLE, + [PROMO_GROUP_COLUMN], + ["id"], + ondelete="RESTRICT", + ) + + inspector = sa.inspect(bind) + if not _table_exists(inspector, PROMO_GROUPS_TABLE) or not _column_exists( + inspector, USERS_TABLE, PROMO_GROUP_COLUMN + ): + return + + promo_groups_table = sa.table( + PROMO_GROUPS_TABLE, + sa.column("id", sa.Integer()), + sa.column("name", sa.String()), + sa.column("server_discount_percent", sa.Integer()), + sa.column("traffic_discount_percent", sa.Integer()), + sa.column("device_discount_percent", sa.Integer()), + sa.column("is_default", sa.Boolean()), + ) + + connection = bind + existing_named_group = ( + connection.execute( + sa.select( + promo_groups_table.c.id, + promo_groups_table.c.is_default, + ) + .where(promo_groups_table.c.name == DEFAULT_PROMO_GROUP_NAME) + .limit(1) + ) + .mappings() + .first() + ) + + if existing_named_group: + default_group_id = existing_named_group["id"] + if not existing_named_group["is_default"]: + connection.execute( + sa.update(promo_groups_table) + .where(promo_groups_table.c.id == default_group_id) + .values(is_default=True) + ) + else: + default_group_id = connection.execute( + sa.select(promo_groups_table.c.id) + .where(promo_groups_table.c.is_default.is_(True)) + .limit(1) + ).scalar_one_or_none() + + if default_group_id is None: + default_group_id = connection.execute( + sa.insert(promo_groups_table) + .values( + name=DEFAULT_PROMO_GROUP_NAME, + server_discount_percent=0, + traffic_discount_percent=0, + device_discount_percent=0, + is_default=True, + ) + .returning(promo_groups_table.c.id) + ).scalar_one() + + users_table = sa.table( + USERS_TABLE, + sa.column("promo_group_id", sa.Integer()), + ) + connection.execute( + sa.update(users_table) + .where(users_table.c.promo_group_id.is_(None)) + .values(promo_group_id=default_group_id) + ) + + inspector = sa.inspect(bind) + column_info = next( + (col for col in inspector.get_columns(USERS_TABLE) if col["name"] == PROMO_GROUP_COLUMN), + None, + ) + if column_info and column_info.get("nullable", True): + op.alter_column( + USERS_TABLE, + PROMO_GROUP_COLUMN, + existing_type=sa.Integer(), + nullable=False, + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if _column_exists(inspector, USERS_TABLE, PROMO_GROUP_COLUMN): + column_info = next( + ( + col + for col in inspector.get_columns(USERS_TABLE) + if col["name"] == PROMO_GROUP_COLUMN + ), + None, + ) + if column_info and not column_info.get("nullable", False): + op.alter_column( + USERS_TABLE, + PROMO_GROUP_COLUMN, + existing_type=sa.Integer(), + nullable=True, + ) + + inspector = sa.inspect(bind) + if _foreign_key_exists(inspector, USERS_TABLE, PROMO_GROUP_FK): + op.drop_constraint(PROMO_GROUP_FK, USERS_TABLE, type_="foreignkey") + + inspector = sa.inspect(bind) + if _index_exists(inspector, USERS_TABLE, PROMO_GROUP_INDEX): + op.drop_index(PROMO_GROUP_INDEX, table_name=USERS_TABLE) + + op.drop_column(USERS_TABLE, PROMO_GROUP_COLUMN) + + inspector = sa.inspect(bind) + if _table_exists(inspector, PROMO_GROUPS_TABLE): + op.drop_table(PROMO_GROUPS_TABLE) diff --git a/migrations/alembic/versions/5d1f1f8b2e9a_add_advertising_campaigns.py b/migrations/alembic/versions/5d1f1f8b2e9a_add_advertising_campaigns.py index 31a943b8..7f6d13e3 100644 --- a/migrations/alembic/versions/5d1f1f8b2e9a_add_advertising_campaigns.py +++ b/migrations/alembic/versions/5d1f1f8b2e9a_add_advertising_campaigns.py @@ -6,6 +6,21 @@ from alembic import op import sqlalchemy as sa +CAMPAIGNS_TABLE = "advertising_campaigns" +CAMPAIGNS_START_INDEX = "ix_advertising_campaigns_start_parameter" +CAMPAIGNS_ID_INDEX = "ix_advertising_campaigns_id" +REGISTRATIONS_TABLE = "advertising_campaign_registrations" +REGISTRATIONS_ID_INDEX = "ix_advertising_campaign_registrations_id" + + +def _table_exists(inspector: sa.Inspector, table_name: str) -> bool: + return table_name in inspector.get_table_names() + + +def _index_exists(inspector: sa.Inspector, table_name: str, index_name: str) -> bool: + return any(index["name"] == index_name for index in inspector.get_indexes(table_name)) + + revision: str = "5d1f1f8b2e9a" down_revision: Union[str, None] = "cbd1be472f3d" branch_labels: Union[str, Sequence[str], None] = None @@ -13,58 +28,119 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.create_table( - "advertising_campaigns", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("name", sa.String(length=255), nullable=False), - sa.Column("start_parameter", sa.String(length=64), nullable=False), - sa.Column("bonus_type", sa.String(length=20), nullable=False), - sa.Column("balance_bonus_kopeks", sa.Integer(), nullable=False, server_default="0"), - sa.Column("subscription_duration_days", sa.Integer(), nullable=True), - sa.Column("subscription_traffic_gb", sa.Integer(), nullable=True), - sa.Column("subscription_device_limit", sa.Integer(), nullable=True), - sa.Column("subscription_squads", sa.JSON(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")), - sa.Column("created_by", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), - sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), - sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"), - ) - op.create_index( - "ix_advertising_campaigns_start_parameter", - "advertising_campaigns", - ["start_parameter"], - unique=True, - ) - op.create_index( - "ix_advertising_campaigns_id", - "advertising_campaigns", - ["id"], - ) + bind = op.get_bind() + inspector = sa.inspect(bind) - op.create_table( - "advertising_campaign_registrations", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("campaign_id", sa.Integer(), nullable=False), - sa.Column("user_id", sa.Integer(), nullable=False), - sa.Column("bonus_type", sa.String(length=20), nullable=False), - sa.Column("balance_bonus_kopeks", sa.Integer(), nullable=False, server_default="0"), - sa.Column("subscription_duration_days", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), - sa.ForeignKeyConstraint(["campaign_id"], ["advertising_campaigns.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), - sa.UniqueConstraint("campaign_id", "user_id", name="uq_campaign_user"), - ) - op.create_index( - "ix_advertising_campaign_registrations_id", - "advertising_campaign_registrations", - ["id"], - ) + if not _table_exists(inspector, CAMPAIGNS_TABLE): + op.create_table( + CAMPAIGNS_TABLE, + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("start_parameter", sa.String(length=64), nullable=False), + sa.Column("bonus_type", sa.String(length=20), nullable=False), + sa.Column( + "balance_bonus_kopeks", + sa.Integer(), + nullable=False, + server_default="0", + ), + sa.Column("subscription_duration_days", sa.Integer(), nullable=True), + sa.Column("subscription_traffic_gb", sa.Integer(), nullable=True), + sa.Column("subscription_device_limit", sa.Integer(), nullable=True), + sa.Column("subscription_squads", sa.JSON(), nullable=True), + sa.Column( + "is_active", + sa.Boolean(), + nullable=False, + server_default=sa.text("true"), + ), + sa.Column("created_by", sa.Integer(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(), + server_default=sa.func.now(), + nullable=False, + ), + sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"), + ) + inspector = sa.inspect(bind) + + if not _index_exists(inspector, CAMPAIGNS_TABLE, CAMPAIGNS_START_INDEX): + op.create_index( + CAMPAIGNS_START_INDEX, + CAMPAIGNS_TABLE, + ["start_parameter"], + unique=True, + ) + + inspector = sa.inspect(bind) + if not _index_exists(inspector, CAMPAIGNS_TABLE, CAMPAIGNS_ID_INDEX): + op.create_index(CAMPAIGNS_ID_INDEX, CAMPAIGNS_TABLE, ["id"]) + + inspector = sa.inspect(bind) + if not _table_exists(inspector, REGISTRATIONS_TABLE): + op.create_table( + REGISTRATIONS_TABLE, + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("campaign_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("bonus_type", sa.String(length=20), nullable=False), + sa.Column( + "balance_bonus_kopeks", + sa.Integer(), + nullable=False, + server_default="0", + ), + sa.Column("subscription_duration_days", sa.Integer(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.func.now(), + nullable=False, + ), + sa.ForeignKeyConstraint( + ["campaign_id"], + [f"{CAMPAIGNS_TABLE}.id"], + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.UniqueConstraint("campaign_id", "user_id", name="uq_campaign_user"), + ) + inspector = sa.inspect(bind) + + if not _index_exists(inspector, REGISTRATIONS_TABLE, REGISTRATIONS_ID_INDEX): + op.create_index( + REGISTRATIONS_ID_INDEX, + REGISTRATIONS_TABLE, + ["id"], + ) def downgrade() -> None: - op.drop_index("ix_advertising_campaign_registrations_id", table_name="advertising_campaign_registrations") - op.drop_table("advertising_campaign_registrations") - op.drop_index("ix_advertising_campaigns_id", table_name="advertising_campaigns") - op.drop_index("ix_advertising_campaigns_start_parameter", table_name="advertising_campaigns") - op.drop_table("advertising_campaigns") + bind = op.get_bind() + inspector = sa.inspect(bind) + + if _index_exists(inspector, REGISTRATIONS_TABLE, REGISTRATIONS_ID_INDEX): + op.drop_index(REGISTRATIONS_ID_INDEX, table_name=REGISTRATIONS_TABLE) + + inspector = sa.inspect(bind) + if _table_exists(inspector, REGISTRATIONS_TABLE): + op.drop_table(REGISTRATIONS_TABLE) + + inspector = sa.inspect(bind) + if _index_exists(inspector, CAMPAIGNS_TABLE, CAMPAIGNS_ID_INDEX): + op.drop_index(CAMPAIGNS_ID_INDEX, table_name=CAMPAIGNS_TABLE) + + inspector = sa.inspect(bind) + if _index_exists(inspector, CAMPAIGNS_TABLE, CAMPAIGNS_START_INDEX): + op.drop_index(CAMPAIGNS_START_INDEX, table_name=CAMPAIGNS_TABLE) + + inspector = sa.inspect(bind) + if _table_exists(inspector, CAMPAIGNS_TABLE): + op.drop_table(CAMPAIGNS_TABLE)