"""Управление тарифами в админ-панели.""" import logging from typing import Dict, List, Optional, Tuple from aiogram import Dispatcher, types, F from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.crud.tariff import ( get_all_tariffs, get_tariff_by_id, create_tariff, update_tariff, delete_tariff, get_tariff_subscriptions_count, get_tariffs_with_subscriptions_count, ) from app.database.crud.promo_group import get_promo_groups_with_counts from app.database.crud.server_squad import get_all_server_squads from app.database.models import Tariff, User from app.localization.texts import get_texts from app.states import AdminStates from app.utils.decorators import admin_required, error_handler logger = logging.getLogger(__name__) ITEMS_PER_PAGE = 10 def _format_traffic(gb: int) -> str: """Форматирует трафик.""" if gb == 0: return "Безлимит" return f"{gb} ГБ" def _format_price_kopeks(kopeks: int) -> str: """Форматирует цену из копеек в рубли.""" rubles = kopeks / 100 if rubles == int(rubles): return f"{int(rubles)} ₽" return f"{rubles:.2f} ₽" def _format_period(days: int) -> str: """Форматирует период.""" if days == 1: return "1 день" elif days < 5: return f"{days} дня" elif days < 21 or days % 10 >= 5 or days % 10 == 0: return f"{days} дней" elif days % 10 == 1: return f"{days} день" else: return f"{days} дня" def _parse_period_prices(text: str) -> Dict[str, int]: """ Парсит строку с ценами периодов. Формат: "30:9900, 90:24900, 180:44900" или "30=9900; 90=24900" """ prices = {} text = text.replace(";", ",").replace("=", ":") for part in text.split(","): part = part.strip() if not part: continue if ":" not in part: continue period_str, price_str = part.split(":", 1) try: period = int(period_str.strip()) price = int(price_str.strip()) if period > 0 and price >= 0: prices[str(period)] = price except ValueError: continue return prices def _format_period_prices_display(prices: Dict[str, int]) -> str: """Форматирует цены периодов для отображения.""" if not prices: return "Не заданы" lines = [] for period_str in sorted(prices.keys(), key=int): period = int(period_str) price = prices[period_str] lines.append(f" • {_format_period(period)}: {_format_price_kopeks(price)}") return "\n".join(lines) def _format_period_prices_for_edit(prices: Dict[str, int]) -> str: """Форматирует цены периодов для редактирования.""" if not prices: return "30:9900, 90:24900, 180:44900" parts = [] for period_str in sorted(prices.keys(), key=int): parts.append(f"{period_str}:{prices[period_str]}") return ", ".join(parts) def get_tariffs_list_keyboard( tariffs: List[Tuple[Tariff, int]], language: str, page: int = 0, total_pages: int = 1, ) -> InlineKeyboardMarkup: """Создает клавиатуру списка тарифов.""" texts = get_texts(language) buttons = [] for tariff, subs_count in tariffs: status = "✅" if tariff.is_active else "❌" button_text = f"{status} {tariff.name} ({subs_count})" buttons.append([ InlineKeyboardButton( text=button_text, callback_data=f"admin_tariff_view:{tariff.id}" ) ]) # Пагинация nav_buttons = [] if page > 0: nav_buttons.append( InlineKeyboardButton(text="◀️", callback_data=f"admin_tariffs_page:{page-1}") ) if page < total_pages - 1: nav_buttons.append( InlineKeyboardButton(text="▶️", callback_data=f"admin_tariffs_page:{page+1}") ) if nav_buttons: buttons.append(nav_buttons) # Кнопка создания buttons.append([ InlineKeyboardButton( text="➕ Создать тариф", callback_data="admin_tariff_create" ) ]) # Кнопка назад buttons.append([ InlineKeyboardButton( text=texts.BACK, callback_data="admin_submenu_settings" ) ]) return InlineKeyboardMarkup(inline_keyboard=buttons) def get_tariff_view_keyboard( tariff: Tariff, language: str, ) -> InlineKeyboardMarkup: """Создает клавиатуру просмотра тарифа.""" texts = get_texts(language) buttons = [] # Редактирование полей buttons.append([ InlineKeyboardButton(text="✏️ Название", callback_data=f"admin_tariff_edit_name:{tariff.id}"), InlineKeyboardButton(text="📝 Описание", callback_data=f"admin_tariff_edit_desc:{tariff.id}"), ]) buttons.append([ InlineKeyboardButton(text="📊 Трафик", callback_data=f"admin_tariff_edit_traffic:{tariff.id}"), InlineKeyboardButton(text="📱 Устройства", callback_data=f"admin_tariff_edit_devices:{tariff.id}"), ]) # Цены за периоды только для обычных тарифов (не суточных) is_daily = getattr(tariff, 'is_daily', False) if not is_daily: buttons.append([ InlineKeyboardButton(text="💰 Цены", callback_data=f"admin_tariff_edit_prices:{tariff.id}"), InlineKeyboardButton(text="🎚️ Уровень", callback_data=f"admin_tariff_edit_tier:{tariff.id}"), ]) else: buttons.append([ InlineKeyboardButton(text="🎚️ Уровень", callback_data=f"admin_tariff_edit_tier:{tariff.id}"), ]) buttons.append([ InlineKeyboardButton(text="📱💰 Цена за устройство", callback_data=f"admin_tariff_edit_device_price:{tariff.id}"), InlineKeyboardButton(text="📱🔒 Макс. устройств", callback_data=f"admin_tariff_edit_max_devices:{tariff.id}"), ]) buttons.append([ InlineKeyboardButton(text="⏰ Дни триала", callback_data=f"admin_tariff_edit_trial_days:{tariff.id}"), ]) buttons.append([ InlineKeyboardButton(text="📈 Докупка трафика", callback_data=f"admin_tariff_edit_traffic_topup:{tariff.id}"), ]) buttons.append([ InlineKeyboardButton(text="🔄 Сброс трафика", callback_data=f"admin_tariff_edit_reset_mode:{tariff.id}"), ]) buttons.append([ InlineKeyboardButton(text="🌐 Серверы", callback_data=f"admin_tariff_edit_squads:{tariff.id}"), InlineKeyboardButton(text="👥 Промогруппы", callback_data=f"admin_tariff_edit_promo:{tariff.id}"), ]) # Суточный режим - только для уже суточных тарифов показываем настройки # Новые тарифы делаются суточными только при создании if is_daily: buttons.append([ InlineKeyboardButton(text="💰 Суточная цена", callback_data=f"admin_tariff_edit_daily_price:{tariff.id}"), ]) # Примечание: отключение суточного режима убрано - это необратимое решение при создании # Переключение триала if tariff.is_trial_available: buttons.append([ InlineKeyboardButton(text="🎁 ❌ Убрать триал", callback_data=f"admin_tariff_toggle_trial:{tariff.id}") ]) else: buttons.append([ InlineKeyboardButton(text="🎁 Сделать триальным", callback_data=f"admin_tariff_toggle_trial:{tariff.id}") ]) # Переключение активности if tariff.is_active: buttons.append([ InlineKeyboardButton(text="❌ Деактивировать", callback_data=f"admin_tariff_toggle:{tariff.id}") ]) else: buttons.append([ InlineKeyboardButton(text="✅ Активировать", callback_data=f"admin_tariff_toggle:{tariff.id}") ]) # Удаление buttons.append([ InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_tariff_delete:{tariff.id}") ]) # Назад к списку buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data="admin_tariffs") ]) return InlineKeyboardMarkup(inline_keyboard=buttons) def _format_traffic_reset_mode(mode: Optional[str]) -> str: """Форматирует режим сброса трафика для отображения.""" mode_labels = { 'DAY': '📅 Ежедневно', 'WEEK': '📆 Еженедельно', 'MONTH': '🗓️ Ежемесячно', 'NO_RESET': '🚫 Никогда', } if mode is None: return f"🌐 Глобальная настройка ({settings.DEFAULT_TRAFFIC_RESET_STRATEGY})" return mode_labels.get(mode, f"⚠️ Неизвестно ({mode})") def _format_traffic_topup_packages(tariff: Tariff) -> str: """Форматирует пакеты докупки трафика для отображения.""" if not getattr(tariff, 'traffic_topup_enabled', False): return "❌ Отключено" packages = tariff.get_traffic_topup_packages() if hasattr(tariff, 'get_traffic_topup_packages') else {} if not packages: return "✅ Включено, но пакеты не настроены" lines = ["✅ Включено"] for gb in sorted(packages.keys()): price = packages[gb] lines.append(f" • {gb} ГБ: {_format_price_kopeks(price)}") return "\n".join(lines) def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> str: """Форматирует информацию о тарифе.""" texts = get_texts(language) status = "✅ Активен" if tariff.is_active else "❌ Неактивен" traffic = _format_traffic(tariff.traffic_limit_gb) prices_display = _format_period_prices_display(tariff.period_prices or {}) # Форматируем список серверов squads_list = tariff.allowed_squads or [] squads_display = f"{len(squads_list)} серверов" if squads_list else "Все серверы" # Форматируем промогруппы promo_groups = tariff.allowed_promo_groups or [] if promo_groups: promo_display = ", ".join(pg.name for pg in promo_groups) else: promo_display = "Доступен всем" trial_status = "✅ Да" if tariff.is_trial_available else "❌ Нет" # Форматируем дни триала trial_days = getattr(tariff, 'trial_duration_days', None) if trial_days: trial_days_display = f"{trial_days} дней" else: trial_days_display = f"По умолчанию ({settings.TRIAL_DURATION_DAYS} дней)" # Форматируем цену за устройство device_price = getattr(tariff, 'device_price_kopeks', None) if device_price is not None and device_price > 0: device_price_display = _format_price_kopeks(device_price) + "/мес" else: device_price_display = "Недоступно" # Форматируем макс. устройств max_devices = getattr(tariff, 'max_device_limit', None) if max_devices is not None and max_devices > 0: max_devices_display = str(max_devices) else: max_devices_display = "∞ (без лимита)" # Форматируем докупку трафика traffic_topup_display = _format_traffic_topup_packages(tariff) # Форматируем режим сброса трафика traffic_reset_mode = getattr(tariff, 'traffic_reset_mode', None) traffic_reset_display = _format_traffic_reset_mode(traffic_reset_mode) # Форматируем суточный тариф is_daily = getattr(tariff, 'is_daily', False) daily_price_kopeks = getattr(tariff, 'daily_price_kopeks', 0) # Формируем блок цен в зависимости от типа тарифа if is_daily: price_block = f"💰 Суточная цена: {_format_price_kopeks(daily_price_kopeks)}/день" tariff_type = "🔄 Суточный" else: price_block = f"Цены:\n{prices_display}" tariff_type = "📅 Периодный" return f"""📦 Тариф: {tariff.name} {status} | {tariff_type} 🎚️ Уровень: {tariff.tier_level} 📊 Порядок: {tariff.display_order} Параметры: • Трафик: {traffic} • Устройств: {tariff.device_limit} • Макс. устройств: {max_devices_display} • Цена за доп. устройство: {device_price_display} • Триал: {trial_status} • Дней триала: {trial_days_display} Докупка трафика: {traffic_topup_display} Сброс трафика: {traffic_reset_display} {price_block} Серверы: {squads_display} Промогруппы: {promo_display} 📊 Подписок на тарифе: {subs_count} {f"📝 {tariff.description}" if tariff.description else ""}""" @admin_required @error_handler async def show_tariffs_list( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Показывает список тарифов.""" await state.clear() texts = get_texts(db_user.language) # Проверяем режим продаж if not settings.is_tariffs_mode(): await callback.message.edit_text( "⚠️ Режим тарифов отключен\n\n" "Для использования тарифов установите:\n" "SALES_MODE=tariffs\n\n" "Текущий режим: classic", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_settings")] ]), parse_mode="HTML" ) await callback.answer() return tariffs_data = await get_tariffs_with_subscriptions_count(db, include_inactive=True) if not tariffs_data: await callback.message.edit_text( "📦 Тарифы\n\n" "Тарифы ещё не созданы.\n" "Создайте первый тариф для начала работы.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="➕ Создать тариф", callback_data="admin_tariff_create")], [InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_settings")] ]), parse_mode="HTML" ) await callback.answer() return total_pages = (len(tariffs_data) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE page_data = tariffs_data[:ITEMS_PER_PAGE] total_subs = sum(count for _, count in tariffs_data) active_count = sum(1 for t, _ in tariffs_data if t.is_active) await callback.message.edit_text( f"📦 Тарифы\n\n" f"Всего: {len(tariffs_data)} (активных: {active_count})\n" f"Подписок на тарифах: {total_subs}\n\n" "Выберите тариф для просмотра и редактирования:", reply_markup=get_tariffs_list_keyboard(page_data, db_user.language, 0, total_pages), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def show_tariffs_page( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Показывает страницу списка тарифов.""" texts = get_texts(db_user.language) page = int(callback.data.split(":")[1]) tariffs_data = await get_tariffs_with_subscriptions_count(db, include_inactive=True) total_pages = (len(tariffs_data) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE start_idx = page * ITEMS_PER_PAGE end_idx = start_idx + ITEMS_PER_PAGE page_data = tariffs_data[start_idx:end_idx] total_subs = sum(count for _, count in tariffs_data) active_count = sum(1 for t, _ in tariffs_data if t.is_active) await callback.message.edit_text( f"📦 Тарифы (стр. {page + 1}/{total_pages})\n\n" f"Всего: {len(tariffs_data)} (активных: {active_count})\n" f"Подписок на тарифах: {total_subs}\n\n" "Выберите тариф для просмотра и редактирования:", reply_markup=get_tariffs_list_keyboard(page_data, db_user.language, page, total_pages), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def view_tariff( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Просмотр тарифа.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return subs_count = await get_tariff_subscriptions_count(db, tariff_id) await callback.message.edit_text( format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def toggle_tariff( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Переключает активность тарифа.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return tariff = await update_tariff(db, tariff, is_active=not tariff.is_active) subs_count = await get_tariff_subscriptions_count(db, tariff_id) status = "активирован" if tariff.is_active else "деактивирован" await callback.answer(f"Тариф {status}", show_alert=True) await callback.message.edit_text( format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def toggle_trial_tariff( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Переключает тариф как триальный.""" from app.database.crud.tariff import set_trial_tariff, clear_trial_tariff tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return if tariff.is_trial_available: # Снимаем флаг триала await clear_trial_tariff(db) await callback.answer("Триал снят с тарифа", show_alert=True) else: # Устанавливаем этот тариф как триальный (снимает флаг с других) await set_trial_tariff(db, tariff_id) await callback.answer(f"Тариф «{tariff.name}» установлен как триальный", show_alert=True) # Перезагружаем тариф tariff = await get_tariff_by_id(db, tariff_id) subs_count = await get_tariff_subscriptions_count(db, tariff_id) await callback.message.edit_text( format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def toggle_daily_tariff( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Переключает суточный режим тарифа.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return is_daily = getattr(tariff, 'is_daily', False) if is_daily: # Отключаем суточный режим tariff = await update_tariff(db, tariff, is_daily=False, daily_price_kopeks=0) await callback.answer("Суточный режим отключен", show_alert=True) else: # Включаем суточный режим (с ценой по умолчанию) tariff = await update_tariff(db, tariff, is_daily=True, daily_price_kopeks=5000) # 50 руб по умолчанию await callback.answer( f"Суточный режим включен. Цена: 50 ₽/день\n" "Настройте цену через кнопку «💰 Суточная цена»", show_alert=True ) subs_count = await get_tariff_subscriptions_count(db, tariff_id) await callback.message.edit_text( format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def start_edit_daily_price( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование суточной цены.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return current_price = getattr(tariff, 'daily_price_kopeks', 0) current_rubles = current_price / 100 if current_price else 0 await state.set_state(AdminStates.editing_tariff_daily_price) await state.update_data(tariff_id=tariff_id, language=db_user.language) await callback.message.edit_text( f"💰 Редактирование суточной цены\n\n" f"Тариф: {tariff.name}\n" f"Текущая цена: {_format_price_kopeks(current_price)}/день\n\n" "Введите новую цену за день в рублях.\n" "Пример: 50 или 99.90", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_daily_price_input( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает ввод суточной цены (создание и редактирование).""" texts = get_texts(db_user.language) data = await state.get_data() tariff_id = data.get("tariff_id") # Парсим цену try: price_rubles = float(message.text.strip().replace(",", ".")) if price_rubles <= 0: raise ValueError("Цена должна быть положительной") price_kopeks = int(price_rubles * 100) except ValueError: await message.answer( "❌ Некорректная цена. Введите положительное число.\n" "Пример: 50 или 99.90", parse_mode="HTML" ) return # Проверяем - это создание или редактирование is_creating = data.get("tariff_is_daily") and not tariff_id if is_creating: # Создаем новый суточный тариф tariff = await create_tariff( db, name=data['tariff_name'], traffic_limit_gb=data['tariff_traffic'], device_limit=data['tariff_devices'], tier_level=data['tariff_tier'], period_prices={}, is_active=True, is_daily=True, daily_price_kopeks=price_kopeks, ) await state.clear() await message.answer( f"✅ Суточный тариф создан!\n\n" + format_tariff_info(tariff, db_user.language, 0), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) else: # Редактируем существующий тариф if not tariff_id: await state.clear() return tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return tariff = await update_tariff(db, tariff, daily_price_kopeks=price_kopeks) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Суточная цена установлена: {_format_price_kopeks(price_kopeks)}/день\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) # ============ СОЗДАНИЕ ТАРИФА ============ @admin_required @error_handler async def start_create_tariff( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает создание тарифа.""" texts = get_texts(db_user.language) await state.set_state(AdminStates.creating_tariff_name) await state.update_data(language=db_user.language) await callback.message.edit_text( "📦 Создание тарифа\n\n" "Шаг 1/6: Введите название тарифа\n\n" "Пример: Базовый, Премиум, Бизнес", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_tariff_name( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает название тарифа.""" texts = get_texts(db_user.language) name = message.text.strip() if len(name) < 2: await message.answer("Название должно быть не короче 2 символов") return if len(name) > 50: await message.answer("Название должно быть не длиннее 50 символов") return await state.update_data(tariff_name=name) await state.set_state(AdminStates.creating_tariff_traffic) await message.answer( "📦 Создание тарифа\n\n" f"Название: {name}\n\n" "Шаг 2/6: Введите лимит трафика в ГБ\n\n" "Введите 0 для безлимитного трафика\n" "Пример: 100, 500, 0", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) @admin_required @error_handler async def process_tariff_traffic( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает лимит трафика.""" texts = get_texts(db_user.language) try: traffic = int(message.text.strip()) if traffic < 0: raise ValueError except ValueError: await message.answer("Введите корректное число (0 или больше)") return data = await state.get_data() await state.update_data(tariff_traffic=traffic) await state.set_state(AdminStates.creating_tariff_devices) traffic_display = _format_traffic(traffic) await message.answer( "📦 Создание тарифа\n\n" f"Название: {data['tariff_name']}\n" f"Трафик: {traffic_display}\n\n" "Шаг 3/6: Введите лимит устройств\n\n" "Пример: 1, 3, 5", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) @admin_required @error_handler async def process_tariff_devices( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает лимит устройств.""" texts = get_texts(db_user.language) try: devices = int(message.text.strip()) if devices < 1: raise ValueError except ValueError: await message.answer("Введите корректное число (1 или больше)") return data = await state.get_data() await state.update_data(tariff_devices=devices) await state.set_state(AdminStates.creating_tariff_tier) traffic_display = _format_traffic(data['tariff_traffic']) await message.answer( "📦 Создание тарифа\n\n" f"Название: {data['tariff_name']}\n" f"Трафик: {traffic_display}\n" f"Устройств: {devices}\n\n" "Шаг 4/6: Введите уровень тарифа (1-10)\n\n" "Уровень используется для визуального отображения\n" "1 - базовый, 10 - максимальный\n" "Пример: 1, 2, 3", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) @admin_required @error_handler async def process_tariff_tier( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает уровень тарифа.""" texts = get_texts(db_user.language) try: tier = int(message.text.strip()) if tier < 1 or tier > 10: raise ValueError except ValueError: await message.answer("Введите число от 1 до 10") return data = await state.get_data() await state.update_data(tariff_tier=tier) traffic_display = _format_traffic(data['tariff_traffic']) # Шаг 5/6: Выбор типа тарифа await message.answer( "📦 Создание тарифа\n\n" f"Название: {data['tariff_name']}\n" f"Трафик: {traffic_display}\n" f"Устройств: {data['tariff_devices']}\n" f"Уровень: {tier}\n\n" "Шаг 5/6: Выберите тип тарифа", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="📅 Периодный (месяцы)", callback_data="tariff_type_periodic")], [InlineKeyboardButton(text="🔄 Суточный (оплата за день)", callback_data="tariff_type_daily")], [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) @admin_required @error_handler async def select_tariff_type_periodic( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Выбирает периодный тип тарифа.""" texts = get_texts(db_user.language) data = await state.get_data() await state.update_data(tariff_is_daily=False) await state.set_state(AdminStates.creating_tariff_prices) traffic_display = _format_traffic(data['tariff_traffic']) await callback.message.edit_text( "📦 Создание тарифа\n\n" f"Название: {data['tariff_name']}\n" f"Трафик: {traffic_display}\n" f"Устройств: {data['tariff_devices']}\n" f"Уровень: {data['tariff_tier']}\n" f"Тип: 📅 Периодный\n\n" "Шаг 6/6: Введите цены на периоды\n\n" "Формат: дней:цена_в_копейках\n" "Несколько периодов через запятую\n\n" "Пример:\n30:9900, 90:24900, 180:44900, 360:79900", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def select_tariff_type_daily( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Выбирает суточный тип тарифа.""" from app.states import AdminStates texts = get_texts(db_user.language) data = await state.get_data() await state.update_data(tariff_is_daily=True) await state.set_state(AdminStates.editing_tariff_daily_price) traffic_display = _format_traffic(data['tariff_traffic']) await callback.message.edit_text( "📦 Создание суточного тарифа\n\n" f"Название: {data['tariff_name']}\n" f"Трафик: {traffic_display}\n" f"Устройств: {data['tariff_devices']}\n" f"Уровень: {data['tariff_tier']}\n" f"Тип: 🔄 Суточный\n\n" "Шаг 6/6: Введите суточную цену в рублях\n\n" "Пример: 50 (50 ₽/день), 99.90 (99.90 ₽/день)", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data="admin_tariffs")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_tariff_prices( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает цены тарифа.""" texts = get_texts(db_user.language) prices = _parse_period_prices(message.text.strip()) if not prices: await message.answer( "Не удалось распознать цены.\n\n" "Формат: дней:цена_в_копейках\n" "Пример: 30:9900, 90:24900", parse_mode="HTML" ) return data = await state.get_data() await state.update_data(tariff_prices=prices) traffic_display = _format_traffic(data['tariff_traffic']) prices_display = _format_period_prices_display(prices) # Создаем тариф tariff = await create_tariff( db, name=data['tariff_name'], traffic_limit_gb=data['tariff_traffic'], device_limit=data['tariff_devices'], tier_level=data['tariff_tier'], period_prices=prices, is_active=True, ) await state.clear() subs_count = 0 await message.answer( f"✅ Тариф создан!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) # ============ РЕДАКТИРОВАНИЕ ТАРИФА ============ @admin_required @error_handler async def start_edit_tariff_name( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование названия тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_name) await state.update_data(tariff_id=tariff_id, language=db_user.language) await callback.message.edit_text( f"✏️ Редактирование названия\n\n" f"Текущее название: {tariff.name}\n\n" "Введите новое название:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_name( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новое название тарифа.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return name = message.text.strip() if len(name) < 2 or len(name) > 50: await message.answer("Название должно быть от 2 до 50 символов") return tariff = await update_tariff(db, tariff, name=name) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Название изменено!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def start_edit_tariff_description( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование описания тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_description) await state.update_data(tariff_id=tariff_id, language=db_user.language) current_desc = tariff.description or "Не задано" await callback.message.edit_text( f"📝 Редактирование описания\n\n" f"Текущее описание:\n{current_desc}\n\n" "Введите новое описание (или - для удаления):", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_description( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новое описание тарифа.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return description = message.text.strip() if description == "-": description = None tariff = await update_tariff(db, tariff, description=description) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Описание изменено!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def start_edit_tariff_traffic( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование трафика тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_traffic) await state.update_data(tariff_id=tariff_id, language=db_user.language) current_traffic = _format_traffic(tariff.traffic_limit_gb) await callback.message.edit_text( f"📊 Редактирование трафика\n\n" f"Текущий лимит: {current_traffic}\n\n" "Введите новый лимит в ГБ (0 = безлимит):", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_traffic( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новый лимит трафика.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return try: traffic = int(message.text.strip()) if traffic < 0: raise ValueError except ValueError: await message.answer("Введите корректное число (0 или больше)") return tariff = await update_tariff(db, tariff, traffic_limit_gb=traffic) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Трафик изменен!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def start_edit_tariff_devices( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование лимита устройств.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_devices) await state.update_data(tariff_id=tariff_id, language=db_user.language) await callback.message.edit_text( f"📱 Редактирование устройств\n\n" f"Текущий лимит: {tariff.device_limit}\n\n" "Введите новый лимит устройств:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_devices( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новый лимит устройств.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return try: devices = int(message.text.strip()) if devices < 1: raise ValueError except ValueError: await message.answer("Введите корректное число (1 или больше)") return tariff = await update_tariff(db, tariff, device_limit=devices) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Лимит устройств изменен!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def start_edit_tariff_tier( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование уровня тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_tier) await state.update_data(tariff_id=tariff_id, language=db_user.language) await callback.message.edit_text( f"🎚️ Редактирование уровня\n\n" f"Текущий уровень: {tariff.tier_level}\n\n" "Введите новый уровень (1-10):", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_tier( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новый уровень тарифа.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return try: tier = int(message.text.strip()) if tier < 1 or tier > 10: raise ValueError except ValueError: await message.answer("Введите число от 1 до 10") return tariff = await update_tariff(db, tariff, tier_level=tier) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Уровень изменен!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) @admin_required @error_handler async def start_edit_tariff_prices( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование цен тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_prices) await state.update_data(tariff_id=tariff_id, language=db_user.language) current_prices = _format_period_prices_for_edit(tariff.period_prices or {}) prices_display = _format_period_prices_display(tariff.period_prices or {}) await callback.message.edit_text( f"💰 Редактирование цен\n\n" f"Текущие цены:\n{prices_display}\n\n" "Введите новые цены в формате:\n" f"{current_prices}\n\n" "(дней:цена_в_копейках, через запятую)", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_prices( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новые цены тарифа.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return prices = _parse_period_prices(message.text.strip()) if not prices: await message.answer( "Не удалось распознать цены.\n" "Формат: дней:цена\n" "Пример: 30:9900, 90:24900", parse_mode="HTML" ) return tariff = await update_tariff(db, tariff, period_prices=prices) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Цены изменены!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) # ============ РЕДАКТИРОВАНИЕ ЦЕНЫ ЗА УСТРОЙСТВО ============ @admin_required @error_handler async def start_edit_tariff_device_price( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование цены за устройство.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_device_price) await state.update_data(tariff_id=tariff_id, language=db_user.language) device_price = getattr(tariff, 'device_price_kopeks', None) if device_price is not None and device_price > 0: current_price = _format_price_kopeks(device_price) + "/мес" else: current_price = "Недоступно (докупка устройств запрещена)" await callback.message.edit_text( f"📱💰 Редактирование цены за устройство\n\n" f"Текущая цена: {current_price}\n\n" "Введите цену в копейках за одно устройство в месяц.\n\n" "• 0 или - — докупка устройств недоступна\n" "• Например: 5000 = 50₽/мес за устройство", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_device_price( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новую цену за устройство.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return text = message.text.strip() if text == "-" or text == "0": device_price = None else: try: device_price = int(text) if device_price < 0: raise ValueError except ValueError: await message.answer( "Введите корректное число (0 или больше).\n" "Для отключения докупки введите 0 или -", parse_mode="HTML" ) return tariff = await update_tariff(db, tariff, device_price_kopeks=device_price) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Цена за устройство изменена!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) # ============ РЕДАКТИРОВАНИЕ МАКС. УСТРОЙСТВ ============ @admin_required @error_handler async def start_edit_tariff_max_devices( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование макс. устройств.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_max_devices) await state.update_data(tariff_id=tariff_id, language=db_user.language) max_devices = getattr(tariff, 'max_device_limit', None) if max_devices is not None and max_devices > 0: current_max = str(max_devices) else: current_max = "∞ (без лимита)" await callback.message.edit_text( f"📱🔒 Редактирование макс. устройств\n\n" f"Текущее значение: {current_max}\n" f"Базовое кол-во устройств: {tariff.device_limit}\n\n" "Введите максимальное количество устройств, которое пользователь может докупить.\n\n" "• 0 или - — без ограничений\n" "• Например: 5 = максимум 5 устройств на тарифе", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_max_devices( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новое макс. кол-во устройств.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return text = message.text.strip() if text == "-" or text == "0": max_devices = None else: try: max_devices = int(text) if max_devices < 1: raise ValueError except ValueError: await message.answer( "Введите корректное число (1 или больше).\n" "Для снятия ограничения введите 0 или -", parse_mode="HTML" ) return tariff = await update_tariff(db, tariff, max_device_limit=max_devices) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Макс. устройств изменено!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) # ============ РЕДАКТИРОВАНИЕ ДНЕЙ ТРИАЛА ============ @admin_required @error_handler async def start_edit_tariff_trial_days( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование дней триала.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_trial_days) await state.update_data(tariff_id=tariff_id, language=db_user.language) trial_days = getattr(tariff, 'trial_duration_days', None) if trial_days: current_days = f"{trial_days} дней" else: current_days = f"По умолчанию ({settings.TRIAL_DURATION_DAYS} дней)" await callback.message.edit_text( f"⏰ Редактирование дней триала\n\n" f"Текущее значение: {current_days}\n\n" "Введите количество дней триала.\n\n" f"• 0 или - — использовать настройку по умолчанию ({settings.TRIAL_DURATION_DAYS} дней)\n" "• Например: 7 = 7 дней триала", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_view:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_tariff_trial_days( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новое количество дней триала.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return text = message.text.strip() if text == "-" or text == "0": trial_days = None else: try: trial_days = int(text) if trial_days < 1: raise ValueError except ValueError: await message.answer( "Введите корректное число дней (1 или больше).\n" "Для использования настройки по умолчанию введите 0 или -", parse_mode="HTML" ) return tariff = await update_tariff(db, tariff, trial_duration_days=trial_days) await state.clear() subs_count = await get_tariff_subscriptions_count(db, tariff_id) await message.answer( f"✅ Дни триала изменены!\n\n" + format_tariff_info(tariff, db_user.language, subs_count), reply_markup=get_tariff_view_keyboard(tariff, db_user.language), parse_mode="HTML" ) # ============ РЕДАКТИРОВАНИЕ ДОКУПКИ ТРАФИКА ============ def _parse_traffic_topup_packages(text: str) -> Dict[int, int]: """ Парсит строку с пакетами докупки трафика. Формат: "5:5000, 10:9000, 20:15000" (ГБ:цена_в_копейках) """ packages = {} text = text.replace(";", ",").replace("=", ":") for part in text.split(","): part = part.strip() if not part: continue if ":" not in part: continue gb_str, price_str = part.split(":", 1) try: gb = int(gb_str.strip()) price = int(price_str.strip()) if gb > 0 and price > 0: packages[gb] = price except ValueError: continue return packages def _format_traffic_topup_packages_for_edit(packages: Dict[int, int]) -> str: """Форматирует пакеты докупки для редактирования.""" if not packages: return "5:5000, 10:9000, 20:15000" parts = [] for gb in sorted(packages.keys()): parts.append(f"{gb}:{packages[gb]}") return ", ".join(parts) @admin_required @error_handler async def start_edit_tariff_traffic_topup( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Показывает меню настройки докупки трафика.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return # Проверяем, безлимитный ли тариф if tariff.is_unlimited_traffic: await callback.answer("Докупка недоступна для безлимитного тарифа", show_alert=True) return is_enabled = getattr(tariff, 'traffic_topup_enabled', False) packages = tariff.get_traffic_topup_packages() if hasattr(tariff, 'get_traffic_topup_packages') else {} max_topup_traffic = getattr(tariff, 'max_topup_traffic_gb', 0) or 0 # Форматируем текущие настройки if is_enabled: status = "✅ Включено" if packages: packages_display = "\n".join(f" • {gb} ГБ: {_format_price_kopeks(price)}" for gb, price in sorted(packages.items())) else: packages_display = " Пакеты не настроены" else: status = "❌ Отключено" packages_display = " -" # Форматируем лимит if max_topup_traffic > 0: max_limit_display = f"{max_topup_traffic} ГБ" else: max_limit_display = "Без ограничений" buttons = [] # Переключение вкл/выкл if is_enabled: buttons.append([ InlineKeyboardButton(text="❌ Отключить", callback_data=f"admin_tariff_toggle_traffic_topup:{tariff_id}") ]) else: buttons.append([ InlineKeyboardButton(text="✅ Включить", callback_data=f"admin_tariff_toggle_traffic_topup:{tariff_id}") ]) # Редактирование пакетов и лимита (только если включено) if is_enabled: buttons.append([ InlineKeyboardButton(text="📦 Настроить пакеты", callback_data=f"admin_tariff_edit_topup_packages:{tariff_id}") ]) buttons.append([ InlineKeyboardButton(text="📊 Макс. лимит трафика", callback_data=f"admin_tariff_edit_max_topup:{tariff_id}") ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) await callback.message.edit_text( f"📈 Докупка трафика для «{tariff.name}»\n\n" f"Статус: {status}\n\n" f"Пакеты:\n{packages_display}\n\n" f"Макс. лимит: {max_limit_display}\n\n" "Пользователи смогут докупать трафик по заданным ценам.", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def toggle_tariff_traffic_topup( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Переключает включение/выключение докупки трафика.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return is_enabled = getattr(tariff, 'traffic_topup_enabled', False) new_value = not is_enabled tariff = await update_tariff(db, tariff, traffic_topup_enabled=new_value) status_text = "включена" if new_value else "отключена" await callback.answer(f"Докупка трафика {status_text}") # Перерисовываем меню texts = get_texts(db_user.language) packages = tariff.get_traffic_topup_packages() if hasattr(tariff, 'get_traffic_topup_packages') else {} max_topup_traffic = getattr(tariff, 'max_topup_traffic_gb', 0) or 0 if new_value: status = "✅ Включено" if packages: packages_display = "\n".join(f" • {gb} ГБ: {_format_price_kopeks(price)}" for gb, price in sorted(packages.items())) else: packages_display = " Пакеты не настроены" else: status = "❌ Отключено" packages_display = " -" # Форматируем лимит if max_topup_traffic > 0: max_limit_display = f"{max_topup_traffic} ГБ" else: max_limit_display = "Без ограничений" buttons = [] if new_value: buttons.append([ InlineKeyboardButton(text="❌ Отключить", callback_data=f"admin_tariff_toggle_traffic_topup:{tariff_id}") ]) buttons.append([ InlineKeyboardButton(text="📦 Настроить пакеты", callback_data=f"admin_tariff_edit_topup_packages:{tariff_id}") ]) buttons.append([ InlineKeyboardButton(text="📊 Макс. лимит трафика", callback_data=f"admin_tariff_edit_max_topup:{tariff_id}") ]) else: buttons.append([ InlineKeyboardButton(text="✅ Включить", callback_data=f"admin_tariff_toggle_traffic_topup:{tariff_id}") ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) try: await callback.message.edit_text( f"📈 Докупка трафика для «{tariff.name}»\n\n" f"Статус: {status}\n\n" f"Пакеты:\n{packages_display}\n\n" f"Макс. лимит: {max_limit_display}\n\n" "Пользователи смогут докупать трафик по заданным ценам.", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) except TelegramBadRequest: pass @admin_required @error_handler async def start_edit_traffic_topup_packages( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование пакетов докупки трафика.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_traffic_topup_packages) await state.update_data(tariff_id=tariff_id, language=db_user.language) packages = tariff.get_traffic_topup_packages() if hasattr(tariff, 'get_traffic_topup_packages') else {} current_packages = _format_traffic_topup_packages_for_edit(packages) if packages: packages_display = "\n".join(f" • {gb} ГБ: {_format_price_kopeks(price)}" for gb, price in sorted(packages.items())) else: packages_display = " Не настроены" await callback.message.edit_text( f"📦 Настройка пакетов докупки трафика\n\n" f"Тариф: {tariff.name}\n\n" f"Текущие пакеты:\n{packages_display}\n\n" "Введите пакеты в формате:\n" f"{current_packages}\n\n" "(ГБ:цена_в_копейках, через запятую)\n" "Например: 5:5000, 10:9000 = 5ГБ за 50₽, 10ГБ за 90₽", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_edit_traffic_topup:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_traffic_topup_packages( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новые пакеты докупки трафика.""" data = await state.get_data() tariff_id = data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return packages = _parse_traffic_topup_packages(message.text.strip()) if not packages: await message.answer( "Не удалось распознать пакеты.\n\n" "Формат: ГБ:цена_в_копейках\n" "Пример: 5:5000, 10:9000, 20:15000", parse_mode="HTML" ) return # Преобразуем в формат для JSON (строковые ключи) packages_json = {str(gb): price for gb, price in packages.items()} tariff = await update_tariff(db, tariff, traffic_topup_packages=packages_json) await state.clear() # Показываем обновленное меню texts = get_texts(db_user.language) packages_display = "\n".join(f" • {gb} ГБ: {_format_price_kopeks(price)}" for gb, price in sorted(packages.items())) max_topup_traffic = getattr(tariff, 'max_topup_traffic_gb', 0) or 0 max_limit_display = f"{max_topup_traffic} ГБ" if max_topup_traffic > 0 else "Без ограничений" buttons = [ [InlineKeyboardButton(text="❌ Отключить", callback_data=f"admin_tariff_toggle_traffic_topup:{tariff_id}")], [InlineKeyboardButton(text="📦 Настроить пакеты", callback_data=f"admin_tariff_edit_topup_packages:{tariff_id}")], [InlineKeyboardButton(text="📊 Макс. лимит трафика", callback_data=f"admin_tariff_edit_max_topup:{tariff_id}")], [InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}")] ] await message.answer( f"✅ Пакеты обновлены!\n\n" f"📈 Докупка трафика для «{tariff.name}»\n\n" f"Статус: ✅ Включено\n\n" f"Пакеты:\n{packages_display}\n\n" f"Макс. лимит: {max_limit_display}\n\n" "Пользователи смогут докупать трафик по заданным ценам.", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) # ============ МАКСИМАЛЬНЫЙ ЛИМИТ ДОКУПКИ ТРАФИКА ============ @admin_required @error_handler async def start_edit_max_topup_traffic( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Начинает редактирование максимального лимита докупки трафика.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await state.set_state(AdminStates.editing_tariff_max_topup_traffic) await state.update_data(tariff_id=tariff_id) current_limit = getattr(tariff, 'max_topup_traffic_gb', 0) or 0 if current_limit > 0: current_display = f"{current_limit} ГБ" else: current_display = "Без ограничений" await callback.message.edit_text( f"📊 Максимальный лимит трафика\n\n" f"Тариф: {tariff.name}\n" f"Текущий лимит: {current_display}\n\n" f"Введите максимальный общий объем трафика (в ГБ), который может быть на подписке после всех докупок.\n\n" f"• Например, если тариф дает 100 ГБ и лимит 200 ГБ — пользователь сможет докупить еще 100 ГБ\n" f"• Введите 0 для снятия ограничения", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=texts.CANCEL, callback_data=f"admin_tariff_edit_traffic_topup:{tariff_id}")] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def process_edit_max_topup_traffic( message: types.Message, db_user: User, db: AsyncSession, state: FSMContext, ): """Обрабатывает новое значение максимального лимита докупки трафика.""" texts = get_texts(db_user.language) state_data = await state.get_data() tariff_id = state_data.get("tariff_id") tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await message.answer("Тариф не найден") await state.clear() return # Парсим значение text = message.text.strip() try: new_limit = int(text) if new_limit < 0: raise ValueError("Negative value") except ValueError: await message.answer( "Введите целое число (0 или больше).\n\n" "• 0 — без ограничений\n" "• 200 — максимум 200 ГБ на подписке", parse_mode="HTML" ) return tariff = await update_tariff(db, tariff, max_topup_traffic_gb=new_limit) await state.clear() # Показываем обновленное меню packages = tariff.get_traffic_topup_packages() if hasattr(tariff, 'get_traffic_topup_packages') else {} if packages: packages_display = "\n".join(f" • {gb} ГБ: {_format_price_kopeks(price)}" for gb, price in sorted(packages.items())) else: packages_display = " Пакеты не настроены" max_limit_display = f"{new_limit} ГБ" if new_limit > 0 else "Без ограничений" buttons = [ [InlineKeyboardButton(text="❌ Отключить", callback_data=f"admin_tariff_toggle_traffic_topup:{tariff_id}")], [InlineKeyboardButton(text="📦 Настроить пакеты", callback_data=f"admin_tariff_edit_topup_packages:{tariff_id}")], [InlineKeyboardButton(text="📊 Макс. лимит трафика", callback_data=f"admin_tariff_edit_max_topup:{tariff_id}")], [InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}")] ] await message.answer( f"✅ Лимит обновлен!\n\n" f"📈 Докупка трафика для «{tariff.name}»\n\n" f"Статус: ✅ Включено\n\n" f"Пакеты:\n{packages_display}\n\n" f"Макс. лимит: {max_limit_display}\n\n" "Пользователи смогут докупать трафик по заданным ценам.", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) # ============ УДАЛЕНИЕ ТАРИФА ============ @admin_required @error_handler async def confirm_delete_tariff( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Запрашивает подтверждение удаления тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return subs_count = await get_tariff_subscriptions_count(db, tariff_id) warning = "" if subs_count > 0: warning = f"\n\n⚠️ Внимание! На этом тарифе {subs_count} подписок.\nОни будут отвязаны от тарифа." await callback.message.edit_text( f"🗑️ Удаление тарифа\n\n" f"Вы действительно хотите удалить тариф {tariff.name}?" f"{warning}", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_tariff_delete_confirm:{tariff_id}"), InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_tariff_view:{tariff_id}"), ] ]), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def delete_tariff_confirmed( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Удаляет тариф после подтверждения.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return tariff_name = tariff.name await delete_tariff(db, tariff) await callback.answer(f"Тариф «{tariff_name}» удален", show_alert=True) # Возвращаемся к списку tariffs_data = await get_tariffs_with_subscriptions_count(db, include_inactive=True) if not tariffs_data: await callback.message.edit_text( "📦 Тарифы\n\n" "Тарифы ещё не созданы.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="➕ Создать тариф", callback_data="admin_tariff_create")], [InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_settings")] ]), parse_mode="HTML" ) return total_pages = (len(tariffs_data) + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE page_data = tariffs_data[:ITEMS_PER_PAGE] await callback.message.edit_text( f"📦 Тарифы\n\n" f"✅ Тариф «{tariff_name}» удален\n\n" f"Всего: {len(tariffs_data)}", reply_markup=get_tariffs_list_keyboard(page_data, db_user.language, 0, total_pages), parse_mode="HTML" ) # ============ РЕДАКТИРОВАНИЕ СЕРВЕРОВ ============ @admin_required @error_handler async def start_edit_tariff_squads( callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext, ): """Показывает меню выбора серверов для тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return squads, _ = await get_all_server_squads(db) if not squads: await callback.answer("Нет доступных серверов", show_alert=True) return current_squads = set(tariff.allowed_squads or []) buttons = [] for squad in squads: is_selected = squad.squad_uuid in current_squads prefix = "✅" if is_selected else "⬜" buttons.append([ InlineKeyboardButton( text=f"{prefix} {squad.display_name}", callback_data=f"admin_tariff_toggle_squad:{tariff_id}:{squad.squad_uuid}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_squads:{tariff_id}"), InlineKeyboardButton(text="✅ Выбрать все", callback_data=f"admin_tariff_select_all_squads:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) selected_count = len(current_squads) await callback.message.edit_text( f"🌐 Серверы для тарифа «{tariff.name}»\n\n" f"Выбрано: {selected_count} из {len(squads)}\n\n" "Если не выбран ни один сервер - доступны все.\n" "Нажмите на сервер для выбора/отмены:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def toggle_tariff_squad( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Переключает выбор сервера для тарифа.""" parts = callback.data.split(":") tariff_id = int(parts[1]) squad_uuid = parts[2] tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return current_squads = set(tariff.allowed_squads or []) if squad_uuid in current_squads: current_squads.remove(squad_uuid) else: current_squads.add(squad_uuid) tariff = await update_tariff(db, tariff, allowed_squads=list(current_squads)) # Перерисовываем меню squads, _ = await get_all_server_squads(db) texts = get_texts(db_user.language) buttons = [] for squad in squads: is_selected = squad.squad_uuid in current_squads prefix = "✅" if is_selected else "⬜" buttons.append([ InlineKeyboardButton( text=f"{prefix} {squad.display_name}", callback_data=f"admin_tariff_toggle_squad:{tariff_id}:{squad.squad_uuid}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_squads:{tariff_id}"), InlineKeyboardButton(text="✅ Выбрать все", callback_data=f"admin_tariff_select_all_squads:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) try: await callback.message.edit_text( f"🌐 Серверы для тарифа «{tariff.name}»\n\n" f"Выбрано: {len(current_squads)} из {len(squads)}\n\n" "Если не выбран ни один сервер - доступны все.\n" "Нажмите на сервер для выбора/отмены:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) except TelegramBadRequest: pass await callback.answer() @admin_required @error_handler async def clear_tariff_squads( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Очищает список серверов тарифа.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return tariff = await update_tariff(db, tariff, allowed_squads=[]) await callback.answer("Все серверы очищены") # Перерисовываем меню squads, _ = await get_all_server_squads(db) texts = get_texts(db_user.language) buttons = [] for squad in squads: buttons.append([ InlineKeyboardButton( text=f"⬜ {squad.display_name}", callback_data=f"admin_tariff_toggle_squad:{tariff_id}:{squad.squad_uuid}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_squads:{tariff_id}"), InlineKeyboardButton(text="✅ Выбрать все", callback_data=f"admin_tariff_select_all_squads:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) try: await callback.message.edit_text( f"🌐 Серверы для тарифа «{tariff.name}»\n\n" f"Выбрано: 0 из {len(squads)}\n\n" "Если не выбран ни один сервер - доступны все.\n" "Нажмите на сервер для выбора/отмены:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) except TelegramBadRequest: pass @admin_required @error_handler async def select_all_tariff_squads( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Выбирает все серверы для тарифа.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return squads, _ = await get_all_server_squads(db) all_uuids = [s.squad_uuid for s in squads] tariff = await update_tariff(db, tariff, allowed_squads=all_uuids) await callback.answer("Все серверы выбраны") texts = get_texts(db_user.language) buttons = [] for squad in squads: buttons.append([ InlineKeyboardButton( text=f"✅ {squad.display_name}", callback_data=f"admin_tariff_toggle_squad:{tariff_id}:{squad.squad_uuid}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_squads:{tariff_id}"), InlineKeyboardButton(text="✅ Выбрать все", callback_data=f"admin_tariff_select_all_squads:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) try: await callback.message.edit_text( f"🌐 Серверы для тарифа «{tariff.name}»\n\n" f"Выбрано: {len(squads)} из {len(squads)}\n\n" "Если не выбран ни один сервер - доступны все.\n" "Нажмите на сервер для выбора/отмены:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) except TelegramBadRequest: pass # ============ РЕДАКТИРОВАНИЕ ПРОМОГРУПП ============ @admin_required @error_handler async def start_edit_tariff_promo_groups( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Показывает меню выбора промогрупп для тарифа.""" texts = get_texts(db_user.language) tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return promo_groups_data = await get_promo_groups_with_counts(db) if not promo_groups_data: await callback.answer("Нет промогрупп", show_alert=True) return current_groups = {pg.id for pg in (tariff.allowed_promo_groups or [])} buttons = [] for promo_group, _ in promo_groups_data: is_selected = promo_group.id in current_groups prefix = "✅" if is_selected else "⬜" buttons.append([ InlineKeyboardButton( text=f"{prefix} {promo_group.name}", callback_data=f"admin_tariff_toggle_promo:{tariff_id}:{promo_group.id}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_promo:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) selected_count = len(current_groups) await callback.message.edit_text( f"👥 Промогруппы для тарифа «{tariff.name}»\n\n" f"Выбрано: {selected_count}\n\n" "Если не выбрана ни одна группа - тариф доступен всем.\n" "Выберите группы, которым доступен этот тариф:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def toggle_tariff_promo_group( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Переключает выбор промогруппы для тарифа.""" from app.database.crud.tariff import add_promo_group_to_tariff, remove_promo_group_from_tariff parts = callback.data.split(":") tariff_id = int(parts[1]) promo_group_id = int(parts[2]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return current_groups = {pg.id for pg in (tariff.allowed_promo_groups or [])} if promo_group_id in current_groups: await remove_promo_group_from_tariff(db, tariff, promo_group_id) current_groups.remove(promo_group_id) else: await add_promo_group_to_tariff(db, tariff, promo_group_id) current_groups.add(promo_group_id) # Обновляем тариф из БД tariff = await get_tariff_by_id(db, tariff_id) current_groups = {pg.id for pg in (tariff.allowed_promo_groups or [])} # Перерисовываем меню promo_groups_data = await get_promo_groups_with_counts(db) texts = get_texts(db_user.language) buttons = [] for promo_group, _ in promo_groups_data: is_selected = promo_group.id in current_groups prefix = "✅" if is_selected else "⬜" buttons.append([ InlineKeyboardButton( text=f"{prefix} {promo_group.name}", callback_data=f"admin_tariff_toggle_promo:{tariff_id}:{promo_group.id}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_promo:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) try: await callback.message.edit_text( f"👥 Промогруппы для тарифа «{tariff.name}»\n\n" f"Выбрано: {len(current_groups)}\n\n" "Если не выбрана ни одна группа - тариф доступен всем.\n" "Выберите группы, которым доступен этот тариф:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) except TelegramBadRequest: pass await callback.answer() @admin_required @error_handler async def clear_tariff_promo_groups( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Очищает список промогрупп тарифа.""" from app.database.crud.tariff import set_tariff_promo_groups tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return await set_tariff_promo_groups(db, tariff, []) await callback.answer("Все промогруппы очищены") # Перерисовываем меню promo_groups_data = await get_promo_groups_with_counts(db) texts = get_texts(db_user.language) buttons = [] for promo_group, _ in promo_groups_data: buttons.append([ InlineKeyboardButton( text=f"⬜ {promo_group.name}", callback_data=f"admin_tariff_toggle_promo:{tariff_id}:{promo_group.id}" ) ]) buttons.append([ InlineKeyboardButton(text="🔄 Очистить все", callback_data=f"admin_tariff_clear_promo:{tariff_id}"), ]) buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) try: await callback.message.edit_text( f"👥 Промогруппы для тарифа «{tariff.name}»\n\n" f"Выбрано: 0\n\n" "Если не выбрана ни одна группа - тариф доступен всем.\n" "Выберите группы, которым доступен этот тариф:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), parse_mode="HTML" ) except TelegramBadRequest: pass # ==================== Режим сброса трафика ==================== TRAFFIC_RESET_MODES = [ ('DAY', '📅 Ежедневно', 'Трафик сбрасывается каждый день'), ('WEEK', '📆 Еженедельно', 'Трафик сбрасывается каждую неделю'), ('MONTH', '🗓️ Ежемесячно', 'Трафик сбрасывается каждый месяц'), ('NO_RESET', '🚫 Никогда', 'Трафик не сбрасывается автоматически'), ] def get_traffic_reset_mode_keyboard(tariff_id: int, current_mode: Optional[str], language: str) -> InlineKeyboardMarkup: """Создает клавиатуру для выбора режима сброса трафика.""" texts = get_texts(language) buttons = [] # Кнопка "Глобальная настройка" global_label = f"{'✅ ' if current_mode is None else ''}🌐 Глобальная настройка ({settings.DEFAULT_TRAFFIC_RESET_STRATEGY})" buttons.append([ InlineKeyboardButton( text=global_label, callback_data=f"admin_tariff_set_reset_mode:{tariff_id}:GLOBAL" ) ]) # Кнопки для каждого режима for mode_value, mode_label, mode_desc in TRAFFIC_RESET_MODES: is_selected = current_mode == mode_value label = f"{'✅ ' if is_selected else ''}{mode_label}" buttons.append([ InlineKeyboardButton( text=label, callback_data=f"admin_tariff_set_reset_mode:{tariff_id}:{mode_value}" ) ]) # Кнопка назад buttons.append([ InlineKeyboardButton(text=texts.BACK, callback_data=f"admin_tariff_view:{tariff_id}") ]) return InlineKeyboardMarkup(inline_keyboard=buttons) @admin_required @error_handler async def start_edit_traffic_reset_mode( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Начинает редактирование режима сброса трафика.""" tariff_id = int(callback.data.split(":")[1]) tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return current_mode = getattr(tariff, 'traffic_reset_mode', None) await callback.message.edit_text( f"🔄 Режим сброса трафика для тарифа «{tariff.name}»\n\n" f"Текущий режим: {_format_traffic_reset_mode(current_mode)}\n\n" "Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n" "• Глобальная настройка — использовать значение из конфига бота\n" "• Ежедневно — сброс каждый день\n" "• Еженедельно — сброс каждую неделю\n" "• Ежемесячно — сброс каждый месяц\n" "• Никогда — трафик накапливается за весь период подписки", reply_markup=get_traffic_reset_mode_keyboard(tariff_id, current_mode, db_user.language), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def set_traffic_reset_mode( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): """Устанавливает режим сброса трафика для тарифа.""" parts = callback.data.split(":") tariff_id = int(parts[1]) new_mode = parts[2] tariff = await get_tariff_by_id(db, tariff_id) if not tariff: await callback.answer("Тариф не найден", show_alert=True) return # Преобразуем GLOBAL в None if new_mode == "GLOBAL": new_mode = None # Обновляем тариф tariff = await update_tariff(db, tariff, traffic_reset_mode=new_mode) mode_display = _format_traffic_reset_mode(new_mode) await callback.answer(f"Режим сброса изменён: {mode_display}", show_alert=True) # Обновляем клавиатуру await callback.message.edit_text( f"🔄 Режим сброса трафика для тарифа «{tariff.name}»\n\n" f"Текущий режим: {mode_display}\n\n" "Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n" "• Глобальная настройка — использовать значение из конфига бота\n" "• Ежедневно — сброс каждый день\n" "• Еженедельно — сброс каждую неделю\n" "• Ежемесячно — сброс каждый месяц\n" "• Никогда — трафик накапливается за весь период подписки", reply_markup=get_traffic_reset_mode_keyboard(tariff_id, new_mode, db_user.language), parse_mode="HTML" ) def register_handlers(dp: Dispatcher): """Регистрирует обработчики для управления тарифами.""" # Список тарифов dp.callback_query.register(show_tariffs_list, F.data == "admin_tariffs") dp.callback_query.register(show_tariffs_page, F.data.startswith("admin_tariffs_page:")) # Просмотр и переключение dp.callback_query.register(view_tariff, F.data.startswith("admin_tariff_view:")) dp.callback_query.register(toggle_tariff, F.data.startswith("admin_tariff_toggle:") & ~F.data.startswith("admin_tariff_toggle_trial:")) dp.callback_query.register(toggle_trial_tariff, F.data.startswith("admin_tariff_toggle_trial:")) # Создание тарифа dp.callback_query.register(start_create_tariff, F.data == "admin_tariff_create") dp.message.register(process_tariff_name, AdminStates.creating_tariff_name) dp.message.register(process_tariff_traffic, AdminStates.creating_tariff_traffic) dp.message.register(process_tariff_devices, AdminStates.creating_tariff_devices) dp.message.register(process_tariff_tier, AdminStates.creating_tariff_tier) dp.callback_query.register(select_tariff_type_periodic, F.data == "tariff_type_periodic") dp.callback_query.register(select_tariff_type_daily, F.data == "tariff_type_daily") dp.message.register(process_tariff_prices, AdminStates.creating_tariff_prices) # Редактирование названия dp.callback_query.register(start_edit_tariff_name, F.data.startswith("admin_tariff_edit_name:")) dp.message.register(process_edit_tariff_name, AdminStates.editing_tariff_name) # Редактирование описания dp.callback_query.register(start_edit_tariff_description, F.data.startswith("admin_tariff_edit_desc:")) dp.message.register(process_edit_tariff_description, AdminStates.editing_tariff_description) # Редактирование трафика dp.callback_query.register(start_edit_tariff_traffic, F.data.startswith("admin_tariff_edit_traffic:")) dp.message.register(process_edit_tariff_traffic, AdminStates.editing_tariff_traffic) # Редактирование устройств dp.callback_query.register(start_edit_tariff_devices, F.data.startswith("admin_tariff_edit_devices:")) dp.message.register(process_edit_tariff_devices, AdminStates.editing_tariff_devices) # Редактирование уровня dp.callback_query.register(start_edit_tariff_tier, F.data.startswith("admin_tariff_edit_tier:")) dp.message.register(process_edit_tariff_tier, AdminStates.editing_tariff_tier) # Редактирование цен dp.callback_query.register(start_edit_tariff_prices, F.data.startswith("admin_tariff_edit_prices:")) dp.message.register(process_edit_tariff_prices, AdminStates.editing_tariff_prices) # Редактирование цены за устройство dp.callback_query.register(start_edit_tariff_device_price, F.data.startswith("admin_tariff_edit_device_price:")) dp.message.register(process_edit_tariff_device_price, AdminStates.editing_tariff_device_price) # Редактирование макс. устройств dp.callback_query.register(start_edit_tariff_max_devices, F.data.startswith("admin_tariff_edit_max_devices:")) dp.message.register(process_edit_tariff_max_devices, AdminStates.editing_tariff_max_devices) # Редактирование дней триала dp.callback_query.register(start_edit_tariff_trial_days, F.data.startswith("admin_tariff_edit_trial_days:")) dp.message.register(process_edit_tariff_trial_days, AdminStates.editing_tariff_trial_days) # Редактирование докупки трафика dp.callback_query.register(start_edit_tariff_traffic_topup, F.data.startswith("admin_tariff_edit_traffic_topup:")) dp.callback_query.register(toggle_tariff_traffic_topup, F.data.startswith("admin_tariff_toggle_traffic_topup:")) dp.callback_query.register(start_edit_traffic_topup_packages, F.data.startswith("admin_tariff_edit_topup_packages:")) dp.message.register(process_edit_traffic_topup_packages, AdminStates.editing_tariff_traffic_topup_packages) # Редактирование макс. лимита докупки трафика dp.callback_query.register(start_edit_max_topup_traffic, F.data.startswith("admin_tariff_edit_max_topup:")) dp.message.register(process_edit_max_topup_traffic, AdminStates.editing_tariff_max_topup_traffic) # Удаление dp.callback_query.register(confirm_delete_tariff, F.data.startswith("admin_tariff_delete:")) dp.callback_query.register(delete_tariff_confirmed, F.data.startswith("admin_tariff_delete_confirm:")) # Редактирование серверов dp.callback_query.register(start_edit_tariff_squads, F.data.startswith("admin_tariff_edit_squads:")) dp.callback_query.register(toggle_tariff_squad, F.data.startswith("admin_tariff_toggle_squad:")) dp.callback_query.register(clear_tariff_squads, F.data.startswith("admin_tariff_clear_squads:")) dp.callback_query.register(select_all_tariff_squads, F.data.startswith("admin_tariff_select_all_squads:")) # Редактирование промогрупп dp.callback_query.register(start_edit_tariff_promo_groups, F.data.startswith("admin_tariff_edit_promo:")) dp.callback_query.register(toggle_tariff_promo_group, F.data.startswith("admin_tariff_toggle_promo:")) dp.callback_query.register(clear_tariff_promo_groups, F.data.startswith("admin_tariff_clear_promo:")) # Суточный режим dp.callback_query.register(toggle_daily_tariff, F.data.startswith("admin_tariff_toggle_daily:")) dp.callback_query.register(start_edit_daily_price, F.data.startswith("admin_tariff_edit_daily_price:")) dp.message.register(process_daily_price_input, AdminStates.editing_tariff_daily_price) # Режим сброса трафика dp.callback_query.register(start_edit_traffic_reset_mode, F.data.startswith("admin_tariff_edit_reset_mode:")) dp.callback_query.register(set_traffic_reset_mode, F.data.startswith("admin_tariff_set_reset_mode:"))