diff --git a/app/handlers/admin/promo_offers.py b/app/handlers/admin/promo_offers.py index a545d665..1f308312 100644 --- a/app/handlers/admin/promo_offers.py +++ b/app/handlers/admin/promo_offers.py @@ -11,11 +11,6 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMar from sqlalchemy.ext.asyncio import AsyncSession from app.database.crud.discount_offer import upsert_discount_offer -from app.database.crud.server_squad import ( - get_all_server_squads, - get_server_squad_by_id, - get_server_squad_by_uuid, -) from app.database.crud.promo_offer_template import ( ensure_default_templates, get_promo_offer_template_by_id, @@ -31,9 +26,6 @@ from app.utils.decorators import admin_required, error_handler logger = logging.getLogger(__name__) -SQUADS_PAGE_LIMIT = 10 - - OFFER_TYPE_CONFIG = { "test_access": { "icon": "🧪", @@ -288,154 +280,12 @@ async def prompt_edit_duration(callback: CallbackQuery, db_user: User, db: Async @error_handler async def prompt_edit_squads(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): template_id = int(callback.data.split("_")[-1]) - template = await get_promo_offer_template_by_id(db, template_id) - if not template: - await callback.answer("❌ Предложение не найдено", show_alert=True) - return - - await state.update_data( - selected_promo_offer=template.id, - promo_edit_message_id=callback.message.message_id, - promo_edit_chat_id=callback.message.chat.id, - ) - - await _render_squad_selection(callback, template, db, db_user.language) - await callback.answer() - - -async def _render_squad_selection( - callback: CallbackQuery, - template: PromoOfferTemplate, - db: AsyncSession, - language: str, - page: int = 1, -): - texts = get_texts(language) - - squads, total_count = await get_all_server_squads( - db, - available_only=False, - page=page, - limit=SQUADS_PAGE_LIMIT, - ) - - if total_count == 0: - await callback.message.edit_text( - texts.t("ADMIN_PROMO_OFFER_NO_SQUADS_AVAILABLE", "❌ Доступные серверы не найдены."), - reply_markup=InlineKeyboardMarkup( - inline_keyboard=[[InlineKeyboardButton(text=texts.BACK, callback_data=f"promo_offer_squad_back_{template.id}")]] - ), - ) - return - - selected_uuid = None - if template.test_squad_uuids: - selected_uuid = str(template.test_squad_uuids[0]) - - selected_server_name = None - if selected_uuid: - selected_server = next((srv for srv in squads if srv.squad_uuid == selected_uuid), None) - if not selected_server: - selected_server = await get_server_squad_by_uuid(db, selected_uuid) - if selected_server: - selected_server_name = selected_server.display_name - - header = texts.t("ADMIN_PROMO_OFFER_SELECT_SQUAD_TITLE", "🌍 Выберите сквад") - if selected_server_name: - current = texts.t( - "ADMIN_PROMO_OFFER_SELECTED_SQUAD", - "Текущий сквад: {name}", - ).format(name=selected_server_name) - elif selected_uuid: - current = texts.t( - "ADMIN_PROMO_OFFER_SELECTED_SQUAD_UUID", - "Текущий сквад: {uuid}", - ).format(uuid=selected_uuid) - else: - current = texts.t( - "ADMIN_PROMO_OFFER_SELECTED_SQUAD_EMPTY", - "Текущий сквад: не выбран", - ) - - hint = texts.t( - "ADMIN_PROMO_OFFER_SELECT_SQUAD_HINT", - "Выберите сервер для тестового доступа из списка ниже.", - ) - - total_pages = (total_count + SQUADS_PAGE_LIMIT - 1) // SQUADS_PAGE_LIMIT or 1 - page = max(1, min(page, total_pages)) - - lines = [header, "", current, "", hint] - if total_pages > 1: - lines.append( - texts.t( - "ADMIN_PROMO_OFFER_SELECT_SQUAD_PAGE", - "Страница {page}/{total}", - ).format(page=page, total=total_pages) - ) - - text = "\n".join(lines) - - keyboard_rows: List[List[InlineKeyboardButton]] = [] - for server in squads: - emoji = "✅" if server.squad_uuid == selected_uuid else ("⚪" if server.is_available else "🔒") - label = f"{emoji} {server.display_name}" - keyboard_rows.append([ - InlineKeyboardButton( - text=label, - callback_data=f"promo_offer_select_squad_{template.id}_{server.id}_{page}", - ) - ]) - - if total_pages > 1: - nav_row: List[InlineKeyboardButton] = [] - if page > 1: - nav_row.append( - InlineKeyboardButton( - text="⬅️", - callback_data=f"promo_offer_squad_page_{template.id}_{page - 1}", - ) - ) - if page < total_pages: - nav_row.append( - InlineKeyboardButton( - text="➡️", - callback_data=f"promo_offer_squad_page_{template.id}_{page + 1}", - ) - ) - if nav_row: - keyboard_rows.append(nav_row) - - action_row = [ - InlineKeyboardButton( - text=texts.t("ADMIN_PROMO_OFFER_SELECT_SQUAD_CLEAR", "🗑 Очистить"), - callback_data=f"promo_offer_clear_squad_{template.id}_{page}", - ), - InlineKeyboardButton( - text=texts.t("ADMIN_PROMO_OFFER_SELECT_SQUAD_BACK", "↩️ Назад"), - callback_data=f"promo_offer_squad_back_{template.id}", - ), - ] - keyboard_rows.append(action_row) - - await callback.message.edit_text( - text, - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_rows), - parse_mode="HTML", - ) - - -async def _render_offer_details( - callback: CallbackQuery, - template: PromoOfferTemplate, - language: str, -): - description = _describe_offer(template, language) - await callback.message.edit_text( - description, - reply_markup=_build_offer_detail_keyboard(template, language), - parse_mode="HTML", + texts = get_texts(db_user.language) + prompt = texts.t( + "ADMIN_PROMO_OFFER_PROMPT_SQUADS", + "Перечислите UUID сквадов через запятую или пробел. Для очистки отправьте 'clear':", ) + await _prompt_edit(callback, state, template_id, prompt, AdminStates.editing_promo_offer_squads) async def _handle_edit_field( @@ -638,128 +488,8 @@ async def process_edit_test_duration(message: Message, state: FSMContext, db: As await _handle_edit_field(message, state, db, db_user, "test_duration_hours") -@admin_required -@error_handler -async def paginate_squad_selection(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): - try: - prefix = "promo_offer_squad_page_" - if not callback.data.startswith(prefix): - raise ValueError("invalid prefix") - payload = callback.data[len(prefix):] - template_id_str, page_str = payload.split("_", 1) - template_id = int(template_id_str) - page = int(page_str) - except (ValueError, AttributeError): - await callback.answer("❌ Некорректные данные", show_alert=True) - return - - template = await get_promo_offer_template_by_id(db, template_id) - if not template: - await callback.answer("❌ Предложение не найдено", show_alert=True) - return - - await state.update_data(selected_promo_offer=template.id) - await _render_squad_selection(callback, template, db, db_user.language, page=page) - await callback.answer() - - -@admin_required -@error_handler -async def select_squad_for_template(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): - try: - prefix = "promo_offer_select_squad_" - if not callback.data.startswith(prefix): - raise ValueError("invalid prefix") - payload = callback.data[len(prefix):] - template_id_str, server_id_str, page_str = payload.split("_", 2) - template_id = int(template_id_str) - server_id = int(server_id_str) - page = int(page_str) - except (ValueError, AttributeError): - await callback.answer("❌ Некорректные данные", show_alert=True) - return - - template = await get_promo_offer_template_by_id(db, template_id) - if not template: - await callback.answer("❌ Предложение не найдено", show_alert=True) - return - - server = await get_server_squad_by_id(db, server_id) - if not server: - await callback.answer( - get_texts(db_user.language).t( - "ADMIN_PROMO_OFFER_SELECT_SQUAD_NOT_FOUND", - "❌ Сервер не найден", - ), - show_alert=True, - ) - return - - await update_promo_offer_template(db, template, test_squad_uuids=[server.squad_uuid]) - updated = await get_promo_offer_template_by_id(db, template.id) - if updated: - await state.update_data(selected_promo_offer=updated.id) - - texts = get_texts(db_user.language) - await callback.answer(texts.t("ADMIN_PROMO_OFFER_SELECT_SQUAD_UPDATED", "✅ Сквад обновлён")) - - if updated: - await _render_offer_details(callback, updated, db_user.language) - else: - await _render_squad_selection(callback, template, db, db_user.language, page=page) - - -@admin_required -@error_handler -async def clear_squad_for_template(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): - try: - prefix = "promo_offer_clear_squad_" - if not callback.data.startswith(prefix): - raise ValueError("invalid prefix") - payload = callback.data[len(prefix):] - template_id_str, page_str = payload.split("_", 1) - template_id = int(template_id_str) - page = int(page_str) - except (ValueError, AttributeError): - await callback.answer("❌ Некорректные данные", show_alert=True) - return - - template = await get_promo_offer_template_by_id(db, template_id) - if not template: - await callback.answer("❌ Предложение не найдено", show_alert=True) - return - - await update_promo_offer_template(db, template, test_squad_uuids=[]) - updated = await get_promo_offer_template_by_id(db, template.id) - if updated: - await state.update_data(selected_promo_offer=updated.id) - - texts = get_texts(db_user.language) - await callback.answer(texts.t("ADMIN_PROMO_OFFER_SELECT_SQUAD_CLEARED", "✅ Сквад очищен")) - - if updated: - await _render_squad_selection(callback, updated, db, db_user.language, page=page) - else: - await _render_squad_selection(callback, template, db, db_user.language, page=page) - - -@admin_required -@error_handler -async def back_to_offer_from_squads(callback: CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): - try: - template_id = int(callback.data.split("_")[-1]) - except (ValueError, AttributeError): - await callback.answer("❌ Некорректные данные", show_alert=True) - return - - template = await get_promo_offer_template_by_id(db, template_id) - if not template: - await callback.answer("❌ Предложение не найдено", show_alert=True) - return - - await state.update_data(selected_promo_offer=template.id) - await _render_offer_details(callback, template, db_user.language) - await callback.answer() +async def process_edit_test_squads(message: Message, state: FSMContext, db: AsyncSession, db_user: User): + await _handle_edit_field(message, state, db, db_user, "test_squad_uuids") def register_handlers(dp: Dispatcher): @@ -770,10 +500,6 @@ def register_handlers(dp: Dispatcher): dp.callback_query.register(prompt_edit_discount, F.data.startswith("promo_offer_edit_discount_")) dp.callback_query.register(prompt_edit_duration, F.data.startswith("promo_offer_edit_duration_")) dp.callback_query.register(prompt_edit_squads, F.data.startswith("promo_offer_edit_squads_")) - dp.callback_query.register(paginate_squad_selection, F.data.startswith("promo_offer_squad_page_")) - dp.callback_query.register(select_squad_for_template, F.data.startswith("promo_offer_select_squad_")) - dp.callback_query.register(clear_squad_for_template, F.data.startswith("promo_offer_clear_squad_")) - dp.callback_query.register(back_to_offer_from_squads, F.data.startswith("promo_offer_squad_back_")) dp.callback_query.register(show_send_segments, F.data.startswith("promo_offer_send_menu_")) dp.callback_query.register(send_offer_to_segment, F.data.startswith("promo_offer_send_")) dp.callback_query.register(show_promo_offer_details, F.data.startswith("promo_offer_")) @@ -783,3 +509,4 @@ def register_handlers(dp: Dispatcher): dp.message.register(process_edit_valid_hours, AdminStates.editing_promo_offer_valid_hours) dp.message.register(process_edit_discount_percent, AdminStates.editing_promo_offer_discount) dp.message.register(process_edit_test_duration, AdminStates.editing_promo_offer_test_duration) + dp.message.register(process_edit_test_squads, AdminStates.editing_promo_offer_squads) diff --git a/app/services/promo_offer_service.py b/app/services/promo_offer_service.py index 873b3920..4dd2c18d 100644 --- a/app/services/promo_offer_service.py +++ b/app/services/promo_offer_service.py @@ -34,21 +34,10 @@ class PromoOfferService: return False, None, None, "subscription_missing" payload = offer.extra_data or {} - raw_squads = payload.get("test_squad_uuids") or payload.get("squads") or [] - if isinstance(raw_squads, str): - candidates = [raw_squads] - else: - try: - candidates = list(raw_squads) - except TypeError: - candidates = [] - - squad_uuids: Sequence[str] = [str(item) for item in candidates if item] + squad_uuids: Sequence[str] = payload.get("test_squad_uuids") or payload.get("squads") or [] if not squad_uuids: return False, None, None, "squads_missing" - squad_uuids = list(dict.fromkeys(squad_uuids)) - try: duration_hours = int(payload.get("test_duration_hours") or payload.get("duration_hours") or 24) except (TypeError, ValueError): diff --git a/locales/en.json b/locales/en.json index e6eeeae1..817b8091 100644 --- a/locales/en.json +++ b/locales/en.json @@ -584,18 +584,7 @@ "ADMIN_PROMO_OFFER_PROMPT_VALID": "Enter validity (hours):", "ADMIN_PROMO_OFFER_PROMPT_DISCOUNT": "Enter discount percentage:", "ADMIN_PROMO_OFFER_PROMPT_DURATION": "Enter test access duration (hours):", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_TITLE": "🌍 Select a squad", - "ADMIN_PROMO_OFFER_SELECTED_SQUAD": "Current squad: {name}", - "ADMIN_PROMO_OFFER_SELECTED_SQUAD_UUID": "Current squad: {uuid}", - "ADMIN_PROMO_OFFER_SELECTED_SQUAD_EMPTY": "Current squad: not selected", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_HINT": "Pick a server to grant temporary access from the list below.", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_PAGE": "Page {page}/{total}", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_CLEAR": "🗑 Clear", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_BACK": "↩️ Back", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_UPDATED": "✅ Squad updated", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_CLEARED": "✅ Squad cleared", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_NOT_FOUND": "❌ Server not found", - "ADMIN_PROMO_OFFER_NO_SQUADS_AVAILABLE": "❌ No servers available.", + "ADMIN_PROMO_OFFER_PROMPT_SQUADS": "List squad UUIDs separated by commas or spaces. Send 'clear' to reset:", "ADMIN_PROMO_OFFER_SENDING": "Starting broadcast...", "ADMIN_PROMO_OFFER_NO_USERS": "No matching users found.", "ADMIN_PROMO_OFFER_RESULT": "📬 Broadcast finished\nSent: {sent}\nFailed: {failed}", diff --git a/locales/ru.json b/locales/ru.json index d135eb59..be4b4c7b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -584,18 +584,7 @@ "ADMIN_PROMO_OFFER_PROMPT_VALID": "Укажите срок действия (в часах):", "ADMIN_PROMO_OFFER_PROMPT_DISCOUNT": "Введите размер скидки в процентах:", "ADMIN_PROMO_OFFER_PROMPT_DURATION": "Введите длительность тестового доступа (в часах):", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_TITLE": "🌍 Выберите сквад", - "ADMIN_PROMO_OFFER_SELECTED_SQUAD": "Текущий сквад: {name}", - "ADMIN_PROMO_OFFER_SELECTED_SQUAD_UUID": "Текущий сквад: {uuid}", - "ADMIN_PROMO_OFFER_SELECTED_SQUAD_EMPTY": "Текущий сквад: не выбран", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_HINT": "Выберите сервер для тестового доступа из списка ниже.", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_PAGE": "Страница {page}/{total}", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_CLEAR": "🗑 Очистить", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_BACK": "↩️ Назад", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_UPDATED": "✅ Сквад обновлён", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_CLEARED": "✅ Сквад очищен", - "ADMIN_PROMO_OFFER_SELECT_SQUAD_NOT_FOUND": "❌ Сервер не найден", - "ADMIN_PROMO_OFFER_NO_SQUADS_AVAILABLE": "❌ Доступные серверы не найдены.", + "ADMIN_PROMO_OFFER_PROMPT_SQUADS": "Перечислите UUID сквадов через запятую или пробел. Для очистки отправьте 'clear':", "ADMIN_PROMO_OFFER_SENDING": "Начинаем рассылку...", "ADMIN_PROMO_OFFER_NO_USERS": "Подходящих пользователей не найдено.", "ADMIN_PROMO_OFFER_RESULT": "📬 Рассылка завершена\nОтправлено: {sent}\nОшибок: {failed}",