diff --git a/bot/handlers/admin/__init__.py b/bot/handlers/admin/__init__.py
index ddfe2bb..2444ab2 100644
--- a/bot/handlers/admin/__init__.py
+++ b/bot/handlers/admin/__init__.py
@@ -2,7 +2,7 @@ from aiogram import Router
from . import common
from . import broadcast
-from . import promo_codes
+from .promo import promo_router_aggregate
from . import user_management
from . import statistics
from . import sync_admin
@@ -12,7 +12,7 @@ admin_router_aggregate = Router(name="admin_features_router")
admin_router_aggregate.include_router(common.router)
admin_router_aggregate.include_router(broadcast.router)
-admin_router_aggregate.include_router(promo_codes.router)
+admin_router_aggregate.include_router(promo_router_aggregate)
admin_router_aggregate.include_router(user_management.router)
admin_router_aggregate.include_router(statistics.router)
admin_router_aggregate.include_router(sync_admin.router)
diff --git a/bot/handlers/admin/common.py b/bot/handlers/admin/common.py
index f08e342..e051dd6 100644
--- a/bot/handlers/admin/common.py
+++ b/bot/handlers/admin/common.py
@@ -16,7 +16,9 @@ from bot.services.panel_api_service import PanelApiService
from bot.services.subscription_service import SubscriptionService
from . import broadcast as admin_broadcast_handlers
-from . import promo_codes as admin_promo_handlers
+from .promo import create as admin_promo_create_handlers
+from .promo import manage as admin_promo_manage_handlers
+from .promo import bulk as admin_promo_bulk_handlers
from . import user_management as admin_user_mgmnt_handlers
from . import statistics as admin_stats_handlers
from . import sync_admin as admin_sync_handlers
@@ -77,16 +79,16 @@ async def admin_panel_actions_callback_handler(
await admin_broadcast_handlers.broadcast_message_prompt_handler(
callback, state, i18n_data, settings, session)
elif action == "create_promo":
- await admin_promo_handlers.create_promo_prompt_handler(
+ await admin_promo_create_handlers.create_promo_prompt_handler(
callback, state, i18n_data, settings, session)
elif action == "create_bulk_promo":
- await admin_promo_handlers.create_bulk_promo_prompt_handler(
+ await admin_promo_bulk_handlers.create_bulk_promo_prompt_handler(
callback, state, i18n_data, settings, session)
elif action == "manage_promos":
- await admin_promo_handlers.manage_promo_codes_handler(
+ await admin_promo_manage_handlers.manage_promo_codes_handler(
callback, i18n_data, settings, session)
elif action == "view_promos":
- await admin_promo_handlers.view_promo_codes_handler(
+ await admin_promo_manage_handlers.view_promo_codes_handler(
callback, i18n_data, settings, session)
elif action == "ban_user_prompt":
await admin_user_mgmnt_handlers.ban_user_prompt_handler(
@@ -106,7 +108,7 @@ async def admin_panel_actions_callback_handler(
await admin_logs_handlers.display_logs_menu(callback, i18n_data,
settings, session)
elif action == "promo_management":
- await admin_promo_handlers.promo_management_handler(
+ await admin_promo_manage_handlers.promo_management_handler(
callback, i18n_data, settings, session)
elif action == "sync_panel":
diff --git a/bot/handlers/admin/promo/__init__.py b/bot/handlers/admin/promo/__init__.py
new file mode 100644
index 0000000..aeca5f9
--- /dev/null
+++ b/bot/handlers/admin/promo/__init__.py
@@ -0,0 +1,13 @@
+from aiogram import Router
+
+from . import create
+from . import manage
+from . import bulk
+
+promo_router_aggregate = Router(name="promo_features_router")
+
+promo_router_aggregate.include_router(create.router)
+promo_router_aggregate.include_router(manage.router)
+promo_router_aggregate.include_router(bulk.router)
+
+__all__ = ("promo_router_aggregate",)
\ No newline at end of file
diff --git a/bot/handlers/admin/promo/bulk.py b/bot/handlers/admin/promo/bulk.py
new file mode 100644
index 0000000..8a2f7c7
--- /dev/null
+++ b/bot/handlers/admin/promo/bulk.py
@@ -0,0 +1,511 @@
+import logging
+import random
+import string
+from aiogram import Router, F, types
+from aiogram.filters import StateFilter
+from aiogram.fsm.context import FSMContext
+from datetime import datetime, timedelta, timezone
+from typing import Optional
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from config.settings import Settings
+from db.dal import promo_code_dal
+from bot.states.admin_states import AdminStates
+from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
+from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
+from bot.middlewares.i18n import JsonI18n
+
+router = Router(name="promo_bulk_router")
+
+
+async def create_bulk_promo_prompt_handler(callback: types.CallbackQuery,
+ state: FSMContext, i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error preparing bulk promo creation.",
+ show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ # Step 1: Ask for quantity
+ prompt_text = _(
+ "admin_bulk_promo_step1_quantity",
+ default="π ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ (1-100):"
+ )
+
+ try:
+ await callback.message.edit_text(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
+ except Exception as e:
+ logging.warning(
+ f"Could not edit message for bulk promo prompt: {e}. Sending new.")
+ await callback.message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
+ await callback.answer()
+ await state.set_state(AdminStates.waiting_for_bulk_promo_quantity)
+
+
+def generate_unique_promo_code(length: int = 8) -> str:
+ """Generate a unique random promo code"""
+ characters = string.ascii_uppercase + string.digits
+ return ''.join(random.choice(characters) for _ in range(length))
+
+
+# Step 1: Process quantity
+@router.message(AdminStates.waiting_for_bulk_promo_quantity, F.text)
+async def process_bulk_promo_quantity_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ quantity = int(message.text.strip())
+ if not (1 <= quantity <= 100):
+ await message.answer(_(
+ "admin_bulk_promo_invalid_quantity",
+ default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 100"
+ ))
+ return
+
+ await state.update_data(quantity=quantity)
+
+ # Step 2: Ask for bonus days
+ prompt_text = _(
+ "admin_bulk_promo_step2_bonus_days",
+ default="π ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):",
+ quantity=quantity
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_bulk_promo_bonus_days)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing bulk promo quantity: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+# Step 2: Process bonus days
+@router.message(AdminStates.waiting_for_bulk_promo_bonus_days, F.text)
+async def process_bulk_promo_bonus_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ bonus_days = int(message.text.strip())
+ if not (1 <= bonus_days <= 365):
+ await message.answer(_(
+ "admin_promo_invalid_bonus_days",
+ default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+ return
+
+ await state.update_data(bonus_days=bonus_days)
+
+ # Step 3: Ask for max activations
+ data = await state.get_data()
+ prompt_text = _(
+ "admin_bulk_promo_step3_max_activations",
+ default="π ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠΈΠΌΠΈΡ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-10000):",
+ quantity=data.get("quantity"),
+ bonus_days=bonus_days
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_bulk_promo_max_activations)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing bulk promo bonus days: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+# Step 3: Process max activations
+@router.message(AdminStates.waiting_for_bulk_promo_max_activations, F.text)
+async def process_bulk_promo_max_activations_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ max_activations = int(message.text.strip())
+ if not (1 <= max_activations <= 10000):
+ await message.answer(_(
+ "admin_promo_invalid_max_activations",
+ default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
+ ))
+ return
+
+ await state.update_data(max_activations=max_activations)
+
+ # Step 4: Ask for validity
+ data = await state.get_data()
+ prompt_text = _(
+ "admin_bulk_promo_step4_validity",
+ default="π ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²:",
+ quantity=data.get("quantity"),
+ bonus_days=data.get("bonus_days"),
+ max_activations=max_activations
+ )
+
+ # Create keyboard for validity options
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_unlimited_validity", default="π ΠΠ΅Π· ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½ΠΈΠΉ"),
+ callback_data="bulk_promo_unlimited_validity"
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_set_validity_days", default="π
Π£ΠΊΠ°Π·Π°ΡΡ Π΄Π½ΠΈ"),
+ callback_data="bulk_promo_set_validity"
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_back_to_panel", default="π Π Π°Π΄ΠΌΠΈΠ½ ΠΏΠ°Π½Π΅Π»Ρ"),
+ callback_data="admin_action:main"
+ )
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_bulk_promo_validity_days)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing bulk promo max activations: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+# Step 4: Handle unlimited validity
+@router.callback_query(F.data == "bulk_promo_unlimited_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
+async def process_bulk_promo_unlimited_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ await state.update_data(validity_days=None)
+ await create_bulk_promo_codes_final(callback, state, i18n_data, settings, session)
+
+
+# Step 4: Handle set validity
+@router.callback_query(F.data == "bulk_promo_set_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
+async def process_bulk_promo_set_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error processing validity.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ data = await state.get_data()
+ prompt_text = _(
+ "admin_bulk_promo_enter_validity_days",
+ default="π ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ: {max_activations}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² (1-365):",
+ quantity=data.get("quantity"),
+ bonus_days=data.get("bonus_days"),
+ max_activations=data.get("max_activations")
+ )
+
+ try:
+ await callback.message.edit_text(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback.message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+# Step 4: Process validity days
+@router.message(AdminStates.waiting_for_bulk_promo_validity_days, F.text)
+async def process_bulk_promo_validity_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ validity_days = int(message.text.strip())
+ if not (1 <= validity_days <= 365):
+ await message.answer(_(
+ "admin_promo_invalid_validity_days",
+ default="β Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365 Π΄Π½Π΅ΠΉ"
+ ))
+ return
+
+ await state.update_data(validity_days=validity_days)
+ await create_bulk_promo_codes_final(message, state, i18n_data, settings, session)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing bulk promo validity days: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+async def create_bulk_promo_codes_final(callback_or_message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ """Final step - create multiple promo codes in database"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ data = await state.get_data()
+ quantity = data["quantity"]
+
+ # Show progress message
+ progress_text = _(
+ "admin_bulk_promo_creating",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ {quantity} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...",
+ quantity=quantity
+ )
+
+ if hasattr(callback_or_message, 'message'): # CallbackQuery
+ try:
+ await callback_or_message.message.edit_text(progress_text, parse_mode="HTML")
+ except Exception:
+ await callback_or_message.message.answer(progress_text, parse_mode="HTML")
+ await callback_or_message.answer()
+ else: # Message
+ await callback_or_message.answer(progress_text, parse_mode="HTML")
+
+ # Generate and create promo codes
+ created_codes = []
+ failed_codes = []
+
+ for i in range(quantity):
+ try:
+ # Generate unique code
+ attempts = 0
+ while attempts < 10: # Max 10 attempts to generate unique code
+ promo_code = generate_unique_promo_code()
+ existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
+ if not existing_promo:
+ break
+ attempts += 1
+
+ if attempts >= 10:
+ failed_codes.append(f"ΠΠΎΠ΄ #{i+1} (Π½Π΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠ³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°ΡΡ ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΡΠΉ)")
+ continue
+
+ # Prepare promo code data
+ promo_data = {
+ "code": promo_code,
+ "bonus_days": data["bonus_days"],
+ "max_activations": data["max_activations"],
+ "current_activations": 0,
+ "is_active": True,
+ "created_by_admin_id": callback_or_message.from_user.id,
+ "created_at": datetime.now(timezone.utc)
+ }
+
+ # Set validity
+ if data.get("validity_days"):
+ promo_data["valid_until"] = datetime.now(timezone.utc) + timedelta(days=data["validity_days"])
+ else:
+ promo_data["valid_until"] = None
+
+ # Create promo code
+ created_promo = await promo_code_dal.create_promo_code(session, promo_data)
+ created_codes.append(created_promo.code)
+
+ except Exception as e:
+ logging.error(f"Error creating bulk promo code #{i+1}: {e}")
+ failed_codes.append(f"ΠΠΎΠ΄ #{i+1} ({str(e)[:50]})")
+
+ await session.commit()
+
+ # Success message
+ success_lines = [
+ _(
+ "admin_bulk_promo_created_title",
+ default="β
ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π·Π°Π²Π΅ΡΡΠ΅Π½ΠΎ!\n"
+ ),
+ _(
+ "admin_bulk_promo_created_stats",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΎ: {created} ΠΈΠ· {total}",
+ created=len(created_codes),
+ total=quantity
+ )
+ ]
+
+ if data.get("validity_days"):
+ validity_text = f"{data['validity_days']} Π΄Π½Π΅ΠΉ"
+ else:
+ validity_text = _("admin_promo_unlimited", default="ΠΠ΅Π· ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½ΠΈΠΉ")
+
+ success_lines.append(
+ _(
+ "admin_bulk_promo_settings",
+ default="π ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n"
+ "π ΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ: {max_activations}\n"
+ "β° Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ: {validity}",
+ bonus_days=data["bonus_days"],
+ max_activations=data["max_activations"],
+ validity=validity_text
+ )
+ )
+
+ if created_codes:
+ success_lines.append("\nπ Π‘ΠΎΠ·Π΄Π°Π½Π½ΡΠ΅ ΠΊΠΎΠ΄Ρ:")
+ # Show first 20 codes, then indicate if there are more
+ codes_to_show = created_codes[:20]
+ for code in codes_to_show:
+ success_lines.append(f"{code}")
+
+ if len(created_codes) > 20:
+ success_lines.append(f"... ΠΈ Π΅ΡΠ΅ {len(created_codes) - 20} ΠΊΠΎΠ΄ΠΎΠ²")
+
+ if failed_codes:
+ success_lines.append(f"\nβ ΠΡΠΈΠ±ΠΊΠΈ ({len(failed_codes)}):")
+ for error in failed_codes[:5]: # Show first 5 errors
+ success_lines.append(error)
+ if len(failed_codes) > 5:
+ success_lines.append(f"... ΠΈ Π΅ΡΠ΅ {len(failed_codes) - 5} ΠΎΡΠΈΠ±ΠΎΠΊ")
+
+ success_text = "\n".join(success_lines)
+
+ if hasattr(callback_or_message, 'message'): # CallbackQuery
+ try:
+ await callback_or_message.message.edit_text(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback_or_message.message.answer(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ else: # Message
+ await callback_or_message.answer(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+
+ await state.clear()
+
+ except Exception as e:
+ logging.error(f"Error creating bulk promo codes: {e}")
+ error_text = _("error_occurred_try_again", default="β ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ°. ΠΠΎΠΏΡΠΎΠ±ΡΠΉΡΠ΅ ΡΠ½ΠΎΠ²Π°.")
+
+ if hasattr(callback_or_message, 'message'): # CallbackQuery
+ await callback_or_message.message.answer(error_text)
+ else: # Message
+ await callback_or_message.answer(error_text)
+
+ await state.clear()
+
+
+# Cancel bulk promo creation
+@router.callback_query(
+ F.data == "admin_action:main",
+ StateFilter(
+ AdminStates.waiting_for_bulk_promo_quantity,
+ AdminStates.waiting_for_bulk_promo_bonus_days,
+ AdminStates.waiting_for_bulk_promo_max_activations,
+ AdminStates.waiting_for_bulk_promo_validity_days,
+ ),
+)
+async def cancel_bulk_promo_creation_state_to_menu(callback: types.CallbackQuery,
+ state: FSMContext,
+ settings: Settings,
+ i18n_data: dict,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error cancelling.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ await callback.message.edit_text(
+ _(key="admin_panel_title"),
+ reply_markup=get_admin_panel_keyboard(i18n, current_lang, settings)
+ )
+ except Exception:
+ await callback.message.answer(
+ _(key="admin_panel_title"),
+ reply_markup=get_admin_panel_keyboard(i18n, current_lang, settings)
+ )
+
+ await callback.answer(_("admin_bulk_promo_creation_cancelled", default="ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² ΠΎΡΠΌΠ΅Π½Π΅Π½ΠΎ"))
+ await state.clear()
\ No newline at end of file
diff --git a/bot/handlers/admin/promo/create.py b/bot/handlers/admin/promo/create.py
new file mode 100644
index 0000000..7c7137f
--- /dev/null
+++ b/bot/handlers/admin/promo/create.py
@@ -0,0 +1,432 @@
+import logging
+from aiogram import Router, F, types
+from aiogram.filters import StateFilter
+from aiogram.fsm.context import FSMContext
+from datetime import datetime, timedelta, timezone
+from typing import Optional
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from config.settings import Settings
+from db.dal import promo_code_dal
+from bot.states.admin_states import AdminStates
+from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
+from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
+from bot.middlewares.i18n import JsonI18n
+
+router = Router(name="promo_create_router")
+
+
+async def create_promo_prompt_handler(callback: types.CallbackQuery,
+ state: FSMContext, i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error preparing promo creation.",
+ show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ # Step 1: Ask for promo code
+ prompt_text = _(
+ "admin_promo_step1_code",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):"
+ )
+
+ try:
+ await callback.message.edit_text(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
+ except Exception as e:
+ logging.warning(
+ f"Could not edit message for promo prompt: {e}. Sending new.")
+ await callback.message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML")
+ await callback.answer()
+ await state.set_state(AdminStates.waiting_for_promo_code)
+
+
+# Step 1: Process promo code
+@router.message(AdminStates.waiting_for_promo_code, F.text)
+async def process_promo_code_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ code_str = message.text.strip().upper()
+ if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
+ await message.answer(_(
+ "admin_promo_invalid_code_format",
+ default="β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)"
+ ))
+ return
+
+ # Check if code already exists
+ existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str)
+ if existing_promo:
+ await message.answer(_(
+ "admin_promo_code_already_exists",
+ default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Ρ ΡΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ"
+ ))
+ return
+
+ await state.update_data(promo_code=code_str)
+
+ # Step 2: Ask for bonus days
+ prompt_text = _(
+ "admin_promo_step2_bonus_days",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
+ code=code_str
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_promo_bonus_days)
+
+ except Exception as e:
+ logging.error(f"Error processing promo code: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+# Step 2: Process bonus days
+@router.message(AdminStates.waiting_for_promo_bonus_days, F.text)
+async def process_promo_bonus_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ bonus_days = int(message.text.strip())
+ if not (1 <= bonus_days <= 365):
+ await message.answer(_(
+ "admin_promo_invalid_bonus_days",
+ default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
+ ))
+ return
+
+ await state.update_data(bonus_days=bonus_days)
+
+ # Step 3: Ask for max activations
+ data = await state.get_data()
+ prompt_text = _(
+ "admin_promo_step3_max_activations",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠΈΠΌΠΈΡ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
+ code=data.get("promo_code"),
+ bonus_days=bonus_days
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_promo_max_activations)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing promo bonus days: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+# Step 3: Process max activations
+@router.message(AdminStates.waiting_for_promo_max_activations, F.text)
+async def process_promo_max_activations_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ max_activations = int(message.text.strip())
+ if not (1 <= max_activations <= 10000):
+ await message.answer(_(
+ "admin_promo_invalid_max_activations",
+ default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
+ ))
+ return
+
+ await state.update_data(max_activations=max_activations)
+
+ # Step 4: Ask for validity
+ data = await state.get_data()
+ prompt_text = _(
+ "admin_promo_step4_validity",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
+ code=data.get("promo_code"),
+ bonus_days=data.get("bonus_days"),
+ max_activations=max_activations
+ )
+
+ # Create keyboard for validity options
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_unlimited_validity", default="π ΠΠ΅Π· ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½ΠΈΠΉ"),
+ callback_data="promo_unlimited_validity"
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_set_validity_days", default="π
Π£ΠΊΠ°Π·Π°ΡΡ Π΄Π½ΠΈ"),
+ callback_data="promo_set_validity"
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_back_to_panel", default="π Π Π°Π΄ΠΌΠΈΠ½ ΠΏΠ°Π½Π΅Π»Ρ"),
+ callback_data="admin_action:main"
+ )
+ )
+
+ await message.answer(
+ prompt_text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ await state.set_state(AdminStates.waiting_for_promo_validity_days)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing promo max activations: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+# Step 4: Handle unlimited validity
+@router.callback_query(F.data == "promo_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
+async def process_promo_unlimited_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ await state.update_data(validity_days=None)
+ await create_promo_code_final(callback, state, i18n_data, settings, session)
+
+
+# Step 4: Handle set validity
+@router.callback_query(F.data == "promo_set_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
+async def process_promo_set_validity(callback: types.CallbackQuery,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error processing validity.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ data = await state.get_data()
+ prompt_text = _(
+ "admin_promo_enter_validity_days",
+ default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ: {max_activations}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):",
+ code=data.get("promo_code"),
+ bonus_days=data.get("bonus_days"),
+ max_activations=data.get("max_activations")
+ )
+
+ try:
+ await callback.message.edit_text(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback.message.answer(
+ prompt_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+
+# Step 4: Process validity days
+@router.message(AdminStates.waiting_for_promo_validity_days, F.text)
+async def process_promo_validity_days_handler(message: types.Message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ validity_days = int(message.text.strip())
+ if not (1 <= validity_days <= 365):
+ await message.answer(_(
+ "admin_promo_invalid_validity_days",
+ default="β Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365 Π΄Π½Π΅ΠΉ"
+ ))
+ return
+
+ await state.update_data(validity_days=validity_days)
+ await create_promo_code_final(message, state, i18n_data, settings, session)
+
+ except ValueError:
+ await message.answer(_(
+ "admin_promo_invalid_number",
+ default="β ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠ΅ ΡΠΈΡΠ»ΠΎ"
+ ))
+ except Exception as e:
+ logging.error(f"Error processing promo validity days: {e}")
+ await message.answer(_("error_occurred_try_again"))
+
+
+async def create_promo_code_final(callback_or_message,
+ state: FSMContext,
+ i18n_data: dict,
+ settings: Settings,
+ session: AsyncSession):
+ """Final step - create the promo code in database"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ data = await state.get_data()
+
+ # Prepare promo code data
+ promo_data = {
+ "code": data["promo_code"],
+ "bonus_days": data["bonus_days"],
+ "max_activations": data["max_activations"],
+ "current_activations": 0,
+ "is_active": True,
+ "created_by_admin_id": callback_or_message.from_user.id,
+ "created_at": datetime.now(timezone.utc)
+ }
+
+ # Set validity
+ if data.get("validity_days"):
+ promo_data["valid_until"] = datetime.now(timezone.utc) + timedelta(days=data["validity_days"])
+ else:
+ promo_data["valid_until"] = None
+
+ # Create promo code
+ created_promo = await promo_code_dal.create_promo_code(session, promo_data)
+ await session.commit()
+
+ # Success message
+ success_text = _(
+ "admin_promo_created_success",
+ default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΡΠΏΠ΅ΡΠ½ΠΎ ΡΠΎΠ·Π΄Π°Π½!\n\n"
+ "π ΠΠΎΠ΄: {code}\n"
+ "π ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n"
+ "π ΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ: {max_activations}\n"
+ "β° Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ: {validity}",
+ code=data["promo_code"],
+ bonus_days=data["bonus_days"],
+ max_activations=data["max_activations"],
+ validity=_("admin_promo_unlimited", default="ΠΠ΅Π· ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½ΠΈΠΉ") if not data.get("validity_days") else f"{data['validity_days']} Π΄Π½Π΅ΠΉ"
+ )
+
+ if hasattr(callback_or_message, 'message'): # CallbackQuery
+ try:
+ await callback_or_message.message.edit_text(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback_or_message.message.answer(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await callback_or_message.answer()
+ else: # Message
+ await callback_or_message.answer(
+ success_text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+
+ await state.clear()
+
+ except Exception as e:
+ logging.error(f"Error creating promo code: {e}")
+ error_text = _("error_occurred_try_again", default="β ΠΡΠΎΠΈΠ·ΠΎΡΠ»Π° ΠΎΡΠΈΠ±ΠΊΠ°. ΠΠΎΠΏΡΠΎΠ±ΡΠΉΡΠ΅ ΡΠ½ΠΎΠ²Π°.")
+
+ if hasattr(callback_or_message, 'message'): # CallbackQuery
+ await callback_or_message.message.answer(error_text)
+ await callback_or_message.answer()
+ else: # Message
+ await callback_or_message.answer(error_text)
+
+ await state.clear()
+
+
+# Cancel promo creation
+@router.callback_query(
+ F.data == "admin_action:main",
+ StateFilter(
+ AdminStates.waiting_for_promo_code,
+ AdminStates.waiting_for_promo_bonus_days,
+ AdminStates.waiting_for_promo_max_activations,
+ AdminStates.waiting_for_promo_validity_days,
+ ),
+)
+async def cancel_promo_creation_state_to_menu(callback: types.CallbackQuery,
+ state: FSMContext,
+ settings: Settings,
+ i18n_data: dict,
+ session: AsyncSession):
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error cancelling.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ await callback.message.edit_text(
+ _(key="admin_panel_title"),
+ reply_markup=get_admin_panel_keyboard(i18n, current_lang, settings)
+ )
+ except Exception:
+ await callback.message.answer(
+ _(key="admin_panel_title"),
+ reply_markup=get_admin_panel_keyboard(i18n, current_lang, settings)
+ )
+
+ await callback.answer(_("admin_promo_creation_cancelled", default="Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° ΠΎΡΠΌΠ΅Π½Π΅Π½ΠΎ"))
+ await state.clear()
\ No newline at end of file
diff --git a/bot/handlers/admin/promo/manage.py b/bot/handlers/admin/promo/manage.py
new file mode 100644
index 0000000..1b00406
--- /dev/null
+++ b/bot/handlers/admin/promo/manage.py
@@ -0,0 +1,422 @@
+import logging
+from aiogram import Router, F, types
+from aiogram.filters import StateFilter
+from aiogram.fsm.context import FSMContext
+from datetime import datetime, timedelta, timezone
+from typing import Optional, List
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from config.settings import Settings
+from db.dal import promo_code_dal
+from db.models import PromoCode, PromoCodeActivation
+from bot.states.admin_states import AdminStates
+from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
+from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
+from bot.middlewares.i18n import JsonI18n
+
+router = Router(name="promo_manage_router")
+
+
+async def view_promo_codes_handler(callback: types.CallbackQuery,
+ i18n_data: dict, settings: Settings,
+ session: AsyncSession):
+ """View all active promo codes"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error processing request.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ promo_models = await promo_code_dal.get_all_active_promo_codes(session,
+ limit=20,
+ offset=0)
+
+ if not promo_models:
+ text = _(
+ "admin_no_promo_codes",
+ default="π ΠΠΊΡΠΈΠ²Π½ΡΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ\n\nβ ΠΠ΅Ρ Π°ΠΊΡΠΈΠ²Π½ΡΡ
ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²"
+ )
+ else:
+ lines = [_("admin_active_promo_codes_title",
+ default="π ΠΠΊΡΠΈΠ²Π½ΡΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ\n")]
+
+ for promo in promo_models:
+ validity_str = _("admin_promo_unlimited", default="ΠΠ΅Π· ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½ΠΈΠΉ")
+ if promo.valid_until:
+ validity_str = promo.valid_until.strftime("%d.%m.%Y")
+
+ lines.append(
+ f"π {promo.code} | "
+ f"π {promo.bonus_days}Π΄ | "
+ f"π {promo.current_activations}/{promo.max_activations} | "
+ f"β° {validity_str}"
+ )
+
+ text = "\n".join(lines)
+
+ try:
+ await callback.message.edit_text(
+ text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback.message.answer(
+ text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error viewing promo codes: {e}")
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+
+async def promo_management_handler(callback: types.CallbackQuery,
+ i18n_data: dict, settings: Settings,
+ session: AsyncSession):
+ """Main promo management interface"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error processing request.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0)
+
+ if not promo_models:
+ text = _(
+ "admin_no_promo_codes_to_manage",
+ default="π Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°ΠΌΠΈ\n\nβ ΠΠ΅Ρ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π΄Π»Ρ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ"
+ )
+ await callback.message.edit_text(
+ text,
+ reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+ return
+
+ builder = InlineKeyboardBuilder()
+ text_lines = [_("admin_promo_management_title", default="π Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°ΠΌΠΈ\n")]
+
+ for promo in promo_models:
+ status_emoji = "β
" if promo.is_active else "β"
+ validity_str = _("admin_promo_unlimited", default="β")
+ if promo.valid_until:
+ validity_str = promo.valid_until.strftime("%d.%m")
+
+ text_lines.append(
+ f"{status_emoji} {promo.code} | "
+ f"π{promo.bonus_days}Π΄ | "
+ f"π{promo.current_activations}/{promo.max_activations} | "
+ f"β°{validity_str}"
+ )
+
+ builder.row(
+ InlineKeyboardButton(
+ text=f"π {promo.code}",
+ callback_data=f"promo_detail:{promo.promo_code_id}"
+ )
+ )
+
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_back_to_panel", default="π Π Π°Π΄ΠΌΠΈΠ½ ΠΏΠ°Π½Π΅Π»Ρ"),
+ callback_data="admin_action:main"
+ )
+ )
+
+ text = "\n".join(text_lines)
+
+ try:
+ await callback.message.edit_text(
+ text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback.message.answer(
+ text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error in promo management: {e}")
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+
+@router.callback_query(F.data.startswith("promo_detail:"))
+async def promo_detail_handler(callback: types.CallbackQuery,
+ i18n_data: dict, settings: Settings,
+ session: AsyncSession):
+ """Show detailed promo code information with management options"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error processing request.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ promo_id = int(callback.data.split(":")[1])
+ promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
+
+ if not promo:
+ await callback.answer(_("admin_promo_not_found", default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
+ return
+
+ # Format promo details
+ status_text = _("admin_promo_active", default="β
ΠΠΊΡΠΈΠ²Π΅Π½") if promo.is_active else _("admin_promo_inactive", default="β ΠΠ΅Π°ΠΊΡΠΈΠ²Π΅Π½")
+ validity_text = _("admin_promo_unlimited", default="ΠΠ΅Π· ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½ΠΈΠΉ")
+ if promo.valid_until:
+ if promo.valid_until > datetime.now(timezone.utc):
+ validity_text = promo.valid_until.strftime("%d.%m.%Y %H:%M")
+ else:
+ validity_text = f"{promo.valid_until.strftime('%d.%m.%Y %H:%M')} (ΠΈΡΡΠ΅ΠΊ)"
+
+ created_at = promo.created_at.strftime("%d.%m.%Y %H:%M") if promo.created_at else "N/A"
+
+ text = _(
+ "admin_promo_detail_info",
+ default="π ΠΠ΅ΡΠ°Π»ΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\n"
+ "π ΠΠΎΠ΄: {code}\n"
+ "π ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n"
+ "π ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠΉ: {current}/{max}\n"
+ "π Π‘ΡΠ°ΡΡΡ: {status}\n"
+ "β° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}\n"
+ "π€ Π‘ΠΎΠ·Π΄Π°Π»: {admin_id}\n"
+ "π Π‘ΠΎΠ·Π΄Π°Π½: {created}",
+ code=promo.code,
+ bonus_days=promo.bonus_days,
+ current=promo.current_activations,
+ max=promo.max_activations,
+ status=status_text,
+ validity=validity_text,
+ admin_id=promo.created_by_admin_id,
+ created=created_at
+ )
+
+ # Create management buttons
+ builder = InlineKeyboardBuilder()
+
+ # Edit button
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_edit", default="βοΈ Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°ΡΡ"),
+ callback_data=f"promo_edit:{promo_id}"
+ )
+ )
+
+ # Toggle active/inactive
+ toggle_text = _("admin_promo_deactivate", default="β ΠΠ΅Π°ΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°ΡΡ") if promo.is_active else _("admin_promo_activate", default="β
ΠΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°ΡΡ")
+ builder.row(
+ InlineKeyboardButton(
+ text=toggle_text,
+ callback_data=f"promo_toggle:{promo_id}"
+ )
+ )
+
+ # View activations
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_view_activations", default="π ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ"),
+ callback_data=f"promo_activations:{promo_id}"
+ )
+ )
+
+ # Delete button
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_promo_delete", default="π Π£Π΄Π°Π»ΠΈΡΡ"),
+ callback_data=f"promo_delete:{promo_id}"
+ )
+ )
+
+ # Back button
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_back_to_management", default="π Π ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ"),
+ callback_data="admin_action:promo_management"
+ )
+ )
+
+ try:
+ await callback.message.edit_text(
+ text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback.message.answer(
+ text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+ except ValueError:
+ await callback.answer(_("admin_invalid_promo_id", default="β ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ID ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"), show_alert=True)
+ except Exception as e:
+ logging.error(f"Error in promo detail: {e}")
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+
+@router.callback_query(F.data.startswith("promo_toggle:"))
+async def promo_toggle_handler(callback: types.CallbackQuery, i18n_data: dict,
+ settings: Settings, session: AsyncSession):
+ """Toggle promo code active status"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await callback.answer("Language service error.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ promo_id = int(callback.data.split(":")[1])
+ promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
+
+ if not promo:
+ await callback.answer(_("admin_promo_not_found", default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
+ return
+
+ new_status = not promo.is_active
+ update_data = {"is_active": new_status}
+
+ updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
+ if updated:
+ await session.commit()
+ status_text = _("admin_promo_activated", default="Π°ΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°Π½") if new_status else _("admin_promo_deactivated", default="Π΄Π΅Π°ΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°Π½")
+ await callback.answer(
+ _("admin_promo_status_changed", default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ {status}", status=status_text)
+ )
+ # Refresh the detail view
+ callback.data = f"promo_detail:{promo_id}"
+ await promo_detail_handler(callback, i18n_data, settings, session)
+ else:
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+ except ValueError:
+ await callback.answer(_("admin_invalid_promo_id", default="β ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ID ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"), show_alert=True)
+ except Exception as e:
+ logging.error(f"Error toggling promo: {e}")
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+
+@router.callback_query(F.data.startswith("promo_activations:"))
+async def promo_activations_handler(callback: types.CallbackQuery, i18n_data: dict,
+ settings: Settings, session: AsyncSession):
+ """Show promo code activations"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error processing request.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ promo_id = int(callback.data.split(":")[1])
+ promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
+
+ if not promo:
+ await callback.answer(_("admin_promo_not_found", default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
+ return
+
+ activations = await promo_code_dal.get_promo_activations_by_code_id(session, promo_id)
+
+ text_lines = [
+ _("admin_promo_activations_title",
+ default="π ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° {code}\n",
+ code=promo.code)
+ ]
+
+ if not activations:
+ text_lines.append(_("admin_no_activations", default="β ΠΠ΅Ρ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ"))
+ else:
+ for activation in activations[:10]: # Show last 10
+ activated_at = activation.activated_at.strftime("%d.%m.%Y %H:%M") if activation.activated_at else "N/A"
+ text_lines.append(
+ f"π€ {activation.user_id} | π {activated_at}"
+ )
+
+ if len(activations) > 10:
+ text_lines.append(f"\n... ΠΈ Π΅ΡΠ΅ {len(activations) - 10}")
+
+ text = "\n".join(text_lines)
+
+ builder = InlineKeyboardBuilder()
+ builder.row(
+ InlineKeyboardButton(
+ text=_("admin_back_to_promo", default="π Π ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ"),
+ callback_data=f"promo_detail:{promo_id}"
+ )
+ )
+
+ try:
+ await callback.message.edit_text(
+ text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ except Exception:
+ await callback.message.answer(
+ text,
+ reply_markup=builder.as_markup(),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+ except ValueError:
+ await callback.answer(_("admin_invalid_promo_id", default="β ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ID ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"), show_alert=True)
+ except Exception as e:
+ logging.error(f"Error viewing activations: {e}")
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+
+@router.callback_query(F.data.startswith("promo_delete:"))
+async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict,
+ settings: Settings, session: AsyncSession):
+ """Delete promo code"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await callback.answer("Language service error.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ promo_id = int(callback.data.split(":")[1])
+ promo = await promo_code_dal.delete_promo_code(session, promo_id)
+
+ if promo:
+ await session.commit()
+ await callback.answer(
+ _("admin_promo_deleted", default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠ΄Π°Π»Π΅Π½", code=promo.code)
+ )
+ # Go back to management
+ callback.data = "admin_action:promo_management"
+ await promo_management_handler(callback, i18n_data, settings, session)
+ else:
+ await callback.answer(_("admin_promo_not_found", default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
+
+ except ValueError:
+ await callback.answer(_("admin_invalid_promo_id", default="β ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ID ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"), show_alert=True)
+ except Exception as e:
+ logging.error(f"Error deleting promo: {e}")
+ await callback.answer(_("error_occurred_try_again"), show_alert=True)
+
+
+# Legacy handlers that redirect to new system
+async def manage_promo_codes_handler(callback: types.CallbackQuery,
+ i18n_data: dict, settings: Settings,
+ session: AsyncSession):
+ """Redirect to new unified handler"""
+ await promo_management_handler(callback, i18n_data, settings, session)
\ No newline at end of file
diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py
deleted file mode 100644
index aacf286..0000000
--- a/bot/handlers/admin/promo_codes.py
+++ /dev/null
@@ -1,1611 +0,0 @@
-import logging
-import random
-import string
-from aiogram import Router, F, types, Bot
-from aiogram.filters import StateFilter
-from aiogram.fsm.context import FSMContext
-from datetime import datetime, timedelta, timezone
-from typing import Optional
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from config.settings import Settings
-
-from db.dal import promo_code_dal
-
-from bot.states.admin_states import AdminStates
-from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
-from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
-from bot.middlewares.i18n import JsonI18n
-
-router = Router(name="admin_promo_codes_router")
-
-
-async def create_promo_prompt_handler(callback: types.CallbackQuery,
- state: FSMContext, i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error preparing promo creation.",
- show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- # Step 1: Ask for promo code
- prompt_text = _(
- "admin_promo_step1_code",
- default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ):"
- )
-
- try:
- await callback.message.edit_text(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- except Exception as e:
- logging.warning(
- f"Could not edit message for promo prompt: {e}. Sending new.")
- await callback.message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- await callback.answer()
- await state.set_state(AdminStates.waiting_for_promo_code)
-
-
-# Step 1: Process promo code
-@router.message(AdminStates.waiting_for_promo_code, F.text)
-async def process_promo_code_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- code_str = message.text.strip().upper()
- if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
- await message.answer(_(
- "admin_promo_invalid_code_format",
- default="β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)"
- ))
- return
-
- # Check if code already exists
- existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str)
- if existing_promo:
- await message.answer(_(
- "admin_promo_code_already_exists",
- default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Ρ ΡΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ"
- ))
- return
-
- await state.update_data(promo_code=code_str)
-
- # Step 2: Ask for bonus days
- prompt_text = _(
- "admin_promo_step2_bonus_days",
- default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
- code=code_str
- )
-
- await message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_promo_bonus_days)
-
- except Exception as e:
- logging.error(f"Error processing promo code: {e}")
- await message.answer(_(
- "admin_promo_processing_error",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"
- ))
-
-# Step 2: Process bonus days
-@router.message(AdminStates.waiting_for_promo_bonus_days, F.text)
-async def process_promo_bonus_days_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- bonus_days = int(message.text.strip())
- if bonus_days <= 0 or bonus_days > 365:
- await message.answer(_(
- "admin_promo_invalid_bonus_days",
- default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
- return
-
- data = await state.get_data()
- await state.update_data(bonus_days=bonus_days)
-
- # Step 3: Ask for max activations
- prompt_text = _(
- "admin_promo_step3_max_activations",
- default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
- code=data['promo_code'],
- bonus_days=bonus_days
- )
-
- await message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_promo_max_activations)
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_bonus_days",
- default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
-
-# Step 3: Process max activations
-@router.message(AdminStates.waiting_for_promo_max_activations, F.text)
-async def process_promo_max_activations_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- max_activations = int(message.text.strip())
- if max_activations <= 0 or max_activations > 10000:
- await message.answer(_(
- "admin_promo_invalid_max_activations",
- default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
- ))
- return
-
- data = await state.get_data()
- await state.update_data(max_activations=max_activations)
-
- # Step 4: Ask for validity days (with buttons)
- keyboard = InlineKeyboardBuilder()
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_promo_unlimited_validity", default="βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ"),
- callback_data="promo_unlimited_validity"
- )
- )
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_promo_set_validity", default="β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ"),
- callback_data="promo_set_validity"
- )
- )
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_panel_back_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
- callback_data="admin_panel_back"
- )
- )
-
- prompt_text = _(
- "admin_promo_step4_validity",
- default="π Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
- code=data['promo_code'],
- bonus_days=data['bonus_days'],
- max_activations=max_activations
- )
-
- await message.answer(
- prompt_text,
- reply_markup=keyboard.as_markup(),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_promo_validity_days)
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_max_activations",
- default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
- ))
-
-# Handle validity choice buttons
-@router.callback_query(F.data == "promo_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
-async def process_promo_unlimited_validity(callback: types.CallbackQuery,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- await state.update_data(validity_days=None)
- await create_promo_code_final(callback, state, i18n_data, settings, session)
-
-@router.callback_query(F.data == "promo_set_validity", StateFilter(AdminStates.waiting_for_promo_validity_days))
-async def process_promo_set_validity(callback: types.CallbackQuery,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await callback.answer("Language service error.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- prompt_text = _(
- "admin_promo_enter_validity_days",
- default="β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):"
- )
-
- try:
- await callback.message.edit_text(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
- except Exception as e:
- await callback.message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
- await callback.answer()
-
-# Step 4: Process validity days (when user chooses to set)
-@router.message(AdminStates.waiting_for_promo_validity_days, F.text)
-async def process_promo_validity_days_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- validity_days = int(message.text.strip())
- if validity_days <= 0 or validity_days > 365:
- await message.answer(_(
- "admin_promo_invalid_validity_days",
- default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
- return
-
- await state.update_data(validity_days=validity_days)
-
- # Create a fake callback for consistency
- fake_callback = types.CallbackQuery(
- id="fake",
- from_user=message.from_user,
- chat_instance="fake",
- message=message,
- data="fake"
- )
-
- await create_promo_code_final(fake_callback, state, i18n_data, settings, session)
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_validity_days",
- default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
-
-# Description handlers removed - not needed for this model
-
-async def create_promo_code_final(callback_or_message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- """Final step to create the promo code"""
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- if hasattr(callback_or_message, 'answer'):
- await callback_or_message.answer("Language service error.", show_alert=True)
- else:
- await callback_or_message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
- promo_code = data['promo_code']
- bonus_days = data['bonus_days']
- max_activations = data['max_activations']
- validity_days = data.get('validity_days')
-
- # Create validity date
- valid_until_date: Optional[datetime] = None
- valid_until_str_display = _("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
-
- if validity_days:
- valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
- valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
-
- # Show creating message
- creating_text = _(
- "admin_promo_creating",
- default="β³ Π‘ΠΎΠ·Π΄Π°Ρ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄..."
- )
-
- if hasattr(callback_or_message, 'message'):
- try:
- await callback_or_message.message.edit_text(creating_text)
- except:
- await callback_or_message.message.answer(creating_text)
- if hasattr(callback_or_message, 'answer'):
- await callback_or_message.answer()
- else:
- await callback_or_message.answer(creating_text)
-
- # Create promo code
- try:
- promo_data = {
- "code": promo_code,
- "bonus_days": bonus_days,
- "max_activations": max_activations,
- "current_activations": 0,
- "is_active": True,
- "valid_until": valid_until_date,
- "created_by_admin_id": callback_or_message.from_user.id if hasattr(callback_or_message, 'from_user') and callback_or_message.from_user else 0
- }
-
- await promo_code_dal.create_promo_code(session, promo_data)
- await session.commit()
-
- # Send success message
- success_text = _(
- "admin_promo_created_success",
- default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΡΠΎΠ·Π΄Π°Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {valid_until_str}",
- code=promo_code,
- bonus_days=bonus_days,
- max_activations=max_activations,
- valid_until_str=valid_until_str_display
- )
-
- if hasattr(callback_or_message, 'message'):
- target_message = callback_or_message.message
- else:
- target_message = callback_or_message
-
- await target_message.answer(
- success_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
-
- except Exception as e:
- logging.error(f"Error creating promo code: {e}")
- await session.rollback()
- error_text = _(
- "admin_promo_creation_failed",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {error}",
- error=str(e)
- )
-
- if hasattr(callback_or_message, 'message'):
- target_message = callback_or_message.message
- else:
- target_message = callback_or_message
-
- await target_message.answer(
- error_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
-
- await state.clear()
-
-# Legacy handler removed - using step-by-step creation now
-
-
-async def view_promo_codes_handler(callback: types.CallbackQuery,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error displaying promo codes.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- promo_models = await promo_code_dal.get_all_active_promo_codes(session,
- limit=20,
- offset=0)
-
- if not promo_models:
- await callback.message.edit_text(
- _("admin_no_active_promos"),
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
- await callback.answer()
- return
-
- response_text_parts = [f"{_('admin_active_promos_list_header')}\n"]
- for promo in promo_models:
- valid_until_display_text = _("admin_promo_valid_indefinitely")
- if promo.valid_until:
-
- valid_until_display_text = promo.valid_until.strftime('%Y-%m-%d')
-
- response_text_parts.append(
- _("admin_promo_list_item",
- code=promo.code,
- bonus=promo.bonus_days,
- current=promo.current_activations,
- max=promo.max_activations,
- valid_until=valid_until_display_text))
-
- final_text = "\n".join(response_text_parts)
- try:
- await callback.message.edit_text(
- final_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- except Exception as e:
- logging.warning(
- f"Failed to edit message for promo list: {e}. Sending new.")
- if callback.message:
- await callback.message.answer(
- final_text,
- reply_markup=get_back_to_admin_panel_keyboard(
- current_lang, i18n),
- parse_mode="HTML")
- await callback.answer()
-
-
-# New unified promo management system
-@router.callback_query(F.data == "admin_action:promo_management")
-async def promo_management_handler(callback: types.CallbackQuery,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- """Show list of all promo codes for management"""
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error displaying promo codes.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- # Get ALL promo codes (including inactive)
- promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0)
- if not promo_models:
- await callback.message.edit_text(
- _("admin_promo_management_empty", default="π ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ ΠΎΡΡΡΡΡΡΠ²ΡΡΡ"),
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
- await callback.answer()
- return
-
- kb = InlineKeyboardBuilder()
- for promo in promo_models:
- # Show promo code with status indicator
- status_emoji = "β
" if promo.is_active else "π«"
- if promo.valid_until and promo.valid_until < datetime.now(timezone.utc):
- status_emoji = "β°" # Expired
- elif promo.current_activations >= promo.max_activations:
- status_emoji = "π" # Used up
-
- button_text = f"{status_emoji} {promo.code} ({promo.current_activations}/{promo.max_activations})"
- kb.row(
- InlineKeyboardButton(
- text=button_text,
- callback_data=f"promo_detail:{promo.promo_code_id}")
- )
-
- kb.row(
- InlineKeyboardButton(text=_("back_to_admin_panel_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
- callback_data="admin_action:main"))
-
- await callback.message.edit_text(
- _("admin_promo_management_title", default="π Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°ΠΌΠΈ\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π΄Π»Ρ Π΄Π΅ΡΠ°Π»ΡΠ½ΠΎΠ³ΠΎ ΠΏΡΠΎΡΠΌΠΎΡΡΠ°:"),
- reply_markup=kb.as_markup(),
- parse_mode="HTML")
- await callback.answer()
-
-
-@router.callback_query(F.data.startswith("promo_detail:"))
-async def promo_detail_handler(callback: types.CallbackQuery,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- """Show detailed promo code information with management options"""
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error displaying promo details.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- promo_id = int(callback.data.split(":")[1])
- promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
- if not promo:
- await callback.answer(_("admin_promo_not_found", default="ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
- return
-
- # Determine status
- status = _("admin_promo_status_active", default="β
ΠΠΊΡΠΈΠ²Π΅Π½")
- if not promo.is_active:
- status = _("admin_promo_status_inactive", default="π« ΠΠ΅Π°ΠΊΡΠΈΠ²Π΅Π½")
- elif promo.valid_until and promo.valid_until < datetime.now(timezone.utc):
- status = _("admin_promo_status_expired", default="β° ΠΡΡΠ΅ΠΊ")
- elif promo.current_activations >= promo.max_activations:
- status = _("admin_promo_status_used_up", default="π ΠΡΡΠ΅ΡΠΏΠ°Π½")
-
- # Format validity
- validity = _("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
- if promo.valid_until:
- validity = promo.valid_until.strftime('%Y-%m-%d %H:%M')
-
- # Format created date
- created_date = promo.created_at.strftime('%Y-%m-%d %H:%M') if promo.created_at else "N/A"
- creator = f"Admin {promo.created_by_admin_id}" if promo.created_by_admin_id else "N/A"
-
- # Build card text
- card_text = _(
- "admin_promo_card_title",
- default="π ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄: {code}",
- code=promo.code
- ) + "\n\n"
-
- card_text += _(
- "admin_promo_card_bonus_days",
- default="π ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}",
- days=promo.bonus_days
- ) + "\n"
-
- card_text += _(
- "admin_promo_card_activations",
- default="π’ ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {current}/{max}",
- current=promo.current_activations,
- max=promo.max_activations
- ) + "\n"
-
- card_text += _(
- "admin_promo_card_validity",
- default="β° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}",
- validity=validity
- ) + "\n"
-
- card_text += _(
- "admin_promo_card_status",
- default="π Π‘ΡΠ°ΡΡΡ: {status}",
- status=status
- ) + "\n"
-
- card_text += _(
- "admin_promo_card_created",
- default="π
Π‘ΠΎΠ·Π΄Π°Π½: {created}",
- created=created_date
- ) + "\n"
-
- card_text += _(
- "admin_promo_card_created_by",
- default="π€ Π‘ΠΎΠ·Π΄Π°Π»: {creator}",
- creator=creator
- )
-
- # Build keyboard
- kb = InlineKeyboardBuilder()
-
- # Row 1: Edit and Toggle status
- kb.row(
- InlineKeyboardButton(
- text=_("admin_promo_edit_button", default="βοΈ Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°ΡΡ"),
- callback_data=f"promo_edit:{promo_id}"),
- InlineKeyboardButton(
- text=_("admin_promo_toggle_status_button", default="π ΠΠΊΠ»/ΠΡΠΊΠ»"),
- callback_data=f"promo_toggle:{promo_id}")
- )
-
- # Row 2: View activations and Delete
- kb.row(
- InlineKeyboardButton(
- text=_("admin_promo_view_activations_button", default="π ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ"),
- callback_data=f"promo_activations:{promo_id}"),
- InlineKeyboardButton(
- text=_("admin_promo_delete_button", default="π Π£Π΄Π°Π»ΠΈΡΡ"),
- callback_data=f"promo_delete:{promo_id}")
- )
-
- # Row 3: Back to list
- kb.row(
- InlineKeyboardButton(
- text=_("admin_promo_back_to_list_button", default="β¬
οΈ Π ΡΠΏΠΈΡΠΊΡ"),
- callback_data="admin_action:promo_management")
- )
-
- await callback.message.edit_text(
- card_text,
- reply_markup=kb.as_markup(),
- parse_mode="HTML")
- await callback.answer()
-
-
-# Legacy manage_promo_codes_handler - keeping for compatibility
-@router.callback_query(F.data == "admin_action:manage_promos")
-async def manage_promo_codes_handler(callback: types.CallbackQuery,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- # Redirect to new unified handler
- await promo_management_handler(callback, i18n_data, settings, session)
-
-
-@router.callback_query(F.data.startswith("promo_edit:"))
-async def promo_edit_select_handler(callback: types.CallbackQuery, state: FSMContext,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- promo_id = int(callback.data.split(":")[1])
- promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not promo or not callback.message:
- await callback.answer("Error", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- await state.update_data(
- edit_promo_id=promo_id,
- original_code=promo.code,
- original_bonus_days=promo.bonus_days,
- original_max_activations=promo.max_activations,
- original_valid_until=promo.valid_until
- )
-
- # Step 1: Edit code
- prompt_text = _(
- "admin_promo_edit_step1_code",
- default="βοΈ Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ’Π΅ΠΊΡΡΠΈΠΉ ΠΊΠΎΠ΄: {current_code}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ Π½ΠΎΠ²ΡΠΉ ΠΊΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ², ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ) ΠΈΠ»ΠΈ ΠΎΡΠΏΡΠ°Π²ΡΡΠ΅ ΡΠ΅ΠΊΡΡΠΈΠΉ Π΄Π»Ρ ΡΠΎΡ
ΡΠ°Π½Π΅Π½ΠΈΡ:",
- current_code=promo.code
- )
-
- await state.set_state(AdminStates.waiting_for_promo_edit_code)
- await callback.message.edit_text(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- await callback.answer()
-
-
-# Step 1: Process edited code
-@router.message(AdminStates.waiting_for_promo_edit_code, F.text)
-async def process_promo_edit_code_handler(message: types.Message, state: FSMContext,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
- original_code = data.get("original_code")
-
- try:
- code_str = message.text.strip().upper()
- if not (3 <= len(code_str) <= 30 and code_str.isalnum()):
- await message.answer(_(
- "admin_promo_invalid_code_format",
- default="β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)"
- ))
- return
-
- # Check if code already exists (except current promo)
- if code_str != original_code:
- existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str)
- if existing_promo:
- await message.answer(_(
- "admin_promo_code_already_exists",
- default="β ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Ρ ΡΠ°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡΠΆΠ΅ ΡΡΡΠ΅ΡΡΠ²ΡΠ΅Ρ"
- ))
- return
-
- await state.update_data(new_code=code_str)
-
- # Step 2: Ask for bonus days
- prompt_text = _(
- "admin_promo_edit_step2_bonus_days",
- default="βοΈ Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ΄: {code}\nΠ’Π΅ΠΊΡΡΠΈΠ΅ Π΄Π½ΠΈ: {current_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
- code=code_str,
- current_days=data.get("original_bonus_days")
- )
-
- await message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_promo_edit_bonus_days)
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_code_format",
- default="β ΠΠΎΠ΄ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅ΡΠΆΠ°ΡΡ 3-30 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ² (ΡΠΎΠ»ΡΠΊΠΎ Π±ΡΠΊΠ²Ρ ΠΈ ΡΠΈΡΡΡ)"
- ))
-
-
-# Step 2: Process edited bonus days
-@router.message(AdminStates.waiting_for_promo_edit_bonus_days, F.text)
-async def process_promo_edit_bonus_days_handler(message: types.Message, state: FSMContext,
- i18n_data: dict, settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
-
- try:
- bonus_days = int(message.text.strip())
- if not (1 <= bonus_days <= 365):
- await message.answer(_(
- "admin_promo_invalid_bonus_days",
- default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
- return
-
- await state.update_data(new_bonus_days=bonus_days)
-
- # Step 3: Ask for max activations
- prompt_text = _(
- "admin_promo_edit_step3_max_activations",
- default="βοΈ Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ΄: {code}\nΠΠ½ΠΈ: {days}\nΠ’Π΅ΠΊΡΡΠΈΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {current_max}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ (1-10000):",
- code=data.get("new_code"),
- days=bonus_days,
- current_max=data.get("original_max_activations")
- )
-
- await message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_promo_edit_max_activations)
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_bonus_days",
- default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
-
-
-# Step 3: Process edited max activations
-@router.message(AdminStates.waiting_for_promo_edit_max_activations, F.text)
-async def process_promo_edit_max_activations_handler(message: types.Message, state: FSMContext,
- i18n_data: dict, settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
-
- try:
- max_activations = int(message.text.strip())
- if not (1 <= max_activations <= 10000):
- await message.answer(_(
- "admin_promo_invalid_max_activations",
- default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
- ))
- return
-
- await state.update_data(new_max_activations=max_activations)
-
- # Step 4: Ask for validity days (with buttons)
- current_valid_until = data.get("original_valid_until")
- current_validity_str = _("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
- if current_valid_until:
- current_validity_str = current_valid_until.strftime('%Y-%m-%d')
-
- keyboard = InlineKeyboardBuilder()
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_promo_edit_unlimited_validity", default="βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ"),
- callback_data="promo_edit_unlimited_validity"
- )
- )
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_promo_edit_set_validity", default="β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ"),
- callback_data="promo_edit_set_validity"
- )
- )
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_panel_back_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
- callback_data="admin_panel_back"
- )
- )
-
- prompt_text = _(
- "admin_promo_edit_step4_validity",
- default="βοΈ Π Π΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ΄: {code}\nΠΠ½ΠΈ: {days}\nΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nΠ’Π΅ΠΊΡΡΠΈΠΉ ΡΡΠΎΠΊ: {current_validity}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°:",
- code=data.get("new_code"),
- days=data.get("new_bonus_days"),
- max_act=max_activations,
- current_validity=current_validity_str
- )
-
- await message.answer(
- prompt_text,
- reply_markup=keyboard.as_markup(),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_promo_edit_validity_days)
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_max_activations",
- default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
- ))
-
-
-# Handle edit validity choice buttons
-@router.callback_query(F.data == "promo_edit_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_edit_validity_days))
-async def process_promo_edit_unlimited_validity(callback: types.CallbackQuery,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await callback.answer("Language service error.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
- promo_id = data.get("edit_promo_id")
-
- # Update promo code with unlimited validity
- update_data = {
- "code": data.get("new_code"),
- "bonus_days": data.get("new_bonus_days"),
- "max_activations": data.get("new_max_activations"),
- "valid_until": None, # Unlimited
- }
-
- # Show updating message
- try:
- await callback.message.edit_text(
- _("admin_promo_creating", default="β³ ΠΠ±Π½ΠΎΠ²Π»ΡΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄..."),
- )
- except:
- await callback.message.answer(
- _("admin_promo_creating", default="β³ ΠΠ±Π½ΠΎΠ²Π»ΡΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄..."),
- )
- await callback.answer()
-
- updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
- if updated:
- await session.commit()
-
- success_text = _(
- "admin_promo_updated_success",
- default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}",
- code=data.get("new_code"),
- days=data.get("new_bonus_days"),
- max_act=data.get("new_max_activations"),
- validity=_("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
- )
-
- await callback.message.answer(
- success_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- else:
- await session.rollback()
- await callback.message.answer(_(
- "admin_promo_update_failed",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"
- ))
-
- await state.clear()
-
-@router.callback_query(F.data == "promo_edit_set_validity", StateFilter(AdminStates.waiting_for_promo_edit_validity_days))
-async def process_promo_edit_set_validity(callback: types.CallbackQuery,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await callback.answer("Language service error.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- prompt_text = _(
- "admin_promo_edit_enter_validity_days",
- default="β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):"
- )
-
- try:
- await callback.message.edit_text(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
- except Exception as e:
- await callback.message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
- await callback.answer()
-
-# Step 4: Process edited validity and finalize
-@router.message(AdminStates.waiting_for_promo_edit_validity_days, F.text)
-async def process_promo_edit_validity_handler(message: types.Message, state: FSMContext,
- i18n_data: dict, settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
- promo_id = data.get("edit_promo_id")
-
- try:
- validity_days = int(message.text.strip())
- if not (1 <= validity_days <= 365):
- await message.answer(_(
- "admin_promo_invalid_validity_days",
- default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
- return
-
- # Calculate validity date
- valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
- valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
-
- # Update promo code
- update_data = {
- "code": data.get("new_code"),
- "bonus_days": data.get("new_bonus_days"),
- "max_activations": data.get("new_max_activations"),
- "valid_until": valid_until_date,
- }
-
- updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
- if updated:
- await session.commit()
-
- success_text = _(
- "admin_promo_updated_success",
- default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½!\n\nπ ΠΠΎΠ΄: {code}\nπ ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {days}\nπ’ ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}\nβ° ΠΠ΅ΠΉΡΡΠ²ΡΠ΅Ρ Π΄ΠΎ: {validity}",
- code=data.get("new_code"),
- days=data.get("new_bonus_days"),
- max_act=data.get("new_max_activations"),
- validity=valid_until_str_display
- )
-
- await message.answer(
- success_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- else:
- await session.rollback()
- await message.answer(_(
- "admin_promo_update_failed",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"
- ))
-
- await state.clear()
-
- except ValueError:
- await message.answer(_(
- "admin_promo_invalid_validity_days",
- default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
-
-
-@router.callback_query(F.data.startswith("promo_toggle:"))
-async def promo_toggle_handler(callback: types.CallbackQuery, i18n_data: dict,
- settings: Settings, session: AsyncSession):
- """Toggle promo code active status"""
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- promo_id = int(callback.data.split(":")[1])
- promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
- if not promo:
- await callback.answer(_("admin_promo_not_found", default="ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
- return
-
- # Toggle status
- new_status = not promo.is_active
- update_data = {"is_active": new_status}
-
- updated = await promo_code_dal.update_promo_code(session, promo_id, update_data)
- if updated:
- await session.commit()
-
- status_text = _("admin_promo_status_activated", default="Π°ΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°Π½") if new_status else _("admin_promo_status_deactivated", default="Π΄Π΅Π°ΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°Π½")
- success_text = _(
- "admin_promo_toggle_success",
- default="β
ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ {code} {status}",
- code=promo.code,
- status=status_text
- )
-
- await callback.answer(success_text, show_alert=True)
-
- # Refresh the detail view
- await promo_detail_handler(callback, i18n_data, settings, session)
- else:
- await session.rollback()
- await callback.answer(_(
- "admin_promo_toggle_failed",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΡΡΠ°ΡΡΡΠ° ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°"
- ), show_alert=True)
-
-
-@router.callback_query(F.data.startswith("promo_activations:"))
-async def promo_activations_handler(callback: types.CallbackQuery, i18n_data: dict,
- settings: Settings, session: AsyncSession):
- """Show promo code activations"""
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- promo_id = int(callback.data.split(":")[1])
- promo = await promo_code_dal.get_promo_code_by_id(session, promo_id)
- if not promo:
- await callback.answer(_("admin_promo_not_found", default="ΠΡΠΎΠΌΠΎΠΊΠΎΠ΄ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½"), show_alert=True)
- return
-
- # Get activations for this promo code
- activations = await promo_code_dal.get_promo_activations_by_code_id(session, promo_id)
-
- if not activations:
- text = _(
- "admin_promo_no_activations",
- default="π ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {code}\n\nβ ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½ΠΎ",
- code=promo.code
- )
- else:
- text_parts = [_(
- "admin_promo_activations_header",
- default="π ΠΠΊΡΠΈΠ²Π°ΡΠΈΠΈ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {code}\n\n",
- code=promo.code
- )]
-
- for activation in activations:
- activation_date = activation.activated_at.strftime('%Y-%m-%d %H:%M') if activation.activated_at else "N/A"
- text_parts.append(_(
- "admin_promo_activation_item",
- default="π€ User ID: {user_id}\nπ
ΠΠ°ΡΠ°: {date}\n",
- user_id=activation.user_id,
- date=activation_date
- ))
-
- text = "".join(text_parts)
-
- # Build keyboard with back button
- kb = InlineKeyboardBuilder()
- kb.row(
- InlineKeyboardButton(
- text=_("admin_promo_back_to_detail_button", default="β¬
οΈ Π ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ"),
- callback_data=f"promo_detail:{promo_id}")
- )
-
- await callback.message.edit_text(
- text,
- reply_markup=kb.as_markup(),
- parse_mode="HTML")
- await callback.answer()
-
-
-@router.callback_query(F.data.startswith("promo_delete:"))
-async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict,
- settings: Settings, session: AsyncSession):
- promo_id = int(callback.data.split(":")[1])
- promo = await promo_code_dal.delete_promo_code(session, promo_id)
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- if promo:
- await session.commit()
- await callback.message.edit_text(
- _("admin_promo_deleted_success", code=promo.code),
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- else:
- await session.rollback()
- await callback.message.edit_text(
- _("admin_promo_not_found"),
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
- await callback.answer()
-
-
-@router.callback_query(
- F.data == "admin_action:main",
- StateFilter(
- AdminStates.waiting_for_promo_details,
- AdminStates.waiting_for_promo_code,
- AdminStates.waiting_for_promo_bonus_days,
- AdminStates.waiting_for_promo_max_activations,
- AdminStates.waiting_for_promo_validity_days,
- AdminStates.waiting_for_promo_edit_details,
- AdminStates.waiting_for_bulk_promo_quantity,
- AdminStates.waiting_for_bulk_promo_bonus_days,
- AdminStates.waiting_for_bulk_promo_max_activations,
- AdminStates.waiting_for_bulk_promo_validity_days,
- ),
-)
-async def cancel_promo_creation_state_to_menu(callback: types.CallbackQuery,
- state: FSMContext,
- settings: Settings,
- i18n_data: dict,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error cancelling.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- await callback.message.edit_text(_("admin_action_cancelled_default"),
- reply_markup=get_admin_panel_keyboard(
- i18n, current_lang, settings))
- except Exception:
- await callback.message.answer(_("admin_action_cancelled_default"),
- reply_markup=get_admin_panel_keyboard(
- i18n, current_lang, settings))
-
- await callback.answer(_("admin_action_cancelled_default_alert"))
- await state.clear()
-
-
-async def create_bulk_promo_prompt_handler(callback: types.CallbackQuery,
- state: FSMContext, i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n or not callback.message:
- await callback.answer("Error preparing bulk promo creation.",
- show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- # Step 1: Ask for quantity
- prompt_text = _(
- "admin_bulk_promo_step1_quantity",
- default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π΄Π»Ρ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ (1-1000):"
- )
-
- try:
- await callback.message.edit_text(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- except Exception as e:
- logging.warning(
- f"Could not edit message for bulk promo prompt: {e}. Sending new.")
- await callback.message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML")
- await callback.answer()
- await state.set_state(AdminStates.waiting_for_bulk_promo_quantity)
-
-
-def generate_unique_promo_code(length: int = 8) -> str:
- """Generate a unique promotional code"""
- characters = string.ascii_uppercase + string.digits
- # Exclude confusing characters
- characters = characters.replace('0', '').replace('O', '').replace('1', '').replace('I', '').replace('L', '')
- return ''.join(random.choice(characters) for _ in range(length))
-
-
-# Step 1: Process quantity
-@router.message(AdminStates.waiting_for_bulk_promo_quantity, F.text)
-async def process_bulk_promo_quantity_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- quantity = int(message.text.strip())
- if quantity <= 0 or quantity > 1000:
- await message.answer(_(
- "admin_bulk_promo_invalid_quantity",
- default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 1000"
- ))
- return
-
- await state.update_data(quantity=quantity)
-
- # Step 2: Ask for bonus days
- prompt_text = _(
- "admin_bulk_promo_step2_bonus_days",
- default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π±ΠΎΠ½ΡΡΠ½ΡΡ
Π΄Π½Π΅ΠΉ (1-365):",
- quantity=quantity
- )
-
- await message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_bulk_promo_bonus_days)
-
- except ValueError:
- await message.answer(_(
- "admin_bulk_promo_invalid_quantity",
- default="β ΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 1000"
- ))
-
-# Step 2: Process bonus days
-@router.message(AdminStates.waiting_for_bulk_promo_bonus_days, F.text)
-async def process_bulk_promo_bonus_days_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- bonus_days = int(message.text.strip())
- if bonus_days <= 0 or bonus_days > 365:
- await message.answer(_(
- "admin_bulk_promo_invalid_bonus_days",
- default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
- return
-
- data = await state.get_data()
- await state.update_data(bonus_days=bonus_days)
-
- # Step 3: Ask for max activations
- prompt_text = _(
- "admin_bulk_promo_step3_max_activations",
- default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\n\nΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΌΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΠΎΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΉ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-10000):",
- quantity=data['quantity'],
- bonus_days=bonus_days
- )
-
- await message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_bulk_promo_max_activations)
-
- except ValueError:
- await message.answer(_(
- "admin_bulk_promo_invalid_bonus_days",
- default="β ΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
-
-# Step 3: Process max activations
-@router.message(AdminStates.waiting_for_bulk_promo_max_activations, F.text)
-async def process_bulk_promo_max_activations_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- max_activations = int(message.text.strip())
- if max_activations <= 0 or max_activations > 10000:
- await message.answer(_(
- "admin_bulk_promo_invalid_max_activations",
- default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
- ))
- return
-
- data = await state.get_data()
- await state.update_data(max_activations=max_activations)
-
- # Step 4: Ask for validity days (optional)
- keyboard = InlineKeyboardBuilder()
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_bulk_promo_unlimited_validity", default="βΎοΈ ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ"),
- callback_data="bulk_promo_unlimited_validity"
- )
- )
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_bulk_promo_set_validity", default="β° Π£ΡΡΠ°Π½ΠΎΠ²ΠΈΡΡ ΡΡΠΎΠΊ"),
- callback_data="bulk_promo_set_validity"
- )
- )
- keyboard.row(
- InlineKeyboardButton(
- text=_("admin_panel_back_button", default="β¬
οΈ ΠΠ°Π·Π°Π΄"),
- callback_data="admin_panel_back"
- )
- )
-
- prompt_text = _(
- "admin_bulk_promo_step4_validity",
- default="π¦ ΠΠ°ΡΡΠΎΠ²ΠΎΠ΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘ΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ\n\nΠΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ: {quantity}\nΠΠΎΠ½ΡΡΠ½ΡΠ΅ Π΄Π½ΠΈ: {bonus_days}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_activations}\n\nΠΡΠ±Π΅ΡΠΈΡΠ΅ ΡΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²:",
- quantity=data['quantity'],
- bonus_days=data['bonus_days'],
- max_activations=max_activations
- )
-
- await message.answer(
- prompt_text,
- reply_markup=keyboard.as_markup(),
- parse_mode="HTML"
- )
- await state.set_state(AdminStates.waiting_for_bulk_promo_validity_days)
-
- except ValueError:
- await message.answer(_(
- "admin_bulk_promo_invalid_max_activations",
- default="β ΠΠ°ΠΊΡΠΈΠΌΠ°Π»ΡΠ½ΡΠ΅ Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 10000"
- ))
-
-# Handle validity choice buttons
-@router.callback_query(F.data == "bulk_promo_unlimited_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
-async def process_bulk_promo_unlimited_validity(callback: types.CallbackQuery,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- await state.update_data(validity_days=None)
- await create_bulk_promo_codes_final(callback, state, i18n_data, settings, session)
-
-@router.callback_query(F.data == "bulk_promo_set_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days))
-async def process_bulk_promo_set_validity(callback: types.CallbackQuery,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await callback.answer("Language service error.", show_alert=True)
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- prompt_text = _(
- "admin_bulk_promo_enter_validity_days",
- default="β° ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ Π΄Π½Π΅ΠΉ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² (1-365):"
- )
-
- try:
- await callback.message.edit_text(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
- except Exception as e:
- await callback.message.answer(
- prompt_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
- await callback.answer()
-
-# Step 4: Process validity days (when user chooses to set)
-@router.message(AdminStates.waiting_for_bulk_promo_validity_days, F.text)
-async def process_bulk_promo_validity_days_handler(message: types.Message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- await message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- try:
- validity_days = int(message.text.strip())
- if validity_days <= 0 or validity_days > 365:
- await message.answer(_(
- "admin_bulk_promo_invalid_validity_days",
- default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
- return
-
- await state.update_data(validity_days=validity_days)
-
- # Create a fake callback for consistency
- fake_callback = types.CallbackQuery(
- id="fake",
- from_user=message.from_user,
- chat_instance="fake",
- message=message,
- data="fake"
- )
-
- await create_bulk_promo_codes_final(fake_callback, state, i18n_data, settings, session)
-
- except ValueError:
- await message.answer(_(
- "admin_bulk_promo_invalid_validity_days",
- default="β ΠΠ½ΠΈ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ Π΄ΠΎΠ»ΠΆΠ½Ρ Π±ΡΡΡ ΠΎΡ 1 Π΄ΠΎ 365"
- ))
-
-async def create_bulk_promo_codes_final(callback_or_message,
- state: FSMContext,
- i18n_data: dict,
- settings: Settings,
- session: AsyncSession):
- """Final step to create all promo codes"""
- current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
- i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
- if not i18n:
- if hasattr(callback_or_message, 'answer'):
- await callback_or_message.answer("Language service error.", show_alert=True)
- else:
- await callback_or_message.reply("Language service error.")
- return
- _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
-
- data = await state.get_data()
- quantity = data['quantity']
- bonus_days = data['bonus_days']
- max_activations = data['max_activations']
- validity_days = data.get('validity_days')
-
- # Create validity date
- valid_until_date: Optional[datetime] = None
- valid_until_str_display = _("admin_promo_valid_indefinitely", default="ΠΠ΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎ")
-
- if validity_days:
- valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days)
- valid_until_str_display = valid_until_date.strftime('%Y-%m-%d')
-
- # Show creating message
- creating_text = _(
- "admin_bulk_promo_creating",
- default="β³ Π‘ΠΎΠ·Π΄Π°Ρ {quantity} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...",
- quantity=quantity
- )
-
- if hasattr(callback_or_message, 'message'):
- try:
- await callback_or_message.message.edit_text(creating_text)
- except:
- await callback_or_message.message.answer(creating_text)
- if hasattr(callback_or_message, 'answer'):
- await callback_or_message.answer()
- else:
- await callback_or_message.answer(creating_text)
-
- # Generate and create promo codes
- created_codes = []
- try:
- for i in range(quantity):
- # Generate unique code
- promo_code = generate_unique_promo_code()
-
- # Ensure uniqueness
- existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
- retries = 0
- while existing_promo and retries < 10:
- promo_code = generate_unique_promo_code()
- existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code)
- retries += 1
-
- if retries >= 10:
- raise Exception("ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΡΠΎΠ·Π΄Π°ΡΡ ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΡΠΉ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄")
-
- # Create promo code
- promo_data = {
- "code": promo_code,
- "bonus_days": bonus_days,
- "max_activations": max_activations,
- "current_activations": 0,
- "is_active": True,
- "valid_until": valid_until_date,
- "created_by_admin_id": callback_or_message.from_user.id if hasattr(callback_or_message, 'from_user') and callback_or_message.from_user else 0
- }
-
- await promo_code_dal.create_promo_code(session, promo_data)
- created_codes.append(promo_code)
-
- await session.commit()
-
- # Send success message
- success_text = _(
- "admin_bulk_promo_created_success",
- default="β
Π‘ΠΎΠ·Π΄Π°Π½ΠΎ {count} ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π½Π° {days} Π΄Π½Π΅ΠΉ!\n\nΠΠ΅ΠΉΡΡΠ²ΡΡΡ Π΄ΠΎ: {validity}\nΠΠ°ΠΊΡ. Π°ΠΊΡΠΈΠ²Π°ΡΠΈΠΈ: {max_act}",
- count=quantity,
- days=bonus_days,
- validity=valid_until_str_display,
- max_act=max_activations
- )
-
- if hasattr(callback_or_message, 'message'):
- target_message = callback_or_message.message
- else:
- target_message = callback_or_message
-
- await target_message.answer(
- success_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
- parse_mode="HTML"
- )
-
- # Always send codes as file
- codes_file_content = "\n".join(created_codes)
- codes_file = types.BufferedInputFile(
- codes_file_content.encode('utf-8'),
- filename=f"bulk_promo_codes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
- )
-
- await target_message.answer_document(
- codes_file,
- caption=_(
- "admin_bulk_promo_codes_file",
- default="π ΠΡΠ΅ ΡΠΎΠ·Π΄Π°Π½Π½ΡΠ΅ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄Ρ Π² ΡΠ°ΠΉΠ»Π΅ ({count} ΡΡ.)",
- count=quantity
- )
- )
-
- except Exception as e:
- logging.error(f"Error creating bulk promo codes: {e}")
- await session.rollback()
- error_text = _(
- "admin_bulk_promo_creation_failed",
- default="β ΠΡΠΈΠ±ΠΊΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΠΏΡΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²: {error}",
- error=str(e)
- )
-
- if hasattr(callback_or_message, 'message'):
- target_message = callback_or_message.message
- else:
- target_message = callback_or_message
-
- await target_message.answer(
- error_text,
- reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
- )
-
- await state.clear()
-
-# Old bulk promo handler removed - now using step-by-step process
-
-
-
diff --git a/db/dal/payment_dal.py b/db/dal/payment_dal.py
index dce3514..6f87e82 100644
--- a/db/dal/payment_dal.py
+++ b/db/dal/payment_dal.py
@@ -38,14 +38,6 @@ async def create_payment_record(session: AsyncSession,
return new_payment
-async def get_payment_by_yookassa_id(
- session: AsyncSession, yookassa_payment_id: str) -> Optional[Payment]:
- stmt = select(Payment).where(
- Payment.yookassa_payment_id == yookassa_payment_id)
- result = await session.execute(stmt)
- return result.scalar_one_or_none()
-
-
async def get_payment_by_provider_payment_id(
session: AsyncSession, provider_payment_id: str) -> Optional[Payment]:
"""Fetch a payment by provider-specific identifier."""
@@ -64,15 +56,6 @@ async def get_payment_by_db_id(session: AsyncSession,
return result.scalar_one_or_none()
-async def get_payment_by_db_id_with_promo(
- session: AsyncSession, payment_db_id: int) -> Optional[Payment]:
-
- stmt = select(Payment).where(Payment.payment_id == payment_db_id).options(
- selectinload(Payment.promo_code_used))
- result = await session.execute(stmt)
- return result.scalar_one_or_none()
-
-
async def update_payment_status_by_db_id(
session: AsyncSession,
payment_db_id: int,
@@ -96,38 +79,6 @@ async def update_payment_status_by_db_id(
return payment
-async def user_has_successful_payment_for_provider(
- session: AsyncSession, user_id: int, provider: str) -> bool:
- """Check if a user has at least one successful payment for the provider."""
-
- stmt = (select(Payment.payment_id)
- .where(Payment.user_id == user_id,
- Payment.provider == provider,
- Payment.status == 'succeeded')
- .limit(1))
- result = await session.execute(stmt)
- return result.scalar_one_or_none() is not None
-
-
-async def update_payment_status_by_yk_id(session: AsyncSession,
- yookassa_payment_id: str,
- new_status: str) -> Optional[Payment]:
- payment = await get_payment_by_yookassa_id(session, yookassa_payment_id)
- if payment:
- payment.status = new_status
- payment.updated_at = func.now()
- await session.flush()
- await session.refresh(payment)
- logging.info(
- f"Payment record with YK ID {yookassa_payment_id} status updated to {new_status}."
- )
- else:
- logging.warning(
- f"Payment record with YK ID {yookassa_payment_id} not found for status update."
- )
- return payment
-
-
async def get_recent_payment_logs_with_user(session: AsyncSession,
limit: int = 20,
offset: int = 0) -> List[Payment]:
diff --git a/db/dal/subscription_dal.py b/db/dal/subscription_dal.py
index 66d4cf4..f67a2eb 100644
--- a/db/dal/subscription_dal.py
+++ b/db/dal/subscription_dal.py
@@ -31,29 +31,6 @@ async def get_subscription_by_panel_subscription_uuid(
return result.scalar_one_or_none()
-async def create_subscription(session: AsyncSession,
- sub_data: Dict[str, Any]) -> Subscription:
- from .user_dal import get_user_by_id
-
- if "user_id" not in sub_data or sub_data["user_id"] is None:
- raise ValueError(
- "user_id is required to create a subscription directly.")
- user = await get_user_by_id(session, sub_data["user_id"])
- if not user:
- raise ValueError(
- f"User with id {sub_data['user_id']} not found for creating subscription."
- )
-
- new_sub = Subscription(**sub_data)
- session.add(new_sub)
- await session.flush()
- await session.refresh(new_sub)
- logging.info(
- f"Subscription {new_sub.subscription_id} created for user {new_sub.user_id}"
- )
- return new_sub
-
-
async def update_subscription(
session: AsyncSession, subscription_id: int,
update_data: Dict[str, Any]) -> Optional[Subscription]:
@@ -205,17 +182,6 @@ async def update_subscription_notification_time(
{"last_notification_sent": notification_time})
-async def get_user_active_subscription_end_date_str(
- session: AsyncSession, user_id: int) -> Optional[str]:
- stmt = (select(Subscription.end_date).where(
- Subscription.user_id == user_id, Subscription.is_active == True,
- Subscription.end_date > datetime.now(timezone.utc)).order_by(
- Subscription.end_date.desc()).limit(1))
- result = await session.execute(stmt)
- end_date_obj = result.scalar_one_or_none()
- return end_date_obj.strftime('%Y-%m-%d') if end_date_obj else None
-
-
async def find_subscription_for_notification_update(
session: AsyncSession, user_id: int,
subscription_end_date_to_match: datetime) -> Optional[Subscription]:
@@ -234,34 +200,4 @@ async def find_subscription_for_notification_update(
return result.scalar_one_or_none()
-async def set_skip_notifications_for_provider(
- session: AsyncSession, user_id: int, provider: str,
- skip: bool) -> int:
- stmt = (update(Subscription).where(
- Subscription.user_id == user_id,
- Subscription.is_active == True,
- Subscription.provider == provider).values(skip_notifications=skip))
- result = await session.execute(stmt)
- return result.rowcount
-
-async def get_active_subscriptions_for_autorenew(
- session: AsyncSession, provider: str,
- days_threshold: int = 1,
- require_skip_flag: bool = True) -> List[Subscription]:
- """Fetch active subscriptions nearing expiration for auto-renew logic."""
-
- now_utc = datetime.now(timezone.utc)
- threshold_date = now_utc + timedelta(days=days_threshold)
-
- conditions = [
- Subscription.provider == provider,
- Subscription.is_active == True,
- Subscription.end_date <= threshold_date,
- ]
- if require_skip_flag:
- conditions.append(Subscription.skip_notifications == True)
-
- stmt = select(Subscription).where(*conditions)
- result = await session.execute(stmt)
- return result.scalars().all()
diff --git a/db/dal/user_dal.py b/db/dal/user_dal.py
index b661fff..af8f58c 100644
--- a/db/dal/user_dal.py
+++ b/db/dal/user_dal.py
@@ -81,18 +81,6 @@ async def update_user_language(
return result.rowcount > 0
-async def set_user_ban_status(
- session: AsyncSession, user_id: int, is_banned: bool
-) -> bool:
- user = await get_user_by_id(session, user_id)
- if user:
- user.is_banned = is_banned
- await session.flush()
- await session.refresh(user)
- return True
- return False
-
-
async def get_banned_users(session: AsyncSession) -> List[User]:
"""Get all banned users"""
stmt = (
@@ -104,58 +92,12 @@ async def get_banned_users(session: AsyncSession) -> List[User]:
return result.scalars().all()
-async def get_banned_users_paginated(
- session: AsyncSession, limit: int, offset: int
-) -> Tuple[List[User], int]:
- stmt_users = (
- select(User)
- .where(User.is_banned == True)
- .order_by(User.registration_date.desc())
- .limit(limit)
- .offset(offset)
- )
- result_users = await session.execute(stmt_users)
- users_list = result_users.scalars().all()
-
- stmt_count = select(func.count()).select_from(User).where(User.is_banned == True)
- result_count = await session.execute(stmt_count)
- total_banned = result_count.scalar_one()
-
- return users_list, total_banned
-
-
async def get_all_active_user_ids_for_broadcast(session: AsyncSession) -> List[int]:
stmt = select(User.user_id).where(User.is_banned == False)
result = await session.execute(stmt)
return result.scalars().all()
-async def get_user_count_stats_dal(session: AsyncSession) -> Dict[str, int]:
- total_users_stmt = select(func.count(User.user_id)).select_from(User)
- banned_users_stmt = (
- select(func.count(User.user_id)).select_from(User).where(User.is_banned == True)
- )
-
- active_subs_stmt = (
- select(func.count(func.distinct(Subscription.user_id)))
- .join(User, Subscription.user_id == User.user_id)
- .where(Subscription.is_active == True)
- .where(Subscription.end_date > datetime.now())
- )
-
- total_users = (await session.execute(total_users_stmt)).scalar_one_or_none() or 0
- banned_users = (await session.execute(banned_users_stmt)).scalar_one_or_none() or 0
- active_subs_users = (
- await session.execute(active_subs_stmt)
- ).scalar_one_or_none() or 0
-
- return {
- "total_users": total_users,
- "banned_users": banned_users,
- "users_with_active_subscriptions": active_subs_users,
- }
-
-
async def get_all_users_with_panel_uuid(session: AsyncSession) -> List[User]:
stmt = select(User).where(User.panel_user_uuid.is_not(None))
result = await session.execute(stmt)