diff --git a/app/handlers/admin/tariffs.py b/app/handlers/admin/tariffs.py index 86bd7ba4..436ebe37 100644 --- a/app/handlers/admin/tariffs.py +++ b/app/handlers/admin/tariffs.py @@ -191,6 +191,9 @@ def get_tariff_view_keyboard( InlineKeyboardButton(text="📱💰 Цена за устройство", callback_data=f"admin_tariff_edit_device_price:{tariff.id}"), 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_squads:{tariff.id}"), InlineKeyboardButton(text="👥 Промогруппы", callback_data=f"admin_tariff_edit_promo:{tariff.id}"), @@ -229,6 +232,23 @@ def get_tariff_view_keyboard( return InlineKeyboardMarkup(inline_keyboard=buttons) +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) @@ -264,6 +284,9 @@ def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> st else: device_price_display = "Недоступно" + # Форматируем докупку трафика + traffic_topup_display = _format_traffic_topup_packages(tariff) + return f"""📦 Тариф: {tariff.name} {status} @@ -277,6 +300,9 @@ def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> st • Триал: {trial_status} • Дней триала: {trial_days_display} +Докупка трафика: +{traffic_topup_display} + Цены: {prices_display} @@ -1310,6 +1336,285 @@ async def process_edit_tariff_trial_days( ) +# ============ РЕДАКТИРОВАНИЕ ДОКУПКИ ТРАФИКА ============ + +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 {} + + # Форматируем текущие настройки + 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 = " -" + + 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=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" + "Пользователи смогут докупать трафик по заданным ценам.", + 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 {} + + 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 = " -" + + 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}") + ]) + 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" + "Пользователи смогут докупать трафик по заданным ценам.", + 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())) + + 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=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" + "Пользователи смогут докупать трафик по заданным ценам.", + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + parse_mode="HTML" + ) + + # ============ УДАЛЕНИЕ ТАРИФА ============ @admin_required @@ -1855,6 +2160,12 @@ def register_handlers(dp: Dispatcher): 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(confirm_delete_tariff, F.data.startswith("admin_tariff_delete:")) dp.callback_query.register(delete_tariff_confirmed, F.data.startswith("admin_tariff_delete_confirm:"))