From 857aef1c4c82c0505b05167927f71b15250e8f61 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Mon, 18 Aug 2025 21:50:02 +0300 Subject: [PATCH] Fix hot leads logic / Add tariff sorting functionality --- database/hot_leads.py | 2 +- database/models.py | 1 + database/tariffs.py | 137 +++++++++++- handlers/admin/gifts/gifts_handler.py | 54 ++--- handlers/admin/tariffs/keyboard.py | 108 +++++++++- handlers/admin/tariffs/tariffs_handler.py | 248 +++++++++++++++++----- handlers/keys/key_mode/key_create.py | 22 +- handlers/keys/key_renew.py | 15 +- 8 files changed, 486 insertions(+), 101 deletions(-) diff --git a/database/hot_leads.py b/database/hot_leads.py index 2e9c1063..171b3876 100644 --- a/database/hot_leads.py +++ b/database/hot_leads.py @@ -8,7 +8,7 @@ async def get_hot_leads(session: AsyncSession): """ Возвращает пользователей, у которых есть успешные оплаты, но нет активных ключей. """ - subquery = select(Key.tg_id).where(Key.expiry_time > func.extract("epoch", func.now()) * 1000).distinct() + subquery = select(Key.tg_id).distinct() stmt = ( select(Payment.tg_id) diff --git a/database/models.py b/database/models.py index 4871939c..59745f03 100644 --- a/database/models.py +++ b/database/models.py @@ -87,6 +87,7 @@ class Tariff(DictLikeMixin, Base): created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow) subgroup_title = Column(String, nullable=True) + sort_order = Column(Integer, nullable=True) class Server(DictLikeMixin, Base): diff --git a/database/tariffs.py b/database/tariffs.py index 871ba41c..bfe9ea8f 100644 --- a/database/tariffs.py +++ b/database/tariffs.py @@ -1,8 +1,9 @@ import hashlib +from collections import defaultdict from datetime import datetime -from sqlalchemy import delete, insert, select, update +from sqlalchemy import delete, func, insert, select, update from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession @@ -34,16 +35,48 @@ async def find_subgroup_by_hash(session: AsyncSession, subgroup_hash: str, group return None -async def get_tariffs(session: AsyncSession, tariff_id: int = None, group_code: str = None): +async def get_tariffs(session: AsyncSession, tariff_id: int = None, group_code: str = None, with_subgroup_weights: bool = False): try: if tariff_id: result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) elif group_code: - result = await session.execute(select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)) + result = await session.execute( + select(Tariff) + .where(Tariff.group_code == group_code) + .order_by(Tariff.sort_order, Tariff.id) + ) else: - result = await session.execute(select(Tariff)) + result = await session.execute(select(Tariff).order_by(Tariff.sort_order, Tariff.id)) - return [dict(r.__dict__) for r in result.scalars().all()] + tariffs = [dict(r.__dict__) for r in result.scalars().all()] + + if with_subgroup_weights and group_code: + tariffs_without_order = [t for t in tariffs if t.get("sort_order") is None] + if tariffs_without_order: + max_order = max((t.get("sort_order") or 0 for t in tariffs), default=0) + for i, tariff in enumerate(tariffs_without_order, start=max_order + 1): + tariff["sort_order"] = i + await session.execute( + update(Tariff).where(Tariff.id == tariff["id"]).values(sort_order=i) + ) + await session.commit() + + grouped = defaultdict(list) + for t in tariffs: + grouped[t.get("subgroup_title")].append(t) + + subgroup_weights = {} + for subgroup, tariffs_list in grouped.items(): + if subgroup: + total_weight = sum(t.get("sort_order", 1) for t in tariffs_list) + subgroup_weights[subgroup] = total_weight + + return { + 'tariffs': tariffs, + 'subgroup_weights': subgroup_weights + } + + return tariffs except SQLAlchemyError as e: logger.error(f"[TARIFF] Ошибка при получении тарифов: {e}") return [] @@ -77,7 +110,9 @@ async def get_tariffs_for_cluster(session: AsyncSession, cluster_name: str): group_code = row[0] result = await session.execute( - select(Tariff).where(Tariff.group_code == group_code, Tariff.is_active.is_(True)).order_by(Tariff.id) + select(Tariff) + .where(Tariff.group_code == group_code, Tariff.is_active.is_(True)) + .order_by(Tariff.sort_order, Tariff.id) ) return [dict(r.__dict__) for r in result.scalars().all()] except SQLAlchemyError as e: @@ -90,6 +125,20 @@ async def create_tariff(session: AsyncSession, data: dict): data["created_at"] = datetime.utcnow() data["updated_at"] = datetime.utcnow() + if "sort_order" not in data: + group_code = data.get("group_code") + if group_code: + result = await session.execute( + select(func.max(Tariff.sort_order)) + .where(Tariff.group_code == group_code) + ) + max_order = result.scalar() or 0 + else: + result = await session.execute(select(func.max(Tariff.sort_order))) + max_order = result.scalar() or 0 + + data["sort_order"] = max_order + 1 + stmt = insert(Tariff).values(**data).returning(Tariff) result = await session.execute(stmt) await session.commit() @@ -136,3 +185,79 @@ async def check_tariff_exists(session: AsyncSession, tariff_id: int): except SQLAlchemyError as e: logger.error(f"[TARIFF] Ошибка при проверке тарифа {tariff_id}: {e}") return False + + +async def get_tariff_sort_order(session: AsyncSession, tariff_id: int) -> int: + try: + result = await session.execute( + select(Tariff.sort_order).where(Tariff.id == tariff_id) + ) + sort_order = result.scalar_one_or_none() + + if sort_order is None: + await session.execute( + update(Tariff).where(Tariff.id == tariff_id).values(sort_order=1) + ) + await session.commit() + return 1 + + return sort_order + except SQLAlchemyError as e: + logger.error(f"[TARIFF] Ошибка при получении sort_order для тарифа {tariff_id}: {e}") + return None + + +async def move_tariff_up(session: AsyncSession, tariff_id: int) -> bool: + try: + current_order = await get_tariff_sort_order(session, tariff_id) + new_order = max(1, current_order - 1) + + await session.execute( + update(Tariff).where(Tariff.id == tariff_id).values(sort_order=new_order) + ) + await session.commit() + return True + except SQLAlchemyError as e: + logger.error(f"[TARIFF] Ошибка при перемещении тарифа {tariff_id} вверх: {e}") + await session.rollback() + return False + + +async def move_tariff_down(session: AsyncSession, tariff_id: int) -> bool: + try: + current_order = await get_tariff_sort_order(session, tariff_id) + new_order = current_order + 1 + + await session.execute( + update(Tariff).where(Tariff.id == tariff_id).values(sort_order=new_order) + ) + await session.commit() + return True + except SQLAlchemyError as e: + logger.error(f"[TARIFF] Ошибка при перемещении тарифа {tariff_id} вниз: {e}") + await session.rollback() + return False + + +async def initialize_tariff_sort_orders(session: AsyncSession, group_code: str) -> bool: + try: + result = await session.execute( + select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id) + ) + tariffs = result.scalars().all() + + if not tariffs: + return True + + for i, tariff in enumerate(tariffs): + new_sort_order = 1 + i + await session.execute( + update(Tariff).where(Tariff.id == tariff.id).values(sort_order=new_sort_order) + ) + + await session.commit() + return True + except SQLAlchemyError as e: + logger.error(f"[TARIFF] Ошибка при инициализации sort_order для группы {group_code}: {e}") + await session.rollback() + return False diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py index 9d31985e..ac33a940 100644 --- a/handlers/admin/gifts/gifts_handler.py +++ b/handlers/admin/gifts/gifts_handler.py @@ -3,11 +3,12 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery from aiogram.utils.keyboard import InlineKeyboardBuilder +from collections import defaultdict from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from database.models import Gift, GiftUsage, Tariff -from database.tariffs import create_subgroup_hash, find_subgroup_by_hash +from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs from handlers.utils import edit_or_send_message, format_days, format_months from logger import logger @@ -32,9 +33,9 @@ async def admin_gift_menu(callback: CallbackQuery): @router.callback_query(F.data == "admin_gift_create") async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession): - stmt = select(Tariff).where(Tariff.group_code == "gifts", Tariff.is_active.is_(True)).order_by(Tariff.duration_days) - result = await session.execute(stmt) - tariffs = result.scalars().all() + tariffs_data = await get_tariffs(session, group_code="gifts", with_subgroup_weights=True) + tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] + subgroup_weights = tariffs_data['subgroup_weights'] if not tariffs: builder = InlineKeyboardBuilder() @@ -42,23 +43,31 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession await callback.message.edit_text("❌ Нет активных тарифов в группе 'gifts'.", reply_markup=builder.as_markup()) return - from collections import defaultdict - grouped_tariffs = defaultdict(list) for t in tariffs: - grouped_tariffs[t.subgroup_title].append(t) + grouped_tariffs[t.get('subgroup_title')].append(t) builder = InlineKeyboardBuilder() for t in grouped_tariffs.get(None, []): - if t.duration_days % 30 == 0: - duration_text = format_months(t.duration_days // 30) + if t.get('duration_days') % 30 == 0: + duration_text = format_months(t.get('duration_days') // 30) else: - duration_text = format_days(t.duration_days) + duration_text = format_days(t.get('duration_days')) - builder.button(text=f"{t.name} – {duration_text}", callback_data=f"admin_gift_select|{t.id}") + builder.row( + types.InlineKeyboardButton( + text=f"{t.get('name')} – {duration_text}", + callback_data=f"admin_gift_select|{t.get('id')}" + ) + ) - for subgroup in sorted(k for k in grouped_tariffs if k): + sorted_subgroups = sorted( + [k for k in grouped_tariffs if k], + key=lambda x: (subgroup_weights.get(x, 999999), x) + ) + + for subgroup in sorted_subgroups: subgroup_hash = create_subgroup_hash(subgroup, "gifts") builder.row( types.InlineKeyboardButton( @@ -82,30 +91,23 @@ async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session: await callback.message.edit_text("❌ Подгруппа не найдена.") return - stmt = ( - select(Tariff) - .where(Tariff.group_code == "gifts", Tariff.is_active.is_(True)) - .order_by(Tariff.duration_days) - ) - result = await session.execute(stmt) - tariffs = result.scalars().all() - - filtered = [t for t in tariffs if t.subgroup_title == subgroup] + tariffs = await get_tariffs(session, group_code="gifts") + filtered = [t for t in tariffs if t.get('subgroup_title') == subgroup and t.get('is_active')] if not filtered: await callback.message.edit_text("❌ В этой подгруппе пока нет тарифов.") return builder = InlineKeyboardBuilder() for t in filtered: - if t.duration_days % 30 == 0: - duration_text = format_months(t.duration_days // 30) + if t.get('duration_days') % 30 == 0: + duration_text = format_months(t.get('duration_days') // 30) else: - duration_text = format_days(t.duration_days) + duration_text = format_days(t.get('duration_days')) builder.row( types.InlineKeyboardButton( - text=f"{t.name} – {duration_text}", - callback_data=f"admin_gift_select|{t.id}", + text=f"{t.get('name')} – {duration_text}", + callback_data=f"admin_gift_select|{t.get('id')}", ) ) diff --git a/handlers/admin/tariffs/keyboard.py b/handlers/admin/tariffs/keyboard.py index ce358999..363fce1e 100644 --- a/handlers/admin/tariffs/keyboard.py +++ b/handlers/admin/tariffs/keyboard.py @@ -27,6 +27,12 @@ def build_tariff_menu_kb() -> InlineKeyboardMarkup: callback_data=AdminTariffCallback(action="list").pack(), ) ) + builder.row( + InlineKeyboardButton( + text="🔢 Расположение тарифов", + callback_data=AdminTariffCallback(action="arrange").pack(), + ) + ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=AdminPanelCallback(action="admin").pack())) return builder.as_markup() @@ -37,6 +43,76 @@ def build_cancel_kb() -> InlineKeyboardMarkup: ) +def build_tariff_arrangement_groups_kb(groups: list[str]) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + row = [] + + for i, group in enumerate(groups): + row.append( + InlineKeyboardButton( + text=group, + callback_data=AdminTariffCallback(action=f"arrange_group|{group}").pack(), + ) + ) + if len(row) == 2 or i == len(groups) - 1: + builder.row(*row) + row = [] + + builder.row( + InlineKeyboardButton( + text="⬅️ Назад", + callback_data=AdminTariffCallback(action="list").pack(), + ) + ) + return builder.as_markup() + + +def build_tariffs_arrangement_kb(group_code: str, tariffs: list) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + grouped_tariffs = defaultdict(list) + for t in tariffs: + grouped_tariffs[t.get("subgroup_title")].append(t) + + for subgroup in grouped_tariffs: + grouped_tariffs[subgroup].sort(key=lambda x: x.get("sort_order")) + + if grouped_tariffs.get(None): + for t in grouped_tariffs[None]: + builder.row( + InlineKeyboardButton(text="⬆️", callback_data=AdminTariffCallback(action=f"quick_move_up|{t.get('id')}|{group_code}").pack()), + InlineKeyboardButton(text=f" {t.get('name')} ", callback_data=AdminTariffCallback(action=f"view|{t.get('id')}").pack()), + InlineKeyboardButton(text="⬇️", callback_data=AdminTariffCallback(action=f"quick_move_down|{t.get('id')}|{group_code}").pack()) + ) + + for subgroup, tariffs_list in grouped_tariffs.items(): + if subgroup: + builder.row( + InlineKeyboardButton(text=f"📁 {subgroup}", callback_data=AdminTariffCallback(action="arrange").pack()) + ) + for t in tariffs_list: + builder.row( + InlineKeyboardButton(text="⬆️", callback_data=AdminTariffCallback(action=f"quick_move_up|{t.get('id')}|{group_code}").pack()), + InlineKeyboardButton(text=f" {t.get('name')} ", callback_data=AdminTariffCallback(action=f"view|{t.get('id')}").pack()), + InlineKeyboardButton(text="⬇️", callback_data=AdminTariffCallback(action=f"quick_move_down|{t.get('id')}|{group_code}").pack()) + ) + + builder.row( + InlineKeyboardButton( + text="⬅️ Назад", + callback_data=AdminPanelCallback(action="tariffs").pack(), + ) + ) + builder.row( + InlineKeyboardButton( + text="🏠 Главное меню", + callback_data=AdminPanelCallback(action="admin").pack(), + ) + ) + + return builder.as_markup() + + def build_tariff_groups_kb(groups: list[str]) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() row = [] @@ -73,14 +149,18 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup: subgroup = t.get("subgroup_title") grouped[subgroup].append(t) - for subgroup_title, _items in grouped.items(): - if subgroup_title: - subgroup_hash = create_subgroup_hash(subgroup_title, group_code) - builder.row( - InlineKeyboardButton( - text=f"{subgroup_title}", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" - ) + sorted_subgroups = sorted( + [k for k in grouped if k], + key=lambda x: (sum(t.get("sort_order", 1) for t in grouped[x]), x) + ) + + for subgroup_title in sorted_subgroups: + subgroup_hash = create_subgroup_hash(subgroup_title, group_code) + builder.row( + InlineKeyboardButton( + text=f"{subgroup_title}", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" ) + ) for t in grouped.get(None, []): title = f"{t['name']} — {t['price_rub']}₽" @@ -110,7 +190,7 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup: return builder.as_markup() -def build_single_tariff_kb(tariff_id: int) -> InlineKeyboardMarkup: +def build_single_tariff_kb(tariff_id: int, group_code: str = None) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [ @@ -123,10 +203,20 @@ def build_single_tariff_kb(tariff_id: int) -> InlineKeyboardMarkup: callback_data=AdminTariffCallback(action=f"delete|{tariff_id}").pack(), ), ], + [ + InlineKeyboardButton( + text="⬆️ Выше", + callback_data=AdminTariffCallback(action=f"move_up|{tariff_id}").pack(), + ), + InlineKeyboardButton( + text="⬇️ Ниже", + callback_data=AdminTariffCallback(action=f"move_down|{tariff_id}").pack(), + ), + ], [ InlineKeyboardButton( text="⬅️ Назад", - callback_data=AdminTariffCallback(action="list").pack(), + callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() if group_code else AdminTariffCallback(action="list").pack(), ) ], ] diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py index f2160296..645be25f 100644 --- a/handlers/admin/tariffs/tariffs_handler.py +++ b/handlers/admin/tariffs/tariffs_handler.py @@ -1,6 +1,8 @@ import re +from collections import defaultdict from datetime import datetime +import pytz from aiogram import F, Router from aiogram.fsm.context import FSMContext @@ -17,7 +19,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from database import create_tariff from database.models import Gift, Key, Server, Tariff -from database.tariffs import create_subgroup_hash, find_subgroup_by_hash +from database.tariffs import ( + create_subgroup_hash, + find_subgroup_by_hash, + get_tariffs, + move_tariff_up as db_move_tariff_up, + move_tariff_down as db_move_tariff_down +) from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback @@ -29,6 +37,8 @@ from .keyboard import ( build_tariff_groups_kb, build_tariff_list_kb, build_tariff_menu_kb, + build_tariff_arrangement_groups_kb, + build_tariffs_arrangement_kb, ) @@ -291,27 +301,54 @@ async def show_tariff_groups(callback: CallbackQuery, session: AsyncSession): await callback.message.edit_text(text, reply_markup=build_tariff_groups_kb(groups)) -def tariff_to_dict(tariff: Tariff) -> dict: +@router.callback_query(AdminTariffCallback.filter(F.action == "arrange"), IsAdminFilter()) +async def show_tariff_arrangement_menu(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("❌ Нет доступных групп тарифов.") + return + + await callback.message.edit_text( + "🔢 Управление расположением тарифов\n\n" + "📋 Как это работает:\n" + "• Тарифы отображаются в порядке их расположения\n" + "• Меньший номер = выше в списке\n" + "• Новые тарифы добавляются в конец списка\n" + "• ⬆️ поднимает тариф выше (номер уменьшается)\n" + "• ⬇️ опускает тариф ниже (номер увеличивается)\n" + "• Подгруппы сортируются по общей сумме тарифов внутри\n\n" + "Выберите группу для управления расположением:", + reply_markup=build_tariff_arrangement_groups_kb(groups), + ) + + +def tariff_to_dict(tariff) -> dict: + if isinstance(tariff, dict): + return tariff return { "id": tariff.id, "name": tariff.name, "price_rub": tariff.price_rub, "group_code": tariff.group_code, "subgroup_title": tariff.subgroup_title, + "sort_order": tariff.sort_order, } @router.callback_query(AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter()) async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): - group_code = callback_data.action.split("|", 1)[1] + group_code = callback_data.action.split("|")[1] - result = await session.execute(select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)) - tariffs = result.scalars().all() + 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( @@ -320,9 +357,61 @@ async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTar ) +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("arrange_group|")), IsAdminFilter()) +async def show_tariffs_arrangement(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): + group_code = callback_data.action.split("|")[1] + + tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True) + tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] + subgroup_weights = tariffs_data['subgroup_weights'] + + if not tariffs: + await callback.message.edit_text("❌ В этой группе пока нет активных тарифов.") + return + + grouped_tariffs = defaultdict(list) + for t in tariffs: + grouped_tariffs[t.get("subgroup_title")].append(t) + + sorted_subgroups = sorted( + [k for k in grouped_tariffs if k], + key=lambda x: (subgroup_weights.get(x, 999999), x) + ) + + moscow_tz = pytz.timezone("Europe/Moscow") + now = datetime.now(moscow_tz) + current_time = now.strftime("%d.%m.%y %H:%M:%S МСК") + + text = f"🔢 Итоговая сортировка тарифов в группе: {group_code}\n\n" + + if grouped_tariffs.get(None): + text += "📋 Основные тарифы:\n" + for t in grouped_tariffs[None]: + sort_order = t.get('sort_order', 1) + text += f"• {t.get('name')} [позиция: {sort_order}]\n" + text += "\n" + + if sorted_subgroups: + text += "📁 Подгруппы:\n" + for subgroup in sorted_subgroups: + subgroup_weight = subgroup_weights.get(subgroup, 999999) + text += f"• {subgroup} [вес группы: {subgroup_weight}]\n" + for t in grouped_tariffs[subgroup]: + sort_order = t.get('sort_order', 1) + text += f" └ {t.get('name')} [позиция: {sort_order}]\n" + text += "\n" + + text += f"\n{current_time}" + + await callback.message.edit_text( + text, + reply_markup=build_tariffs_arrangement_kb(group_code, tariffs), + ) + + @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)[1]) + 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() @@ -337,7 +426,7 @@ async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallbac @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)[1]) + 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() @@ -567,6 +656,7 @@ async def start_tariff_creation_existing_group( def render_tariff_card(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]: traffic_text = f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит" device_text = f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит" + sort_order = getattr(tariff, 'sort_order', 1) text = ( f"📄 Тариф: {tariff.name}\n\n" @@ -575,22 +665,19 @@ def render_tariff_card(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]: f"💰 Стоимость: {tariff.price_rub}₽\n" f"📦 Трафик: {traffic_text}\n" f"📱 Устройств: {device_text}\n" + f"🔢 Позиция: {sort_order}\n" f"{'✅ Активен' if tariff.is_active else '⛔ Отключен'}" ) - return text, build_single_tariff_kb(tariff.id) + return text, build_single_tariff_kb(tariff.id, tariff.group_code) @router.callback_query(F.data.startswith("start_subgrouping|"), IsAdminFilter()) async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: AsyncSession): group_code = callback.data.split("|", 1)[1] - result = await session.execute( - select(Tariff) - .where(Tariff.group_code == group_code, (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == ""))) - .order_by(Tariff.id) - ) - tariffs = result.scalars().all() + tariffs = await get_tariffs(session, group_code=group_code) + tariffs = [t for t in tariffs if not t.get("subgroup_title") or t.get("subgroup_title") == ""] if not tariffs: await callback.message.edit_text( @@ -612,7 +699,7 @@ async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: builder = InlineKeyboardBuilder() for tariff in tariffs: - builder.row(InlineKeyboardButton(text=f"{tariff.name}", callback_data=f"sub_select|{tariff.id}")) + builder.row(InlineKeyboardButton(text=f"{tariff.get('name')}", callback_data=f"sub_select|{tariff.get('id')}")) builder.row( InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), @@ -638,18 +725,14 @@ async def toggle_tariff_subgroup_selection(callback: CallbackQuery, state: FSMCo await state.update_data(selected_tariff_ids=list(selected)) group_code = data["group_code"] - result = await session.execute( - select(Tariff) - .where(Tariff.group_code == group_code, (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == ""))) - .order_by(Tariff.id) - ) - tariffs = result.scalars().all() + tariffs = await get_tariffs(session, group_code=group_code) + tariffs = [t for t in tariffs if not t.get("subgroup_title") or t.get("subgroup_title") == ""] builder = InlineKeyboardBuilder() for tariff in tariffs: - is_selected = tariff.id in selected + is_selected = tariff.get("id") in selected prefix = "✅ " if is_selected else "" - builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"sub_select|{tariff.id}")) + builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.get('name')}", callback_data=f"sub_select|{tariff.get('id')}")) builder.row( InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), @@ -732,12 +815,8 @@ async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): await callback.message.edit_text("❌ Подгруппа не найдена.") return - result = await session.execute( - select(Tariff) - .where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) - .order_by(Tariff.id) - ) - tariffs = result.scalars().all() + tariffs = await get_tariffs(session, group_code=group_code) + tariffs = [t for t in tariffs if t.get("subgroup_title") == subgroup_title] if not tariffs: await callback.message.edit_text("❌ В этой подгруппе пока нет тарифов.") @@ -938,17 +1017,10 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext 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() + all_tariffs_to_show = await get_tariffs(session, group_code=group_code) + all_tariffs_to_show = [t for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title or not t.get("subgroup_title") or t.get("subgroup_title") == ""] - subgroup_tariff_ids = {t.id for t in all_tariffs_to_show if t.subgroup_title == subgroup_title} + subgroup_tariff_ids = {t.get("id") for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title} if not all_tariffs_to_show: await callback.message.edit_text( @@ -971,9 +1043,9 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext builder = InlineKeyboardBuilder() for tariff in all_tariffs_to_show: - is_in_subgroup = tariff.id in subgroup_tariff_ids + is_in_subgroup = tariff.get("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=f"{prefix}{tariff.get('name')}", callback_data=f"edit_sub_toggle|{tariff.get('id')}")) builder.row( InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), @@ -1005,21 +1077,14 @@ async def toggle_tariff_in_subgroup_edit(callback: CallbackQuery, state: FSMCont 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() + all_tariffs_to_show = await get_tariffs(session, group_code=group_code) + all_tariffs_to_show = [t for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title or not t.get("subgroup_title") or t.get("subgroup_title") == ""] builder = InlineKeyboardBuilder() for tariff in all_tariffs_to_show: - is_selected = tariff.id in selected_ids + is_selected = tariff.get("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=f"{prefix}{tariff.get('name')}", callback_data=f"edit_sub_toggle|{tariff.get('id')}")) builder.row( InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), @@ -1111,3 +1176,80 @@ async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMConte ] ), ) + + +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("move_up|")), IsAdminFilter()) +async def move_tariff_up(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): + tariff_id = int(callback_data.action.split("|")[1]) + + success = await db_move_tariff_up(session, tariff_id) + + if not success: + await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) + return + + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) + tariff = result.scalar_one_or_none() + + if not tariff: + await callback.answer("❌ Тариф не найден", show_alert=True) + return + + text, markup = render_tariff_card(tariff) + await callback.message.edit_text(text=text, reply_markup=markup) + await callback.answer("✅ Тариф перемещен выше (-1)") + + +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("move_down|")), IsAdminFilter()) +async def move_tariff_down(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): + tariff_id = int(callback_data.action.split("|")[1]) + + success = await db_move_tariff_down(session, tariff_id) + + if not success: + await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) + return + + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) + tariff = result.scalar_one_or_none() + + if not tariff: + await callback.answer("❌ Тариф не найден", show_alert=True) + return + + text, markup = render_tariff_card(tariff) + await callback.message.edit_text(text=text, reply_markup=markup) + await callback.answer("✅ Тариф перемещен ниже (+1)") + + +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("quick_move_up|")), IsAdminFilter()) +async def quick_move_tariff_up(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): + parts = callback_data.action.split("|") + tariff_id = int(parts[1]) + group_code = parts[2] + + success = await db_move_tariff_up(session, tariff_id) + + if not success: + await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) + return + + await callback.answer("✅ Тариф перемещен выше (-1)") + new_callback_data = AdminTariffCallback(action=f"arrange_group|{group_code}") + await show_tariffs_arrangement(callback, new_callback_data, session) + +@router.callback_query(AdminTariffCallback.filter(F.action.startswith("quick_move_down|")), IsAdminFilter()) +async def quick_move_tariff_down(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): + parts = callback_data.action.split("|") + tariff_id = int(parts[1]) + group_code = parts[2] + + success = await db_move_tariff_down(session, tariff_id) + + if not success: + await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) + return + + await callback.answer("✅ Тариф перемещен ниже (+1)") + new_callback_data = AdminTariffCallback(action=f"arrange_group|{group_code}") + await show_tariffs_arrangement(callback, new_callback_data, session) diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index e2c88133..dc12510a 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -142,6 +142,13 @@ async def handle_key_creation( tariffs = await get_tariffs_for_cluster(session, cluster_name) + if tariffs: + group_code = tariffs[0].get("group_code") + if group_code: + tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True) + tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] + subgroup_weights = tariffs_data['subgroup_weights'] + if not tariffs: result = await session.execute(select(Admin).where(Admin.tg_id == tg_id)) is_admin = result.scalar_one_or_none() is not None @@ -202,7 +209,12 @@ async def handle_key_creation( ) ) - for subgroup in sorted(k for k in grouped_tariffs if k): + sorted_subgroups = sorted( + [k for k in grouped_tariffs if k], + key=lambda x: (subgroup_weights.get(x, 999999), x) + ) + + for subgroup in sorted_subgroups: subgroup_hash = create_subgroup_hash(subgroup, group_code) builder.row( InlineKeyboardButton( @@ -245,7 +257,13 @@ async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMConte return tariffs = await get_tariffs_for_cluster(session, cluster_name) - filtered = [t for t in tariffs if t.get("subgroup_title") == subgroup] + filtered = [] + + if tariffs: + group_code = tariffs[0].get("group_code") + if group_code: + tariffs = await get_tariffs(session, group_code=group_code) + filtered = [t for t in tariffs if t.get("subgroup_title") == subgroup and t.get('is_active')] if not filtered: await edit_or_send_message( diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index 622f9cb5..0516cb4a 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -26,7 +26,7 @@ from database import ( update_key_expiry, ) from database.models import Key, Server -from database.tariffs import create_subgroup_hash, find_subgroup_by_hash +from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs from handlers.buttons import BACK, MAIN_MENU, MY_SUB, PAYMENT from handlers.keys.operations import renew_key_in_cluster from handlers.payments.robokassa_pay import handle_custom_amount_input @@ -97,8 +97,10 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo if current_tariff["group_code"] not in ["discounts", "discounts_max", "gifts", "trial"]: group_code = current_tariff["group_code"] - tariffs = await get_tariffs(session, group_code=group_code) - tariffs = [t for t in tariffs if t["is_active"]] + tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True) + tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] + subgroup_weights = tariffs_data['subgroup_weights'] + if not tariffs: await callback_query.message.answer("❌ Нет доступных тарифов для продления.") return @@ -118,7 +120,12 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo ) ) - for subgroup in sorted(k for k in grouped_tariffs if k): + sorted_subgroups = sorted( + [k for k in grouped_tariffs if k], + key=lambda x: (subgroup_weights.get(x, 999999), x) + ) + + for subgroup in sorted_subgroups: subgroup_hash = create_subgroup_hash(subgroup, group_code) builder.row( InlineKeyboardButton(