Files
remnawave-bedolaga-telegram…/app/handlers/admin/tariffs.py
2026-01-16 08:36:08 +03:00

2828 lines
104 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Управление тарифами в админ-панели."""
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"<b>💰 Суточная цена:</b> {_format_price_kopeks(daily_price_kopeks)}/день"
tariff_type = "🔄 Суточный"
else:
price_block = f"<b>Цены:</b>\n{prices_display}"
tariff_type = "📅 Периодный"
return f"""📦 <b>Тариф: {tariff.name}</b>
{status} | {tariff_type}
🎚️ Уровень: {tariff.tier_level}
📊 Порядок: {tariff.display_order}
<b>Параметры:</b>
• Трафик: {traffic}
• Устройств: {tariff.device_limit}
• Макс. устройств: {max_devices_display}
• Цена за доп. устройство: {device_price_display}
• Триал: {trial_status}
• Дней триала: {trial_days_display}
<b>Докупка трафика:</b>
{traffic_topup_display}
<b>Сброс трафика:</b> {traffic_reset_display}
{price_block}
<b>Серверы:</b> {squads_display}
<b>Промогруппы:</b> {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(
"⚠️ <b>Режим тарифов отключен</b>\n\n"
"Для использования тарифов установите:\n"
"<code>SALES_MODE=tariffs</code>\n\n"
"Текущий режим: <code>classic</code>",
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(
"📦 <b>Тарифы</b>\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"📦 <b>Тарифы</b>\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"📦 <b>Тарифы</b> (стр. {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"💰 <b>Редактирование суточной цены</b>\n\n"
f"Тариф: {tariff.name}\n"
f"Текущая цена: {_format_price_kopeks(current_price)}/день\n\n"
"Введите новую цену за день в рублях.\n"
"Пример: <code>50</code> или <code>99.90</code>",
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"
"Пример: <code>50</code> или <code>99.90</code>",
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"✅ <b>Суточный тариф создан!</b>\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(
"📦 <b>Создание тарифа</b>\n\n"
"Шаг 1/6: Введите название тарифа\n\n"
"Пример: <i>Базовый</i>, <i>Премиум</i>, <i>Бизнес</i>",
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(
"📦 <b>Создание тарифа</b>\n\n"
f"Название: <b>{name}</b>\n\n"
"Шаг 2/6: Введите лимит трафика в ГБ\n\n"
"Введите <code>0</code> для безлимитного трафика\n"
"Пример: <i>100</i>, <i>500</i>, <i>0</i>",
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(
"📦 <b>Создание тарифа</b>\n\n"
f"Название: <b>{data['tariff_name']}</b>\n"
f"Трафик: <b>{traffic_display}</b>\n\n"
"Шаг 3/6: Введите лимит устройств\n\n"
"Пример: <i>1</i>, <i>3</i>, <i>5</i>",
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(
"📦 <b>Создание тарифа</b>\n\n"
f"Название: <b>{data['tariff_name']}</b>\n"
f"Трафик: <b>{traffic_display}</b>\n"
f"Устройств: <b>{devices}</b>\n\n"
"Шаг 4/6: Введите уровень тарифа (1-10)\n\n"
"Уровень используется для визуального отображения\n"
"1 - базовый, 10 - максимальный\n"
"Пример: <i>1</i>, <i>2</i>, <i>3</i>",
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(
"📦 <b>Создание тарифа</b>\n\n"
f"Название: <b>{data['tariff_name']}</b>\n"
f"Трафик: <b>{traffic_display}</b>\n"
f"Устройств: <b>{data['tariff_devices']}</b>\n"
f"Уровень: <b>{tier}</b>\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(
"📦 <b>Создание тарифа</b>\n\n"
f"Название: <b>{data['tariff_name']}</b>\n"
f"Трафик: <b>{traffic_display}</b>\n"
f"Устройств: <b>{data['tariff_devices']}</b>\n"
f"Уровень: <b>{data['tariff_tier']}</b>\n"
f"Тип: <b>📅 Периодный</b>\n\n"
"Шаг 6/6: Введите цены на периоды\n\n"
"Формат: <code>дней:цена_в_копейках</code>\n"
"Несколько периодов через запятую\n\n"
"Пример:\n<code>30:9900, 90:24900, 180:44900, 360:79900</code>",
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(
"📦 <b>Создание суточного тарифа</b>\n\n"
f"Название: <b>{data['tariff_name']}</b>\n"
f"Трафик: <b>{traffic_display}</b>\n"
f"Устройств: <b>{data['tariff_devices']}</b>\n"
f"Уровень: <b>{data['tariff_tier']}</b>\n"
f"Тип: <b>🔄 Суточный</b>\n\n"
"Шаг 6/6: Введите суточную цену в рублях\n\n"
"Пример: <i>50</i> (50 ₽/день), <i>99.90</i> (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"
"Формат: <code>дней:цена_в_копейках</code>\n"
"Пример: <code>30:9900, 90:24900</code>",
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"✅ <b>Тариф создан!</b>\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"✏️ <b>Редактирование названия</b>\n\n"
f"Текущее название: <b>{tariff.name}</b>\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"📝 <b>Редактирование описания</b>\n\n"
f"Текущее описание:\n{current_desc}\n\n"
"Введите новое описание (или <code>-</code> для удаления):",
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"📊 <b>Редактирование трафика</b>\n\n"
f"Текущий лимит: <b>{current_traffic}</b>\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"📱 <b>Редактирование устройств</b>\n\n"
f"Текущий лимит: <b>{tariff.device_limit}</b>\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"🎚️ <b>Редактирование уровня</b>\n\n"
f"Текущий уровень: <b>{tariff.tier_level}</b>\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"💰 <b>Редактирование цен</b>\n\n"
f"Текущие цены:\n{prices_display}\n\n"
"Введите новые цены в формате:\n"
f"<code>{current_prices}</code>\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"
"Формат: <code>дней:цена</code>\n"
"Пример: <code>30:9900, 90:24900</code>",
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"📱💰 <b>Редактирование цены за устройство</b>\n\n"
f"Текущая цена: <b>{current_price}</b>\n\n"
"Введите цену в копейках за одно устройство в месяц.\n\n"
"• <code>0</code> или <code>-</code> — докупка устройств недоступна\n"
"• Например: <code>5000</code> = 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"
"Для отключения докупки введите <code>0</code> или <code>-</code>",
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"📱🔒 <b>Редактирование макс. устройств</b>\n\n"
f"Текущее значение: <b>{current_max}</b>\n"
f"Базовое кол-во устройств: <b>{tariff.device_limit}</b>\n\n"
"Введите максимальное количество устройств, которое пользователь может докупить.\n\n"
"• <code>0</code> или <code>-</code> — без ограничений\n"
"• Например: <code>5</code> = максимум 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"
"Для снятия ограничения введите <code>0</code> или <code>-</code>",
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"⏰ <b>Редактирование дней триала</b>\n\n"
f"Текущее значение: <b>{current_days}</b>\n\n"
"Введите количество дней триала.\n\n"
f"• <code>0</code> или <code>-</code> — использовать настройку по умолчанию ({settings.TRIAL_DURATION_DAYS} дней)\n"
"• Например: <code>7</code> = 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"
"Для использования настройки по умолчанию введите <code>0</code> или <code>-</code>",
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"📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n"
f"Статус: {status}\n\n"
f"<b>Пакеты:</b>\n{packages_display}\n\n"
f"<b>Макс. лимит:</b> {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"📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n"
f"Статус: {status}\n\n"
f"<b>Пакеты:</b>\n{packages_display}\n\n"
f"<b>Макс. лимит:</b> {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"📦 <b>Настройка пакетов докупки трафика</b>\n\n"
f"Тариф: <b>{tariff.name}</b>\n\n"
f"<b>Текущие пакеты:</b>\n{packages_display}\n\n"
"Введите пакеты в формате:\n"
f"<code>{current_packages}</code>\n\n"
"(ГБ:цена_в_копейках, через запятую)\n"
"Например: <code>5:5000, 10:9000</code> = 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"
"Формат: <code>ГБ:цена_в_копейках</code>\n"
"Пример: <code>5:5000, 10:9000, 20:15000</code>",
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"✅ <b>Пакеты обновлены!</b>\n\n"
f"📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n"
f"Статус: ✅ Включено\n\n"
f"<b>Пакеты:</b>\n{packages_display}\n\n"
f"<b>Макс. лимит:</b> {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"📊 <b>Максимальный лимит трафика</b>\n\n"
f"Тариф: <b>{tariff.name}</b>\n"
f"Текущий лимит: <b>{current_display}</b>\n\n"
f"Введите максимальный общий объем трафика (в ГБ), который может быть на подписке после всех докупок.\n\n"
f"• Например, если тариф дает 100 ГБ и лимит 200 ГБ — пользователь сможет докупить еще 100 ГБ\n"
f"• Введите <code>0</code> для снятия ограничения",
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"
"• <code>0</code> — без ограничений\n"
"• <code>200</code> — максимум 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"✅ <b>Лимит обновлен!</b>\n\n"
f"📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n"
f"Статус: ✅ Включено\n\n"
f"<b>Пакеты:</b>\n{packages_display}\n\n"
f"<b>Макс. лимит:</b> {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⚠️ <b>Внимание!</b> На этом тарифе {subs_count} подписок.\nОни будут отвязаны от тарифа."
await callback.message.edit_text(
f"🗑️ <b>Удаление тарифа</b>\n\n"
f"Вы действительно хотите удалить тариф <b>{tariff.name}</b>?"
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(
"📦 <b>Тарифы</b>\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"📦 <b>Тарифы</b>\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"🌐 <b>Серверы для тарифа «{tariff.name}»</b>\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"🌐 <b>Серверы для тарифа «{tariff.name}»</b>\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"🌐 <b>Серверы для тарифа «{tariff.name}»</b>\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"🌐 <b>Серверы для тарифа «{tariff.name}»</b>\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"👥 <b>Промогруппы для тарифа «{tariff.name}»</b>\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"👥 <b>Промогруппы для тарифа «{tariff.name}»</b>\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"👥 <b>Промогруппы для тарифа «{tariff.name}»</b>\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"🔄 <b>Режим сброса трафика для тарифа «{tariff.name}»</b>\n\n"
f"Текущий режим: {_format_traffic_reset_mode(current_mode)}\n\n"
"Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n"
"• <b>Глобальная настройка</b> — использовать значение из конфига бота\n"
"• <b>Ежедневно</b> — сброс каждый день\n"
"• <b>Еженедельно</b> — сброс каждую неделю\n"
"• <b>Ежемесячно</b> — сброс каждый месяц\n"
"• <b>Никогда</b> — трафик накапливается за весь период подписки",
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"🔄 <b>Режим сброса трафика для тарифа «{tariff.name}»</b>\n\n"
f"Текущий режим: {mode_display}\n\n"
"Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n"
"• <b>Глобальная настройка</b> — использовать значение из конфига бота\n"
"• <b>Ежедневно</b> — сброс каждый день\n"
"• <b>Еженедельно</b> — сброс каждую неделю\n"
"• <b>Ежемесячно</b> — сброс каждый месяц\n"
"• <b>Никогда</b> — трафик накапливается за весь период подписки",
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:"))