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:"))