From fc0079770dc0a2e1c2830e47e2989bc18aaa251c Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Mon, 23 Jun 2025 20:57:07 +0300 Subject: [PATCH] tariff subgroups use hash callbacks / only ungrouped tariffs when creating / add subgroup management --- database/tariffs.py | 25 ++ handlers/admin/gifts/gifts_handler.py | 12 +- handlers/admin/tariffs/keyboard.py | 5 +- handlers/admin/tariffs/tariffs_handler.py | 356 ++++++++++++++++++++-- handlers/keys/key_mode/key_create.py | 32 +- handlers/keys/key_renew.py | 14 +- 6 files changed, 414 insertions(+), 30 deletions(-) diff --git a/database/tariffs.py b/database/tariffs.py index 143592af..2e867b70 100644 --- a/database/tariffs.py +++ b/database/tariffs.py @@ -1,4 +1,5 @@ from datetime import datetime +import hashlib from sqlalchemy import delete, insert, select, update from sqlalchemy.exc import SQLAlchemyError @@ -8,6 +9,30 @@ from database.models import Server, Tariff from logger import logger +def create_subgroup_hash(subgroup_title: str, group_code: str) -> str: + if not subgroup_title: + return "" + + unique_key = f"{subgroup_title}:{group_code}" + hash_object = hashlib.md5(unique_key.encode('utf-8')) + return hash_object.hexdigest()[:8] + + +async def find_subgroup_by_hash(session: AsyncSession, subgroup_hash: str, group_code: str) -> str | None: + result = await session.execute( + select(Tariff.subgroup_title) + .where(Tariff.group_code == group_code, Tariff.subgroup_title.isnot(None)) + .distinct() + ) + subgroups = [row[0] for row in result.fetchall()] + + for subgroup_title in subgroups: + if create_subgroup_hash(subgroup_title, group_code) == subgroup_hash: + return subgroup_title + + return None + + async def get_tariffs( session: AsyncSession, tariff_id: int = None, group_code: str = None ): diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py index c6fb0c85..ecd2893d 100644 --- a/handlers/admin/gifts/gifts_handler.py +++ b/handlers/admin/gifts/gifts_handler.py @@ -10,6 +10,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from ..panel.keyboard import AdminPanelCallback from .keyboard import build_admin_gifts_kb, build_gifts_list_kb from database.models import Tariff, Gift, GiftUsage +from database.tariffs import create_subgroup_hash, find_subgroup_by_hash from handlers.utils import format_days, format_months, edit_or_send_message from logger import logger @@ -66,11 +67,11 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession ) for subgroup in sorted(k for k in grouped_tariffs if k): - safe = subgroup.replace(" ", "_")[:30] + subgroup_hash = create_subgroup_hash(subgroup, "gifts") builder.row( types.InlineKeyboardButton( text=subgroup, - callback_data=f"admin_gift_subgroup|{safe}", + callback_data=f"admin_gift_subgroup|{subgroup_hash}", ) ) @@ -89,7 +90,12 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession @router.callback_query(F.data.startswith("admin_gift_subgroup|")) async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session: AsyncSession): try: - subgroup = callback.data.split("|", 1)[1].replace("_", " ") + subgroup_hash = callback.data.split("|", 1)[1] + + subgroup = await find_subgroup_by_hash(session, subgroup_hash, "gifts") + if not subgroup: + await callback.message.edit_text("❌ Подгруппа не найдена.") + return stmt = ( select(Tariff) diff --git a/handlers/admin/tariffs/keyboard.py b/handlers/admin/tariffs/keyboard.py index 3c4518ec..22f55df8 100644 --- a/handlers/admin/tariffs/keyboard.py +++ b/handlers/admin/tariffs/keyboard.py @@ -3,6 +3,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from collections import defaultdict +from database.tariffs import create_subgroup_hash from ..panel.keyboard import AdminPanelCallback @@ -82,11 +83,11 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup: for subgroup_title, items in grouped.items(): if subgroup_title: - safe_subgroup = subgroup_title.replace(" ", "_") + subgroup_hash = create_subgroup_hash(subgroup_title, group_code) builder.row( InlineKeyboardButton( text=f"{subgroup_title}", - callback_data=f"view_subgroup|{safe_subgroup}|{group_code}" + callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" ) ) diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py index 4c5e8b49..a27b9aae 100644 --- a/handlers/admin/tariffs/tariffs_handler.py +++ b/handlers/admin/tariffs/tariffs_handler.py @@ -10,11 +10,12 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder -from sqlalchemy import delete, distinct, select, update +from sqlalchemy import delete, distinct, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from database import create_tariff from database.models import Key, Server, Tariff +from database.tariffs import create_subgroup_hash, find_subgroup_by_hash from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback @@ -54,6 +55,23 @@ class TariffSubgroupState(StatesGroup): class SubgroupEditState(StatesGroup): entering_new_title = State() confirming_deletion = State() + editing_tariffs = State() + + +MAX_TARIFF_NAME_LENGTH = 40 +MAX_SUBGROUP_TITLE_LENGTH = 40 + + +def validate_tariff_name(name: str) -> tuple[bool, str]: + if len(name) > MAX_TARIFF_NAME_LENGTH: + return False, f"Название тарифа слишком длинное. Максимум {MAX_TARIFF_NAME_LENGTH} символов." + return True, "" + + +def validate_subgroup_title(title: str) -> tuple[bool, str]: + if len(title) > MAX_SUBGROUP_TITLE_LENGTH: + return False, f"Название подгруппы слишком длинное. Максимум {MAX_SUBGROUP_TITLE_LENGTH} символов." + return True, "" @router.callback_query( @@ -108,7 +126,17 @@ async def process_tariff_group(message: Message, state: FSMContext): @router.message(TariffCreateState.name, IsAdminFilter()) async def process_tariff_name(message: Message, state: FSMContext): - await state.update_data(name=message.text.strip()) + 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):", @@ -441,6 +469,15 @@ async def apply_edit(message: Message, state: FSMContext, session: AsyncSession) 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 in ["duration_days", "price_rub", "traffic_limit", "device_limit"]: try: num = int(value) @@ -523,12 +560,27 @@ async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: group_code = callback.data.split("|", 1)[1] result = await session.execute( - select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id) + select(Tariff) + .where( + Tariff.group_code == group_code, + (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == "")) + ) + .order_by(Tariff.id) ) tariffs = result.scalars().all() if not tariffs: - await callback.message.edit_text("❌ В этой группе нет тарифов.") + await callback.message.edit_text( + "❌ Нет доступных тарифов для группировки.\n\nВсе тарифы уже находятся в подгруппах.", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton( + text="⬅️ Назад", + callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + )] + ] + ) + ) return await state.set_state(TariffSubgroupState.selecting_tariffs) @@ -572,7 +624,12 @@ async def toggle_tariff_subgroup_selection(callback: CallbackQuery, state: FSMCo group_code = data["group_code"] result = await session.execute( - select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id) + select(Tariff) + .where( + Tariff.group_code == group_code, + (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == "")) + ) + .order_by(Tariff.id) ) tariffs = result.scalars().all() @@ -623,6 +680,19 @@ async def ask_subgroup_title(callback: CallbackQuery, state: FSMContext): @router.message(TariffSubgroupState.entering_subgroup_title, IsAdminFilter()) async def apply_subgroup_title(message: Message, state: FSMContext, session: AsyncSession): title = message.text.strip() + + is_valid, error_msg = validate_subgroup_title(title) + if not is_valid: + await message.answer( + f"❌ {error_msg}\n\nПовторите ввод:", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")] + ] + ), + ) + return + data = await state.get_data() selected_ids = data.get("selected_tariff_ids", []) @@ -650,8 +720,13 @@ async def cancel_subgrouping(callback: CallbackQuery, state: FSMContext): @router.callback_query(F.data.startswith("view_subgroup|"), IsAdminFilter()) async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): - _, encoded_subgroup, group_code = callback.data.split("|", 2) - subgroup_title = encoded_subgroup.replace("_", " ") + _, subgroup_hash, group_code = callback.data.split("|", 2) + + subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) + + if not subgroup_title: + await callback.message.edit_text("❌ Подгруппа не найдена.") + return result = await session.execute( select(Tariff).where( @@ -680,13 +755,19 @@ async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): builder.row( InlineKeyboardButton( text="📝 Переименовать подгруппу", - callback_data=f"rename_subgroup|{encoded_subgroup}|{group_code}", + callback_data=f"rename_subgroup|{subgroup_hash}|{group_code}", + ) + ) + builder.row( + InlineKeyboardButton( + text="✏️ Редактировать подгруппу", + callback_data=f"edit_subgroup_tariffs|{subgroup_hash}|{group_code}", ) ) builder.row( InlineKeyboardButton( text="🗑 Удалить подгруппу", - callback_data=f"delete_subgroup|{encoded_subgroup}|{group_code}", + callback_data=f"delete_subgroup|{subgroup_hash}|{group_code}", ) ) @@ -704,13 +785,19 @@ async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): @router.callback_query(F.data.startswith("rename_subgroup|"), IsAdminFilter()) -async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext): - _, encoded_subgroup, group_code = callback.data.split("|", 2) - subgroup_title = encoded_subgroup.replace("_", " ") +async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext, session: AsyncSession): + _, subgroup_hash, group_code = callback.data.split("|", 2) + + subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) + + if not subgroup_title: + await callback.message.edit_text("❌ Подгруппа не найдена.") + return await state.update_data( subgroup_title=subgroup_title, group_code=group_code, + subgroup_hash=subgroup_hash, ) await state.set_state(SubgroupEditState.entering_new_title) @@ -718,7 +805,7 @@ async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext): f"📝 Введите новое название подгруппы:\n{subgroup_title}\n\nИли нажмите Отмена.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{encoded_subgroup}|{group_code}")] + [InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] ] ) ) @@ -727,6 +814,23 @@ async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext): @router.message(SubgroupEditState.entering_new_title, IsAdminFilter()) async def save_new_subgroup_title(message: Message, state: FSMContext, session: AsyncSession): new_title = message.text.strip() + + is_valid, error_msg = validate_subgroup_title(new_title) + if not is_valid: + data = await state.get_data() + subgroup_hash = data.get("subgroup_hash") + group_code = data.get("group_code") + + await message.answer( + f"❌ {error_msg}\n\nПовторите ввод:", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] + ] + ), + ) + return + data = await state.get_data() old_title = data["subgroup_title"] group_code = data["group_code"] @@ -742,6 +846,8 @@ async def save_new_subgroup_title(message: Message, state: FSMContext, session: await session.commit() await state.clear() + new_hash = create_subgroup_hash(new_title, group_code) + await message.answer( f"✅ Подгруппа {old_title} переименована в {new_title}.", reply_markup=InlineKeyboardMarkup( @@ -753,13 +859,19 @@ async def save_new_subgroup_title(message: Message, state: FSMContext, session: @router.callback_query(F.data.startswith("delete_subgroup|"), IsAdminFilter()) -async def confirm_delete_subgroup(callback: CallbackQuery, state: FSMContext): - _, encoded_subgroup, group_code = callback.data.split("|", 2) - subgroup_title = encoded_subgroup.replace("_", " ") +async def confirm_delete_subgroup(callback: CallbackQuery, state: FSMContext, session: AsyncSession): + _, subgroup_hash, group_code = callback.data.split("|", 2) + + subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) + + if not subgroup_title: + await callback.message.edit_text("❌ Подгруппа не найдена.") + return await state.update_data( subgroup_title=subgroup_title, group_code=group_code, + subgroup_hash=subgroup_hash, ) await state.set_state(SubgroupEditState.confirming_deletion) @@ -770,7 +882,7 @@ async def confirm_delete_subgroup(callback: CallbackQuery, state: FSMContext): inline_keyboard=[ [ InlineKeyboardButton(text="✅ Удалить", callback_data="confirm_subgroup_deletion"), - InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{encoded_subgroup}|{group_code}") + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}") ] ] ) @@ -802,3 +914,213 @@ async def perform_subgroup_deletion(callback: CallbackQuery, state: FSMContext, ] ) ) + + +@router.callback_query(F.data.startswith("edit_subgroup_tariffs|"), IsAdminFilter()) +async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext, session: AsyncSession): + _, subgroup_hash, group_code = callback.data.split("|", 2) + + subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) + + if not subgroup_title: + await callback.message.edit_text("❌ Подгруппа не найдена.") + return + + result = await session.execute( + select(Tariff) + .where( + Tariff.group_code == group_code, + or_( + Tariff.subgroup_title == subgroup_title, + Tariff.subgroup_title.is_(None), + Tariff.subgroup_title == '' + ) + ) + .order_by(Tariff.id) + ) + all_tariffs_to_show = result.scalars().all() + + subgroup_tariff_ids = {t.id for t in all_tariffs_to_show if t.subgroup_title == subgroup_title} + + if not all_tariffs_to_show: + await callback.message.edit_text( + "❌ Нет доступных тарифов для редактирования.", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton( + text="⬅️ Назад", + callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" + )] + ] + ) + ) + return + + await state.set_state(SubgroupEditState.editing_tariffs) + await state.update_data( + subgroup_title=subgroup_title, + group_code=group_code, + subgroup_hash=subgroup_hash, + selected_tariff_ids=list(subgroup_tariff_ids) + ) + + builder = InlineKeyboardBuilder() + for tariff in all_tariffs_to_show: + is_in_subgroup = tariff.id in subgroup_tariff_ids + prefix = "✅ " if is_in_subgroup else "" + builder.row( + InlineKeyboardButton( + text=f"{prefix}{tariff.name}", + callback_data=f"edit_sub_toggle|{tariff.id}" + ) + ) + + builder.row( + InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}") + ) + + await callback.message.edit_text( + f"✏️ Редактирование подгруппы: {subgroup_title}\n\n" + "✅ - тарифы в подгруппе\n\n" + "Нажмите на тариф, чтобы добавить/убрать его:", + reply_markup=builder.as_markup() + ) + + +@router.callback_query(F.data.startswith("edit_sub_toggle|"), SubgroupEditState.editing_tariffs, IsAdminFilter()) +async def toggle_tariff_in_subgroup_edit(callback: CallbackQuery, state: FSMContext, session: AsyncSession): + tariff_id = int(callback.data.split("|")[1]) + data = await state.get_data() + selected_ids = set(data.get("selected_tariff_ids", [])) + + if tariff_id in selected_ids: + selected_ids.remove(tariff_id) + else: + selected_ids.add(tariff_id) + + await state.update_data(selected_tariff_ids=list(selected_ids)) + + subgroup_title = data["subgroup_title"] + group_code = data["group_code"] + subgroup_hash = data["subgroup_hash"] + + result = await session.execute( + select(Tariff) + .where( + Tariff.group_code == group_code, + or_( + Tariff.subgroup_title == subgroup_title, + Tariff.subgroup_title.is_(None), + Tariff.subgroup_title == '' + ) + ) + .order_by(Tariff.id) + ) + all_tariffs_to_show = result.scalars().all() + + builder = InlineKeyboardBuilder() + for tariff in all_tariffs_to_show: + is_selected = tariff.id in selected_ids + prefix = "✅ " if is_selected else "" + builder.row( + InlineKeyboardButton( + text=f"{prefix}{tariff.name}", + callback_data=f"edit_sub_toggle|{tariff.id}" + ) + ) + + builder.row( + InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), + InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}") + ) + + await callback.message.edit_reply_markup(reply_markup=builder.as_markup()) + + +@router.callback_query(F.data == "edit_sub_save", SubgroupEditState.editing_tariffs, IsAdminFilter()) +async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMContext, session: AsyncSession): + data = await state.get_data() + subgroup_title = data["subgroup_title"] + group_code = data["group_code"] + subgroup_hash = data["subgroup_hash"] + selected_tariff_ids = set(data.get("selected_tariff_ids", [])) + + result = await session.execute( + select(Tariff) + .where( + Tariff.group_code == group_code, + Tariff.subgroup_title == subgroup_title + ) + ) + current_subgroup_tariffs = result.scalars().all() + current_tariff_ids = {t.id for t in current_subgroup_tariffs} + + to_add = selected_tariff_ids - current_tariff_ids + to_remove = current_tariff_ids - selected_tariff_ids + + if to_remove: + await session.execute( + update(Tariff) + .where(Tariff.id.in_(to_remove)) + .values(subgroup_title=None, updated_at=datetime.utcnow()) + ) + + if to_add: + await session.execute( + update(Tariff) + .where(Tariff.id.in_(to_add)) + .values(subgroup_title=subgroup_title, updated_at=datetime.utcnow()) + ) + + await session.commit() + await state.clear() + + if not selected_tariff_ids: + await callback.message.edit_text( + f"✅ Подгруппа {subgroup_title} была расформирована.", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton( + text="⬅️ Назад к группе тарифов", + callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() + )] + ] + ) + ) + return + + changes_text = [] + if to_add: + added_names = [] + for tariff_id in to_add: + result = await session.execute(select(Tariff.name).where(Tariff.id == tariff_id)) + name = result.scalar_one() + if name: + added_names.append(name) + changes_text.append(f"➕ Добавлено: {', '.join(added_names)}") + + if to_remove: + removed_names = [] + for tariff_id in to_remove: + result = await session.execute(select(Tariff.name).where(Tariff.id == tariff_id)) + name = result.scalar_one() + if name: + removed_names.append(name) + changes_text.append(f"➖ Удалено: {', '.join(removed_names)}") + + if not changes_text: + changes_text.append("Изменений не было") + + await callback.message.edit_text( + f"✅ Подгруппа обновлена: {subgroup_title}\n\n" + f"{chr(10).join(changes_text)}", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton( + text="⬅️ Назад к подгруппе", + callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" + )] + ] + ) + ) diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index 65645176..ca6772c0 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -26,6 +26,7 @@ from database import ( get_tariffs_for_cluster, get_trial, ) +from database.tariffs import create_subgroup_hash, find_subgroup_by_hash from handlers.buttons import MAIN_MENU, PAYMENT from handlers.payments.robokassa_pay import handle_custom_amount_input from handlers.payments.stars_pay import process_custom_amount_input_stars @@ -106,6 +107,19 @@ async def handle_key_creation( ) return + group_code = tariffs[0].get("group_code") if tariffs else None + if not group_code: + await edit_or_send_message( + target_message=( + message_or_query.message + if isinstance(message_or_query, CallbackQuery) + else message_or_query + ), + text="❌ Не удалось определить группу тарифов.", + reply_markup=None, + ) + return + grouped_tariffs = defaultdict(list) for t in tariffs: subgroup = t.get("subgroup_title") @@ -122,11 +136,11 @@ async def handle_key_creation( ) for subgroup in sorted(k for k in grouped_tariffs if k): - safe = subgroup.replace(" ", "_") + subgroup_hash = create_subgroup_hash(subgroup, group_code) builder.row( InlineKeyboardButton( text=f"{subgroup}", - callback_data=f"tariff_subgroup_user|{safe}", + callback_data=f"tariff_subgroup_user|{subgroup_hash}", ) ) @@ -144,15 +158,25 @@ async def handle_key_creation( reply_markup=builder.as_markup(), ) - await state.update_data(tg_id=tg_id, cluster_name=cluster_name) + await state.update_data(tg_id=tg_id, cluster_name=cluster_name, group_code=group_code) await state.set_state(Form.waiting_for_server_selection) @router.callback_query(F.data.startswith("tariff_subgroup_user|")) async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMContext, session: Any): - subgroup = callback.data.split("|")[1].replace("_", " ") + subgroup_hash = callback.data.split("|")[1] data = await state.get_data() cluster_name = data.get("cluster_name") + group_code = data.get("group_code") + + subgroup = await find_subgroup_by_hash(session, subgroup_hash, group_code) + if not subgroup: + await edit_or_send_message( + target_message=callback.message, + text="❌ Подгруппа не найдена.", + reply_markup=None, + ) + return tariffs = await get_tariffs_for_cluster(session, cluster_name) filtered = [t for t in tariffs if t.get("subgroup_title") == subgroup] diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index a632b61d..5e0c10fe 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -25,6 +25,7 @@ from database import ( check_tariff_exists, ) from database.models import Server, Key +from database.tariffs import create_subgroup_hash, find_subgroup_by_hash from handlers.buttons import BACK, MAIN_MENU, PAYMENT from handlers.keys.key_utils import renew_key_in_cluster from handlers.payments.robokassa_pay import handle_custom_amount_input @@ -120,11 +121,11 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo ) for subgroup in sorted(k for k in grouped_tariffs if k): - safe = subgroup.replace(" ", "_")[:30] + subgroup_hash = create_subgroup_hash(subgroup, group_code) builder.row( InlineKeyboardButton( text=subgroup, - callback_data=f"renew_subgroup|{safe}", + callback_data=f"renew_subgroup|{subgroup_hash}", ) ) @@ -152,8 +153,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo @router.callback_query(F.data.startswith("renew_subgroup|")) async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMContext, session: AsyncSession): try: - parts = callback.data.split("|") - subgroup = parts[1].replace("_", " ") + subgroup_hash = callback.data.split("|")[1] data = await state.get_data() client_id = data.get("renew_client_id") @@ -192,6 +192,12 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont return group_code = row[0] + + subgroup = await find_subgroup_by_hash(session, subgroup_hash, group_code) + if not subgroup: + await callback.message.answer("❌ Подгруппа не найдена.") + return + tariffs = await get_tariffs(session, group_code=group_code) filtered = [t for t in tariffs if t["subgroup_title"] == subgroup and t["is_active"]]