From 9cf5a93625a0dbaa77b2bff0c7f9770eeab9cc0e Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Tue, 5 Aug 2025 14:55:55 +0300 Subject: [PATCH] Refactor promo code handling in admin panel and clean up unused methods - Replaced the promo_codes module with a new promo management structure, enhancing organization and clarity in the admin panel. - Updated admin panel handlers to utilize the new promo management methods, improving the flow of promo code creation and management. - Removed deprecated methods from payment and subscription data access layers, streamlining the codebase and improving maintainability. - Ensured localization files are updated to reflect changes in promo management messaging, enhancing user experience. --- bot/handlers/admin/__init__.py | 4 +- bot/handlers/admin/common.py | 14 +- bot/handlers/admin/promo/__init__.py | 13 + bot/handlers/admin/promo/bulk.py | 511 ++++++++ bot/handlers/admin/promo/create.py | 432 +++++++ bot/handlers/admin/promo/manage.py | 422 +++++++ bot/handlers/admin/promo_codes.py | 1611 -------------------------- db/dal/payment_dal.py | 49 - db/dal/subscription_dal.py | 64 - db/dal/user_dal.py | 58 - 10 files changed, 1388 insertions(+), 1790 deletions(-) create mode 100644 bot/handlers/admin/promo/__init__.py create mode 100644 bot/handlers/admin/promo/bulk.py create mode 100644 bot/handlers/admin/promo/create.py create mode 100644 bot/handlers/admin/promo/manage.py delete mode 100644 bot/handlers/admin/promo_codes.py 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)