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"]]