diff --git a/app/database/models.py b/app/database/models.py
index 291db5af..db2f0507 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -993,6 +993,7 @@ class PromoCode(Base):
valid_until = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True)
+ first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
promo_group_id = Column(Integer, ForeignKey("promo_groups.id", ondelete="SET NULL"), nullable=True, index=True)
diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py
index 7f91521b..07fdddcc 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -4630,6 +4630,38 @@ async def add_promocode_promo_group_column() -> bool:
return False
+async def add_promocode_first_purchase_only_column() -> bool:
+ """Добавляет колонку first_purchase_only в таблицу promocodes."""
+ column_exists = await check_column_exists('promocodes', 'first_purchase_only')
+ if column_exists:
+ logger.info("Колонка first_purchase_only уже существует в promocodes")
+ return True
+
+ try:
+ async with engine.begin() as conn:
+ db_type = await get_database_type()
+
+ if db_type == 'sqlite':
+ await conn.execute(
+ text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT 0")
+ )
+ elif db_type == 'postgresql':
+ await conn.execute(
+ text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT FALSE")
+ )
+ elif db_type == 'mysql':
+ await conn.execute(
+ text("ALTER TABLE promocodes ADD COLUMN first_purchase_only BOOLEAN DEFAULT FALSE")
+ )
+
+ logger.info("✅ Добавлена колонка first_purchase_only в promocodes")
+ return True
+
+ except Exception as error:
+ logger.error(f"❌ Ошибка добавления first_purchase_only в promocodes: {error}")
+ return False
+
+
async def migrate_contest_templates_prize_columns() -> bool:
"""Миграция contest_templates: prize_days -> prize_type + prize_value."""
try:
@@ -5085,6 +5117,13 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с добавлением promo_group_id в promocodes")
+ logger.info("=== ДОБАВЛЕНИЕ FIRST_PURCHASE_ONLY В PROMOCODES ===")
+ first_purchase_ready = await add_promocode_first_purchase_only_column()
+ if first_purchase_ready:
+ logger.info("✅ Колонка first_purchase_only в promocodes готова")
+ else:
+ logger.warning("⚠️ Проблемы с добавлением first_purchase_only в promocodes")
+
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ MAIN_MENU_BUTTONS ===")
main_menu_buttons_created = await create_main_menu_buttons_table()
if main_menu_buttons_created:
diff --git a/app/handlers/admin/promocodes.py b/app/handlers/admin/promocodes.py
index e8668282..13ea48a4 100644
--- a/app/handlers/admin/promocodes.py
+++ b/app/handlers/admin/promocodes.py
@@ -187,27 +187,39 @@ async def show_promocode_management(
if promo.valid_until:
text += f"⏰ Действует до: {format_datetime(promo.valid_until)}\n"
-
+
+ first_purchase_only = getattr(promo, 'first_purchase_only', False)
+ first_purchase_emoji = "✅" if first_purchase_only else "❌"
+ text += f"🆕 Только первая покупка: {first_purchase_emoji}\n"
+
text += f"📅 Создан: {format_datetime(promo.created_at)}\n"
-
+
+ first_purchase_btn_text = "🆕 Первая покупка: ✅" if first_purchase_only else "🆕 Первая покупка: ❌"
+
keyboard = [
[
types.InlineKeyboardButton(
- text="✏️ Редактировать",
+ text="✏️ Редактировать",
callback_data=f"promo_edit_{promo.id}"
),
types.InlineKeyboardButton(
- text="🔄 Переключить статус",
+ text="🔄 Переключить статус",
callback_data=f"promo_toggle_{promo.id}"
)
],
[
types.InlineKeyboardButton(
- text="📊 Статистика",
+ text=first_purchase_btn_text,
+ callback_data=f"promo_toggle_first_{promo.id}"
+ )
+ ],
+ [
+ types.InlineKeyboardButton(
+ text="📊 Статистика",
callback_data=f"promo_stats_{promo.id}"
),
types.InlineKeyboardButton(
- text="🗑️ Удалить",
+ text="🗑️ Удалить",
callback_data=f"promo_delete_{promo.id}"
)
],
@@ -946,7 +958,31 @@ async def toggle_promocode_status(
status_text = "активирован" if new_status else "деактивирован"
await callback.answer(f"✅ Промокод {status_text}", show_alert=True)
-
+
+ await show_promocode_management(callback, db_user, db)
+
+
+@admin_required
+@error_handler
+async def toggle_promocode_first_purchase(
+ callback: types.CallbackQuery,
+ db_user: User,
+ db: AsyncSession
+):
+ """Переключает режим 'только для первой покупки'."""
+ promo_id = int(callback.data.split('_')[-1])
+
+ promo = await get_promocode_by_id(db, promo_id)
+ if not promo:
+ await callback.answer("❌ Промокод не найден", show_alert=True)
+ return
+
+ new_status = not getattr(promo, 'first_purchase_only', False)
+ await update_promocode(db, promo, first_purchase_only=new_status)
+
+ status_text = "включён" if new_status else "выключен"
+ await callback.answer(f"✅ Режим 'первая покупка' {status_text}", show_alert=True)
+
await show_promocode_management(callback, db_user, db)
@@ -1124,6 +1160,7 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(process_promo_group_selection, F.data.startswith("promo_select_group_"))
dp.callback_query.register(show_promocode_management, F.data.startswith("promo_manage_"))
+ dp.callback_query.register(toggle_promocode_first_purchase, F.data.startswith("promo_toggle_first_"))
dp.callback_query.register(toggle_promocode_status, F.data.startswith("promo_toggle_"))
dp.callback_query.register(show_promocode_stats, F.data.startswith("promo_stats_"))
diff --git a/app/handlers/promocode.py b/app/handlers/promocode.py
index f8303acb..79c377ed 100644
--- a/app/handlers/promocode.py
+++ b/app/handlers/promocode.py
@@ -125,6 +125,10 @@ async def process_promocode(
"expired": texts.PROMOCODE_EXPIRED,
"used": texts.PROMOCODE_USED,
"already_used_by_user": texts.PROMOCODE_USED,
+ "not_first_purchase": texts.t(
+ "PROMOCODE_NOT_FIRST_PURCHASE",
+ "❌ Этот промокод доступен только для первой покупки"
+ ),
"server_error": texts.ERROR
}
diff --git a/app/services/promocode_service.py b/app/services/promocode_service.py
index 992139b1..548519b3 100644
--- a/app/services/promocode_service.py
+++ b/app/services/promocode_service.py
@@ -51,7 +51,13 @@ class PromoCodeService:
existing_use = await check_user_promocode_usage(db, user_id, promocode.id)
if existing_use:
return {"success": False, "error": "already_used_by_user"}
-
+
+ # Проверка "только для первой покупки"
+ if getattr(promocode, 'first_purchase_only', False):
+ has_purchase = await self._user_has_paid_purchase(db, user_id)
+ if has_purchase:
+ return {"success": False, "error": "not_first_purchase"}
+
balance_before_kopeks = user.balance_kopeks
result_description = await self._apply_promocode_effects(db, user, promocode)
@@ -228,3 +234,20 @@ class PromoCodeService:
effects.append("ℹ️ У вас уже есть активная подписка")
return "\n".join(effects) if effects else "✅ Промокод активирован"
+
+ async def _user_has_paid_purchase(self, db: AsyncSession, user_id: int) -> bool:
+ """Проверяет была ли у пользователя хотя бы одна успешная платная покупка."""
+ from sqlalchemy import select, func
+ from app.database.models import Transaction
+
+ result = await db.execute(
+ select(func.count(Transaction.id))
+ .where(
+ Transaction.user_id == user_id,
+ Transaction.status == "success",
+ Transaction.amount_kopeks > 0, # Платные транзакции
+ Transaction.type.in_(["subscription", "balance_topup", "renewal"])
+ )
+ )
+ count = result.scalar()
+ return count > 0