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🎁 БонусныС Π΄Π½ΠΈ: {days}\nπŸ”’ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_act}\n⏰ ДСйствуСт Π΄ΠΎ: {validity}", code=promo_code, days=bonus_days, max_act=max_activations, validity=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