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(