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() # Log successful creation logging.info(f"Promo code '{data['promo_code']}' created with ID {created_promo.promo_code_id}") # Success message valid_until_str = _("admin_promo_unlimited", default="Π‘Π΅Π· ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠΉ") if not data.get("validity_days") else f"{data['validity_days']} Π΄Π½Π΅ΠΉ" success_text = _( "admin_promo_created_success", default="βœ… ΠŸΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ создан!\n\n" "🎟 Код: {code}\n" "🎁 БонусныС Π΄Π½ΠΈ: {bonus_days}\n" "πŸ“Š Макс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΉ: {max_activations}\n" "⏰ Π‘Ρ€ΠΎΠΊ дСйствия: {valid_until_str}", code=data["promo_code"], bonus_days=data["bonus_days"], max_activations=data["max_activations"], valid_until_str=valid_until_str ) 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()