import re from datetime import datetime from aiogram import F from aiogram.fsm.context import FSMContext from aiogram.types import ( CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, ) from sqlalchemy import delete, distinct, select, update from sqlalchemy.ext.asyncio import AsyncSession from database import create_tariff from database.models import Gift, Key, Server, Tariff from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback from handlers.buttons import BACK from . import router from .keyboard import ( AdminTariffCallback, build_cancel_kb, build_edit_tariff_fields_kb, build_tariff_groups_kb, build_tariff_list_kb, build_tariff_menu_kb, ) from .tariff_states import TariffCreateState, TariffEditState from .tariff_utils import render_tariff_card, validate_tariff_name @router.callback_query(AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter()) async def handle_tariff_menu(callback_query: CallbackQuery): text = ( "💸 Управление тарифами\n\n" "Вы можете выполнить следующие действия:\n\n" "🆕 Создать тариф\n" "
• Установите длительность (в днях)\n" "• Задайте цену (в рублях)\n" "• Задайте лимит устройств (hwid/ip_limit)\n" "• Укажите лимит трафика (в ГБ)\n\n" "📋 Редактировать тарифы\n" "
• Просматривайте список текущих тарифов\n" "• Изменяйте параметры или удаляйте при необходимости" ) await callback_query.message.edit_text(text=text, reply_markup=build_tariff_menu_kb()) @router.callback_query(AdminTariffCallback.filter(F.action == "create"), IsAdminFilter()) async def start_tariff_creation(callback: CallbackQuery, state: FSMContext): await state.set_state(TariffCreateState.group) await callback.message.edit_text( "📁 Введите код группы, в которую вы хотите добавить тариф.\n\n" "Например:
basic, vip, business\n\n"
"Специальные группы:\n"
"• discounts — тарифы со скидкой\n"
"• discounts_max — тарифы с максимальной скидкой\n"
"• gifts — тарифы для подарков\n"
"• trial — тариф для пробного периода",
reply_markup=build_cancel_kb(),
)
@router.message(TariffCreateState.group, IsAdminFilter())
async def process_tariff_group(message: Message, state: FSMContext):
group_code = message.text.strip().lower()
if not re.fullmatch(r"[a-z0-9_-]+", group_code):
await message.answer(
"❌ Код группы должен содержать только латинские буквы, цифры, дефисы и подчёркивания.\n\nПовторите ввод:",
reply_markup=build_cancel_kb(),
)
return
await state.update_data(group_code=group_code)
await state.set_state(TariffCreateState.name)
await message.answer(
"📝 Введите название тарифа\n\n"
"Например: 30 дней или 1 месяц\n\n"
"Это название будет отображаться пользователю при выборе тарифа",
reply_markup=build_cancel_kb(),
)
@router.message(TariffCreateState.name, IsAdminFilter())
async def process_tariff_name(message: Message, state: FSMContext):
name = message.text.strip()
is_valid, error_msg = validate_tariff_name(name)
if not is_valid:
await message.answer(
f"❌ {error_msg}\n\nПовторите ввод:",
reply_markup=build_cancel_kb(),
)
return
await state.update_data(name=name)
await state.set_state(TariffCreateState.duration)
await message.answer(
"📅 Введите длительность тарифа в днях (например: 30):",
reply_markup=build_cancel_kb(),
)
@router.message(TariffCreateState.duration, IsAdminFilter())
async def process_tariff_duration(message: Message, state: FSMContext):
try:
days = int(message.text.strip())
if days <= 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректное количество дней (целое число больше 0):")
return
await state.update_data(duration_days=days)
await state.set_state(TariffCreateState.price)
await message.answer(
"💰 Введите цену тарифа в рублях (например: 150)\n\n"
"Будет показано клиенту при выборе тарифа",
reply_markup=build_cancel_kb(),
)
@router.message(TariffCreateState.price, IsAdminFilter())
async def process_tariff_price(message: Message, state: FSMContext):
try:
price = int(message.text.strip())
if price < 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректную цену (целое число 0 или больше):")
return
await state.update_data(price_rub=price)
await state.set_state(TariffCreateState.traffic)
await message.answer(
"📦 Введите лимит трафика в ГБ (например: 100, 0 — безлимит):",
reply_markup=build_cancel_kb(),
)
@router.message(TariffCreateState.traffic, IsAdminFilter())
async def process_tariff_traffic(message: Message, state: FSMContext):
try:
traffic = int(message.text.strip())
if traffic < 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректный лимит трафика (целое число 0 или больше):")
return
await state.update_data(traffic_limit=traffic if traffic > 0 else None)
await state.set_state(TariffCreateState.device_limit)
await message.answer(
"📱 Введите лимит устройств (HWID) для тарифа (например: 3, 0 — безлимит):",
reply_markup=build_cancel_kb(),
)
@router.message(TariffCreateState.device_limit, IsAdminFilter())
async def process_tariff_device_limit(message: Message, state: FSMContext):
try:
device_limit = int(message.text.strip())
if device_limit < 0:
raise ValueError
except ValueError:
await message.answer("❌ Введите корректный лимит устройств (целое число 0 или больше):")
return
await state.update_data(device_limit=device_limit if device_limit > 0 else None)
await state.set_state(TariffCreateState.vless)
await message.answer(
"🔗 Этот тариф для выдачи VLESS (конфигурация для роутера)?",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да (VLESS)", callback_data="create_vless|1"),
InlineKeyboardButton(text="❌ Нет", callback_data="create_vless|0"),
],
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_tariff_creation")],
]
),
)
@router.callback_query(F.data.startswith("create_vless|"), TariffCreateState.vless, IsAdminFilter())
async def select_vless_creation(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
_, flag = callback.data.split("|", 1)
vless_flag = flag == "1"
data = await state.get_data()
new_tariff = await create_tariff(
session,
{
"name": data["name"],
"group_code": data["group_code"],
"duration_days": data["duration_days"],
"price_rub": data["price_rub"],
"traffic_limit": data["traffic_limit"],
"device_limit": data.get("device_limit"),
"vless": vless_flag,
},
)
await state.set_state(TariffCreateState.confirm_more)
await callback.message.edit_text(
f"✅ Тариф {new_tariff.name} добавлен в группу {data['group_code']}.\n\n"
"➕ Хотите добавить ещё один тариф в эту группу?",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data="add_more_tariff"),
InlineKeyboardButton(text="❌ Нет", callback_data="done_tariff_group"),
]
]
),
)
@router.callback_query(F.data == "add_more_tariff", IsAdminFilter())
async def handle_add_more_tariff(callback: CallbackQuery, state: FSMContext):
await state.set_state(TariffCreateState.name)
await callback.message.edit_text("📝 Введите название следующего тарифа:", reply_markup=build_cancel_kb())
@router.callback_query(F.data == "done_tariff_group", IsAdminFilter())
async def handle_done_tariff_group(callback: CallbackQuery, state: FSMContext):
await state.clear()
await callback.message.edit_text("✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb())
@router.callback_query(F.data == "cancel_tariff_creation", IsAdminFilter())
async def cancel_tariff_creation(callback: CallbackQuery, state: FSMContext):
await state.clear()
await callback.message.edit_text("❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb())
@router.callback_query(AdminTariffCallback.filter(F.action == "list"), IsAdminFilter())
async def show_tariff_groups(callback: CallbackQuery, session: AsyncSession):
result = await session.execute(
select(distinct(Tariff.group_code)).where(Tariff.group_code.isnot(None)).order_by(Tariff.group_code)
)
groups = [row[0] for row in result.fetchall()]
if not groups:
await callback.message.edit_text("❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb())
return
special_groups = {
"discounts": "🔻 Скидки",
"discounts_max": "🔻 Макс. скидки",
"gifts": "🎁 Подарки",
"trial": "🚀 Пробный период",
}
text = "📋 Выберите тарифную группу:\n\n"
text += "Специальные группы:\n"
for code, label in special_groups.items():
status = "✅ создана" if code in groups else "❌ не создана"
text += f"{label} — {code} — {status}\n"
text += "\n"
await callback.message.edit_text(text, reply_markup=build_tariff_groups_kb(groups))
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter())
async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession):
from database.tariffs import get_tariffs
from .tariff_utils import tariff_to_dict
group_code = callback_data.action.split("|")[1]
tariffs = await get_tariffs(session, group_code=group_code)
if not tariffs:
await callback.message.edit_text("❌ В этой группе пока нет тарифов.")
return
tariff_dicts = [tariff_to_dict(t) for t in tariffs]
await callback.message.edit_text(
f"📦 Тарифы группы: {group_code}",
reply_markup=build_tariff_list_kb(tariff_dicts),
)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter())
async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession):
tariff_id = int(callback_data.action.split("|")[1])
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
text, markup = render_tariff_card(tariff)
await callback.message.edit_text(text=text, reply_markup=markup)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter())
async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession):
tariff_id = int(callback_data.action.split("|")[1])
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
group_code = tariff.group_code
if group_code == "gifts":
gift_check = await session.execute(select(Gift).where(Gift.tariff_id == tariff_id).limit(1))
if gift_check.scalar_one_or_none():
result = await session.execute(select(Tariff).where(Tariff.group_code == "gifts", Tariff.id != tariff_id))
other_tariffs = result.scalars().all()
if not other_tariffs:
await callback.message.edit_text(
"❌ Нельзя удалить тариф — он используется в подарках, а других тарифов в группе 'gifts' нет.",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=BACK, callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
)
]
]
),
)
return
builder = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=f"{t.name} — {t.price_rub}₽",
callback_data=f"confirm_delete_tariff_with_replace|{tariff_id}|{t.id}",
)
]
for t in other_tariffs
]
+ [
[
InlineKeyboardButton(
text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
)
]
]
)
await callback.message.edit_text(
"Этот тариф используется в подарках.\n\n"
"Выберите тариф, на который заменить его во всех подарках перед удалением:",
reply_markup=builder,
)
return
await callback.message.edit_text(
"⚠️ Вы уверены, что хотите удалить этот тариф?",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"),
InlineKeyboardButton(
text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
),
]
]
),
)
@router.callback_query(F.data.startswith("confirm_delete_tariff_with_replace|"), IsAdminFilter())
async def delete_tariff_with_gift_replacement(callback: CallbackQuery, session: AsyncSession):
_, tariff_id_str, replacement_id_str = callback.data.split("|")
tariff_id = int(tariff_id_str)
replacement_id = int(replacement_id_str)
await session.execute(update(Gift).where(Gift.tariff_id == tariff_id).values(tariff_id=replacement_id))
await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None))
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
group_code = tariff.group_code
await session.execute(delete(Tariff).where(Tariff.id == tariff_id))
result = await session.execute(select(Tariff).where(Tariff.group_code == group_code))
remaining_tariffs = result.scalars().all()
if not remaining_tariffs:
await session.execute(update(Server).where(Server.tariff_group == group_code).values(tariff_group=None))
await session.commit()
await callback.message.edit_text("🗑 Тариф удалён. Все подарки обновлены.", reply_markup=build_tariff_menu_kb())
@router.callback_query(F.data.startswith("confirm_delete_tariff|"), IsAdminFilter())
async def delete_tariff(callback: CallbackQuery, session: AsyncSession):
tariff_id = int(callback.data.split("|", 1)[1])
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
group_code = tariff.group_code
await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None))
await session.execute(delete(Tariff).where(Tariff.id == tariff_id))
result = await session.execute(select(Tariff).where(Tariff.group_code == group_code))
remaining_tariffs = result.scalars().all()
if not remaining_tariffs:
await session.execute(update(Server).where(Server.tariff_group == group_code).values(tariff_group=None))
await session.commit()
await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb())
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter())
async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext):
tariff_id = int(callback_data.action.split("|")[1])
await state.update_data(tariff_id=tariff_id)
await state.set_state(TariffEditState.choosing_field)
await callback.message.edit_text(
"✏️ Что вы хотите изменить?",
reply_markup=build_edit_tariff_fields_kb(tariff_id),
)
@router.callback_query(F.data.startswith("edit_field|"), IsAdminFilter())
async def ask_new_value(callback: CallbackQuery, state: FSMContext):
_, _tariff_id, field = callback.data.split("|")
await state.update_data(field=field)
await state.set_state(TariffEditState.editing_value)
if field == "vless":
data = await state.get_data()
tariff_id = int(data["tariff_id"])
await callback.message.edit_text(
"🔗 Установить флаг VLESS:",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да (VLESS)", callback_data=f"set_vless|{tariff_id}|1"),
InlineKeyboardButton(text="❌ Нет", callback_data=f"set_vless|{tariff_id}|0"),
],
[
InlineKeyboardButton(
text=BACK,
callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack(),
)
],
]
),
)
return
field_names = {
"name": "название тарифа",
"duration_days": "длительность в днях",
"price_rub": "цену в рублях",
"traffic_limit": "лимит трафика в ГБ (0 — безлимит)",
"device_limit": "лимит устройств (0 — безлимит)",
"vless": "VLESS (да/нет)",
"external_squad": "внешний сквад (0 — убрать)",
}
await callback.message.edit_text(
f"✏️ Введите новое значение для {field_names.get(field, field)}:",
reply_markup=build_cancel_kb(),
)
@router.callback_query(F.data.startswith("set_vless|"), TariffEditState.editing_value, IsAdminFilter())
async def set_vless_flag(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
_, tariff_id_str, flag = callback.data.split("|", 2)
tariff_id = int(tariff_id_str)
vless_flag = flag == "1"
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
await state.clear()
return
tariff.vless = vless_flag
tariff.updated_at = datetime.utcnow()
await session.commit()
await state.clear()
text, markup = render_tariff_card(tariff)
await callback.message.edit_text(text=text, reply_markup=markup)
@router.message(TariffEditState.editing_value, IsAdminFilter())
async def apply_edit(message: Message, state: FSMContext, session: AsyncSession):
data = await state.get_data()
tariff_id = data["tariff_id"]
field = data["field"]
value = message.text.strip()
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await message.answer("❌ Тариф не найден.")
await state.clear()
return
if field == "name":
is_valid, error_msg = validate_tariff_name(value)
if not is_valid:
await message.answer(
f"❌ {error_msg}\n\nПовторите ввод:",
reply_markup=build_cancel_kb(),
)
return
if field == "external_squad":
if value in ("", "0", "-"):
value = None
setattr(tariff, field, value)
tariff.updated_at = datetime.utcnow()
await session.commit()
await state.clear()
text, markup = render_tariff_card(tariff)
await message.answer(text=text, reply_markup=markup)
return
if field in ["duration_days", "price_rub", "traffic_limit", "device_limit"]:
try:
num = int(value)
if num < 0:
raise ValueError
if field in ["traffic_limit", "device_limit"]:
value = num if num > 0 else None
else:
value = num
except ValueError:
await message.answer("❌ Введите корректное число.")
return
setattr(tariff, field, value)
tariff.updated_at = datetime.utcnow()
await session.commit()
await state.clear()
text, markup = render_tariff_card(tariff)
await message.answer(text=text, reply_markup=markup)
@router.callback_query(F.data.startswith("toggle_active|"), IsAdminFilter())
async def toggle_tariff_status(callback: CallbackQuery, session: AsyncSession):
tariff_id = int(callback.data.split("|")[1])
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
tariff.is_active = not tariff.is_active
await session.commit()
text, markup = render_tariff_card(tariff)
await callback.message.edit_text(text=text, reply_markup=markup)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter())
async def start_tariff_creation_existing_group(
callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext
):
group_code = callback_data.action.split("|", 1)[1]
await state.update_data(group_code=group_code)
await state.set_state(TariffCreateState.name)
await callback.message.edit_text(
f"📦 Добавление нового тарифа в группу {group_code}\n\n📝 Введите название тарифа:",
reply_markup=build_cancel_kb(),
)
@router.callback_query(F.data.startswith("toggle_configurable|"), IsAdminFilter())
async def toggle_tariff_configurable(callback: CallbackQuery, session: AsyncSession):
tariff_id = int(callback.data.split("|")[1])
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
current = bool(tariff.configurable)
tariff.configurable = not current
tariff.updated_at = datetime.utcnow()
await session.commit()
text, markup = render_tariff_card(tariff)
await callback.message.edit_text(text=text, reply_markup=markup)