mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Merge pull request #1802 from Fr1ngg/revert-1801-bedolaga/add-server-category-feature-to-admin-panel-ltnwjl
Revert "Fix server category button routing in admin panel"
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database.models import ServerCategory
|
||||
|
||||
|
||||
async def get_all_server_categories(db: AsyncSession, include_inactive: bool = False) -> List[ServerCategory]:
|
||||
query = select(ServerCategory).order_by(ServerCategory.sort_order, ServerCategory.name)
|
||||
if not include_inactive:
|
||||
query = query.where(ServerCategory.is_active.is_(True))
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_server_category_by_id(db: AsyncSession, category_id: int) -> Optional[ServerCategory]:
|
||||
result = await db.execute(
|
||||
select(ServerCategory).where(ServerCategory.id == category_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def create_server_category(
|
||||
db: AsyncSession,
|
||||
name: str,
|
||||
sort_order: int = 0,
|
||||
is_active: bool = True,
|
||||
) -> ServerCategory:
|
||||
category = ServerCategory(name=name, sort_order=sort_order, is_active=is_active)
|
||||
db.add(category)
|
||||
await db.commit()
|
||||
await db.refresh(category)
|
||||
return category
|
||||
|
||||
|
||||
async def update_server_category(
|
||||
db: AsyncSession,
|
||||
category_id: int,
|
||||
**updates,
|
||||
) -> Optional[ServerCategory]:
|
||||
valid_fields = {"name", "sort_order", "is_active"}
|
||||
filtered_updates = {k: v for k, v in updates.items() if k in valid_fields}
|
||||
if not filtered_updates:
|
||||
return await get_server_category_by_id(db, category_id)
|
||||
|
||||
await db.execute(
|
||||
update(ServerCategory).where(ServerCategory.id == category_id).values(**filtered_updates)
|
||||
)
|
||||
await db.commit()
|
||||
return await get_server_category_by_id(db, category_id)
|
||||
|
||||
|
||||
async def delete_server_category(db: AsyncSession, category_id: int) -> bool:
|
||||
await db.execute(delete(ServerCategory).where(ServerCategory.id == category_id))
|
||||
await db.commit()
|
||||
return True
|
||||
@@ -19,7 +19,6 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import (
|
||||
PromoGroup,
|
||||
ServerCategory,
|
||||
ServerSquad,
|
||||
SubscriptionServer,
|
||||
Subscription,
|
||||
@@ -49,7 +48,6 @@ async def create_server_squad(
|
||||
is_available: bool = True,
|
||||
is_trial_eligible: bool = False,
|
||||
sort_order: int = 0,
|
||||
category_id: Optional[int] = None,
|
||||
promo_group_ids: Optional[Iterable[int]] = None,
|
||||
) -> ServerSquad:
|
||||
|
||||
@@ -73,13 +71,6 @@ async def create_server_squad(
|
||||
"Не все промогруппы найдены при создании сервера %s", display_name
|
||||
)
|
||||
|
||||
if category_id is not None:
|
||||
category_exists = await db.execute(
|
||||
select(ServerCategory.id).where(ServerCategory.id == category_id)
|
||||
)
|
||||
if category_exists.scalar_one_or_none() is None:
|
||||
raise ValueError("Server category not found")
|
||||
|
||||
server_squad = ServerSquad(
|
||||
squad_uuid=squad_uuid,
|
||||
display_name=display_name,
|
||||
@@ -91,7 +82,6 @@ async def create_server_squad(
|
||||
is_available=is_available,
|
||||
is_trial_eligible=is_trial_eligible,
|
||||
sort_order=sort_order,
|
||||
category_id=category_id,
|
||||
allowed_promo_groups=promo_groups,
|
||||
)
|
||||
|
||||
@@ -110,10 +100,7 @@ async def get_server_squad_by_uuid(
|
||||
|
||||
result = await db.execute(
|
||||
select(ServerSquad)
|
||||
.options(
|
||||
selectinload(ServerSquad.allowed_promo_groups),
|
||||
selectinload(ServerSquad.category),
|
||||
)
|
||||
.options(selectinload(ServerSquad.allowed_promo_groups))
|
||||
.where(ServerSquad.squad_uuid == squad_uuid)
|
||||
)
|
||||
return result.scalars().unique().one_or_none()
|
||||
@@ -126,10 +113,7 @@ async def get_server_squad_by_id(
|
||||
|
||||
result = await db.execute(
|
||||
select(ServerSquad)
|
||||
.options(
|
||||
selectinload(ServerSquad.allowed_promo_groups),
|
||||
selectinload(ServerSquad.category),
|
||||
)
|
||||
.options(selectinload(ServerSquad.allowed_promo_groups))
|
||||
.where(ServerSquad.id == server_id)
|
||||
)
|
||||
return result.scalars().unique().one_or_none()
|
||||
@@ -142,8 +126,8 @@ async def get_all_server_squads(
|
||||
limit: int = 50
|
||||
) -> Tuple[List[ServerSquad], int]:
|
||||
|
||||
query = select(ServerSquad).options(selectinload(ServerSquad.category))
|
||||
|
||||
query = select(ServerSquad)
|
||||
|
||||
if available_only:
|
||||
query = query.where(ServerSquad.is_available == True)
|
||||
|
||||
@@ -167,16 +151,11 @@ async def get_all_server_squads(
|
||||
async def get_available_server_squads(
|
||||
db: AsyncSession,
|
||||
promo_group_id: Optional[int] = None,
|
||||
category_id: Optional[int] = None,
|
||||
only_with_capacity: bool = False,
|
||||
) -> List[ServerSquad]:
|
||||
|
||||
query = (
|
||||
select(ServerSquad)
|
||||
.options(
|
||||
selectinload(ServerSquad.allowed_promo_groups),
|
||||
selectinload(ServerSquad.category),
|
||||
)
|
||||
.options(selectinload(ServerSquad.allowed_promo_groups))
|
||||
.where(ServerSquad.is_available.is_(True))
|
||||
.order_by(ServerSquad.sort_order, ServerSquad.display_name)
|
||||
)
|
||||
@@ -186,21 +165,8 @@ async def get_available_server_squads(
|
||||
PromoGroup.id == promo_group_id
|
||||
)
|
||||
|
||||
if category_id is not None:
|
||||
query = query.where(ServerSquad.category_id == category_id)
|
||||
|
||||
result = await db.execute(query)
|
||||
squads = result.scalars().unique().all()
|
||||
|
||||
if only_with_capacity:
|
||||
filtered: List[ServerSquad] = []
|
||||
for squad in squads:
|
||||
if squad.is_full:
|
||||
continue
|
||||
filtered.append(squad)
|
||||
return filtered
|
||||
|
||||
return squads
|
||||
return result.scalars().unique().all()
|
||||
|
||||
|
||||
async def get_active_server_squads(db: AsyncSession) -> List[ServerSquad]:
|
||||
@@ -255,35 +221,6 @@ async def get_random_active_squad_uuid(
|
||||
return fallback_uuid
|
||||
|
||||
|
||||
async def choose_least_loaded_server_in_category(
|
||||
db: AsyncSession,
|
||||
category_id: int,
|
||||
promo_group_id: Optional[int] = None,
|
||||
) -> Optional[ServerSquad]:
|
||||
"""Выбирает наименее загруженный сервер в категории."""
|
||||
|
||||
category_squads = await get_available_server_squads(
|
||||
db,
|
||||
promo_group_id=promo_group_id,
|
||||
category_id=category_id,
|
||||
only_with_capacity=True,
|
||||
)
|
||||
|
||||
if not category_squads:
|
||||
return None
|
||||
|
||||
def load_key(squad: ServerSquad) -> tuple:
|
||||
max_users = squad.max_users or 0
|
||||
current_users = squad.current_users or 0
|
||||
if max_users:
|
||||
ratio = current_users / max_users
|
||||
else:
|
||||
ratio = 0
|
||||
return (ratio, current_users, squad.id)
|
||||
|
||||
return min(category_squads, key=load_key)
|
||||
|
||||
|
||||
async def update_server_squad_promo_groups(
|
||||
db: AsyncSession, server_id: int, promo_group_ids: Iterable[int]
|
||||
) -> Optional[ServerSquad]:
|
||||
@@ -334,20 +271,10 @@ async def update_server_squad(
|
||||
"is_available",
|
||||
"sort_order",
|
||||
"is_trial_eligible",
|
||||
"category_id",
|
||||
}
|
||||
|
||||
filtered_updates = {k: v for k, v in updates.items() if k in valid_fields}
|
||||
|
||||
if "category_id" in filtered_updates:
|
||||
category_id = filtered_updates["category_id"]
|
||||
if category_id is not None:
|
||||
category_exists = await db.execute(
|
||||
select(ServerCategory.id).where(ServerCategory.id == category_id)
|
||||
)
|
||||
if category_exists.scalar_one_or_none() is None:
|
||||
raise ValueError("Server category not found")
|
||||
|
||||
|
||||
if not filtered_updates:
|
||||
return None
|
||||
|
||||
|
||||
@@ -1281,22 +1281,6 @@ class PollAnswer(Base):
|
||||
)
|
||||
|
||||
|
||||
class ServerCategory(Base):
|
||||
__tablename__ = "server_categories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), nullable=False, unique=True)
|
||||
sort_order = Column(Integer, default=0)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
servers = relationship("ServerSquad", back_populates="category")
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debug helper
|
||||
return f"<ServerCategory(id={self.id}, name={self.name!r})>"
|
||||
|
||||
|
||||
class ServerSquad(Base):
|
||||
__tablename__ = "server_squads"
|
||||
|
||||
@@ -1318,10 +1302,8 @@ class ServerSquad(Base):
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
sort_order = Column(Integer, default=0)
|
||||
|
||||
category_id = Column(Integer, ForeignKey("server_categories.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
max_users = Column(Integer, nullable=True)
|
||||
|
||||
max_users = Column(Integer, nullable=True)
|
||||
current_users = Column(Integer, default=0)
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
@@ -1333,8 +1315,6 @@ class ServerSquad(Base):
|
||||
back_populates="server_squads",
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
category = relationship("ServerCategory", back_populates="servers")
|
||||
|
||||
@property
|
||||
def price_rubles(self) -> float:
|
||||
|
||||
@@ -3053,63 +3053,6 @@ async def ensure_server_promo_groups_setup() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def ensure_server_categories_table() -> bool:
|
||||
logger.info("=== ПРОВЕРКА ТАБЛИЦЫ КАТЕГОРИЙ СЕРВЕРОВ ===")
|
||||
|
||||
try:
|
||||
table_exists = await check_table_exists("server_categories")
|
||||
|
||||
if table_exists:
|
||||
logger.info("ℹ️ Таблица server_categories уже существует")
|
||||
return True
|
||||
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if db_type == "sqlite":
|
||||
create_sql = """
|
||||
CREATE TABLE server_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
elif db_type == "postgresql":
|
||||
create_sql = """
|
||||
CREATE TABLE server_categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
"""
|
||||
else:
|
||||
create_sql = """
|
||||
CREATE TABLE server_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
|
||||
await conn.execute(text(create_sql))
|
||||
logger.info("✅ Таблица server_categories создана")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"Ошибка создания таблицы server_categories: {error}")
|
||||
return False
|
||||
|
||||
|
||||
async def add_server_trial_flag_column() -> bool:
|
||||
column_exists = await check_column_exists('server_squads', 'is_trial_eligible')
|
||||
if column_exists:
|
||||
@@ -3144,53 +3087,6 @@ async def add_server_trial_flag_column() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def add_server_category_column() -> bool:
|
||||
column_exists = await check_column_exists('server_squads', 'category_id')
|
||||
if column_exists:
|
||||
logger.info("Колонка category_id уже существует в server_squads")
|
||||
return True
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
column_def = 'INTEGER'
|
||||
|
||||
await conn.execute(
|
||||
text(f"ALTER TABLE server_squads ADD COLUMN category_id {column_def}")
|
||||
)
|
||||
|
||||
if db_type == 'postgresql':
|
||||
await conn.execute(
|
||||
text(
|
||||
"ALTER TABLE server_squads ADD CONSTRAINT "
|
||||
"fk_server_squads_category FOREIGN KEY (category_id) "
|
||||
"REFERENCES server_categories(id) ON DELETE SET NULL"
|
||||
)
|
||||
)
|
||||
elif db_type != 'sqlite':
|
||||
await conn.execute(
|
||||
text(
|
||||
"ALTER TABLE server_squads ADD CONSTRAINT fk_server_squads_category "
|
||||
"FOREIGN KEY (category_id) REFERENCES server_categories(id) ON DELETE SET NULL"
|
||||
)
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_server_squads_category_id "
|
||||
"ON server_squads(category_id)"
|
||||
)
|
||||
)
|
||||
|
||||
logger.info("✅ Добавлена колонка category_id в server_squads")
|
||||
return True
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f"Ошибка добавления колонки category_id: {error}")
|
||||
return False
|
||||
|
||||
|
||||
async def create_system_settings_table() -> bool:
|
||||
table_exists = await check_table_exists("system_settings")
|
||||
if table_exists:
|
||||
@@ -4143,18 +4039,6 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с настройкой доступа серверов к промогруппам")
|
||||
|
||||
categories_ready = await ensure_server_categories_table()
|
||||
if categories_ready:
|
||||
logger.info("✅ Таблица категорий серверов готова")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с созданием таблицы категорий серверов")
|
||||
|
||||
category_column_ready = await add_server_category_column()
|
||||
if category_column_ready:
|
||||
logger.info("✅ Колонка категории для серверов настроена")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с добавлением колонки категории для серверов")
|
||||
|
||||
logger.info("=== ОБНОВЛЕНИЕ ВНЕШНИХ КЛЮЧЕЙ ===")
|
||||
fk_updated = await fix_foreign_keys_for_user_deletion()
|
||||
if fk_updated:
|
||||
@@ -4231,8 +4115,6 @@ async def check_migration_status():
|
||||
"promo_groups_table": False,
|
||||
"server_promo_groups_table": False,
|
||||
"server_squads_trial_column": False,
|
||||
"server_categories_table": False,
|
||||
"server_squads_category_column": False,
|
||||
"privacy_policies_table": False,
|
||||
"public_offers_table": False,
|
||||
"users_promo_group_column": False,
|
||||
@@ -4266,8 +4148,6 @@ async def check_migration_status():
|
||||
status["promo_groups_table"] = await check_table_exists('promo_groups')
|
||||
status["server_promo_groups_table"] = await check_table_exists('server_squad_promo_groups')
|
||||
status["server_squads_trial_column"] = await check_column_exists('server_squads', 'is_trial_eligible')
|
||||
status["server_categories_table"] = await check_table_exists('server_categories')
|
||||
status["server_squads_category_column"] = await check_column_exists('server_squads', 'category_id')
|
||||
|
||||
status["discount_offers_table"] = await check_table_exists('discount_offers')
|
||||
status["discount_offers_effect_column"] = await check_column_exists('discount_offers', 'effect_type')
|
||||
@@ -4324,8 +4204,6 @@ async def check_migration_status():
|
||||
"promo_groups_table": "Таблица промо-групп",
|
||||
"server_promo_groups_table": "Связи серверов и промогрупп",
|
||||
"server_squads_trial_column": "Колонка триального назначения у серверов",
|
||||
"server_categories_table": "Таблица категорий серверов",
|
||||
"server_squads_category_column": "Колонка категории у серверов",
|
||||
"users_promo_group_column": "Колонка promo_group_id у пользователей",
|
||||
"promo_groups_period_discounts_column": "Колонка period_discounts у промо-групп",
|
||||
"promo_groups_auto_assign_column": "Колонка auto_assign_total_spent_kopeks у промо-групп",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import html
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -20,10 +18,6 @@ from app.database.crud.server_squad import (
|
||||
update_server_squad_promo_groups,
|
||||
get_server_connected_users,
|
||||
)
|
||||
from app.database.crud.server_category import (
|
||||
get_all_server_categories,
|
||||
create_server_category,
|
||||
)
|
||||
from app.database.crud.promo_group import get_promo_groups_with_counts
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
@@ -42,7 +36,6 @@ def _build_server_edit_view(server):
|
||||
)
|
||||
|
||||
trial_status = "✅ Да" if server.is_trial_eligible else "⚪️ Нет"
|
||||
category_name = getattr(getattr(server, "category", None), "name", None) or "Не указана"
|
||||
|
||||
text = f"""
|
||||
🌐 <b>Редактирование сервера</b>
|
||||
@@ -60,7 +53,6 @@ def _build_server_edit_view(server):
|
||||
• Лимит пользователей: {server.max_users or 'Без лимита'}
|
||||
• Текущих пользователей: {server.current_users}
|
||||
• Промогруппы: {promo_groups_text}
|
||||
• Категория: {category_name}
|
||||
• Выдача триала: {trial_status}
|
||||
|
||||
<b>Описание:</b>
|
||||
@@ -105,11 +97,6 @@ def _build_server_edit_view(server):
|
||||
text="📝 Описание", callback_data=f"admin_server_edit_desc_{server.id}"
|
||||
),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="🏷 Категория", callback_data=f"admin_server_edit_category_{server.id}"
|
||||
),
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="❌ Отключить" if server.is_available else "✅ Включить",
|
||||
@@ -1087,225 +1074,6 @@ async def process_server_description_edit(
|
||||
await message.answer("❌ Ошибка при обновлении сервера")
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_server_category_menu(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
server_id = int(callback.data.split("_")[-1])
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
|
||||
if not server:
|
||||
await callback.answer("❌ Сервер не найден!", show_alert=True)
|
||||
return
|
||||
|
||||
categories = await get_all_server_categories(db)
|
||||
|
||||
current_category = getattr(getattr(server, "category", None), "name", None)
|
||||
safe_current = html.escape(current_category or "Не выбрана")
|
||||
|
||||
text_lines = [
|
||||
"🏷 <b>Категория сервера</b>",
|
||||
"",
|
||||
f"Текущая категория: <b>{safe_current}</b>",
|
||||
"",
|
||||
"Выберите категорию из списка или создайте новую:",
|
||||
]
|
||||
|
||||
keyboard_rows: List[List[types.InlineKeyboardButton]] = []
|
||||
|
||||
for category in categories:
|
||||
emoji = "✅" if server.category_id == category.id else "⚪️"
|
||||
keyboard_rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{emoji} {category.name}",
|
||||
callback_data=f"admin_server_category_set_{server.id}_{category.id}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
keyboard_rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="➕ Создать категорию",
|
||||
callback_data=f"admin_server_category_create_{server.id}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
if server.category_id:
|
||||
keyboard_rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="🗑 Удалить категорию",
|
||||
callback_data=f"admin_server_category_clear_{server.id}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
keyboard_rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=f"admin_server_edit_{server.id}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"\n".join(text_lines),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def set_server_category(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
parts = callback.data.split("_")
|
||||
server_id = int(parts[-2])
|
||||
category_id = int(parts[-1])
|
||||
|
||||
server = await update_server_squad(db, server_id, category_id=category_id)
|
||||
|
||||
if not server:
|
||||
await callback.answer("❌ Не удалось обновить категорию", show_alert=True)
|
||||
return
|
||||
|
||||
await cache.delete_pattern("available_countries*")
|
||||
|
||||
text, keyboard = _build_server_edit_view(server)
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer("✅ Категория обновлена")
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def clear_server_category(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
server_id = int(callback.data.split("_")[-1])
|
||||
|
||||
server = await update_server_squad(db, server_id, category_id=None)
|
||||
|
||||
if not server:
|
||||
await callback.answer("❌ Не удалось обновить категорию", show_alert=True)
|
||||
return
|
||||
|
||||
await cache.delete_pattern("available_countries*")
|
||||
|
||||
text, keyboard = _build_server_edit_view(server)
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer("✅ Категория удалена")
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_server_category_creation(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
server_id = int(callback.data.split("_")[-1])
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
|
||||
if not server:
|
||||
await callback.answer("❌ Сервер не найден!", show_alert=True)
|
||||
return
|
||||
|
||||
await state.set_data({"server_id": server_id})
|
||||
await state.set_state(AdminStates.creating_server_category)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🏷 <b>Новая категория</b>\n\n"
|
||||
"Отправьте название новой категории для сервера:",
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️ Назад", callback_data=f"admin_server_edit_category_{server_id}"
|
||||
)
|
||||
]
|
||||
]
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_server_category_creation(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
data = await state.get_data()
|
||||
server_id = data.get("server_id")
|
||||
|
||||
if not server_id:
|
||||
await message.answer("❌ Сервер не найден")
|
||||
return
|
||||
|
||||
category_name = (message.text or "").strip()
|
||||
|
||||
if not (2 <= len(category_name) <= 255):
|
||||
await message.answer("❌ Название категории должно содержать от 2 до 255 символов")
|
||||
return
|
||||
|
||||
try:
|
||||
category = await create_server_category(db, name=category_name)
|
||||
except Exception as error:
|
||||
logger.error("Не удалось создать категорию %s: %s", category_name, error)
|
||||
await message.answer("❌ Не удалось создать категорию. Попробуйте другое название")
|
||||
return
|
||||
|
||||
server = await update_server_squad(db, server_id, category_id=category.id)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await cache.delete_pattern("available_countries*")
|
||||
|
||||
if not server:
|
||||
await message.answer("❌ Категория создана, но не удалось обновить сервер")
|
||||
return
|
||||
|
||||
safe_name = html.escape(category.name)
|
||||
await message.answer(
|
||||
f"✅ Категория <b>{safe_name}</b> создана и назначена серверу",
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="🔙 К серверу", callback_data=f"admin_server_edit_{server_id}"
|
||||
)
|
||||
]
|
||||
]
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_server_edit_promo_groups(
|
||||
@@ -1526,8 +1294,7 @@ def register_handlers(dp: Dispatcher):
|
||||
& ~F.data.contains("country")
|
||||
& ~F.data.contains("limit")
|
||||
& ~F.data.contains("desc")
|
||||
& ~F.data.contains("promo")
|
||||
& ~F.data.contains("category"),
|
||||
& ~F.data.contains("promo"),
|
||||
)
|
||||
dp.callback_query.register(toggle_server_availability, F.data.startswith("admin_server_toggle_"))
|
||||
dp.callback_query.register(toggle_server_trial_assignment, F.data.startswith("admin_server_trial_"))
|
||||
@@ -1537,19 +1304,14 @@ def register_handlers(dp: Dispatcher):
|
||||
dp.callback_query.register(start_server_edit_price, F.data.startswith("admin_server_edit_price_"))
|
||||
dp.callback_query.register(start_server_edit_country, F.data.startswith("admin_server_edit_country_"))
|
||||
dp.callback_query.register(start_server_edit_promo_groups, F.data.startswith("admin_server_edit_promo_"))
|
||||
dp.callback_query.register(start_server_edit_limit, F.data.startswith("admin_server_edit_limit_"))
|
||||
dp.callback_query.register(start_server_edit_description, F.data.startswith("admin_server_edit_desc_"))
|
||||
dp.callback_query.register(show_server_category_menu, F.data.startswith("admin_server_edit_category_"))
|
||||
dp.callback_query.register(set_server_category, F.data.startswith("admin_server_category_set_"))
|
||||
dp.callback_query.register(clear_server_category, F.data.startswith("admin_server_category_clear_"))
|
||||
dp.callback_query.register(start_server_category_creation, F.data.startswith("admin_server_category_create_"))
|
||||
|
||||
dp.callback_query.register(start_server_edit_limit, F.data.startswith("admin_server_edit_limit_"))
|
||||
dp.callback_query.register(start_server_edit_description, F.data.startswith("admin_server_edit_desc_"))
|
||||
|
||||
dp.message.register(process_server_name_edit, AdminStates.editing_server_name)
|
||||
dp.message.register(process_server_price_edit, AdminStates.editing_server_price)
|
||||
dp.message.register(process_server_country_edit, AdminStates.editing_server_country)
|
||||
dp.message.register(process_server_limit_edit, AdminStates.editing_server_limit)
|
||||
dp.message.register(process_server_country_edit, AdminStates.editing_server_country)
|
||||
dp.message.register(process_server_limit_edit, AdminStates.editing_server_limit)
|
||||
dp.message.register(process_server_description_edit, AdminStates.editing_server_description)
|
||||
dp.message.register(process_server_category_creation, AdminStates.creating_server_category)
|
||||
dp.callback_query.register(toggle_server_promo_group, F.data.startswith("admin_server_promo_toggle_"))
|
||||
dp.callback_query.register(save_server_promo_groups, F.data.startswith("admin_server_promo_save_"))
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from app.database.models import (
|
||||
User, Subscription, Transaction, PromoCode, PromoCodeUse,
|
||||
ReferralEarning, Squad, ServiceRule, SystemSetting, MonitoringLog,
|
||||
SubscriptionConversion, SentNotification, BroadcastHistory,
|
||||
ServerCategory, ServerSquad, SubscriptionServer, UserMessage, YooKassaPayment,
|
||||
ServerSquad, SubscriptionServer, UserMessage, YooKassaPayment,
|
||||
CryptoBotPayment, WelcomeText, Base, PromoGroup, AdvertisingCampaign,
|
||||
AdvertisingCampaignRegistration, SupportAuditLog, Ticket, TicketMessage,
|
||||
MulenPayPayment, Pal24Payment, DiscountOffer, WebApiToken,
|
||||
@@ -68,7 +68,6 @@ class BackupService:
|
||||
SystemSetting,
|
||||
ServiceRule,
|
||||
Squad,
|
||||
ServerCategory,
|
||||
ServerSquad,
|
||||
PromoGroup,
|
||||
User,
|
||||
@@ -727,7 +726,7 @@ class BackupService:
|
||||
"mulenpay_payments", "pal24_payments",
|
||||
"transactions", "welcome_texts", "subscriptions",
|
||||
"promocodes", "users", "promo_groups",
|
||||
"server_squads", "server_categories", "squads", "service_rules",
|
||||
"server_squads", "squads", "service_rules",
|
||||
"system_settings", "web_api_tokens", "monitoring_logs"
|
||||
]
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from app.database.crud.server_squad import (
|
||||
get_available_server_squads,
|
||||
get_server_ids_by_uuids,
|
||||
get_server_squad_by_uuid,
|
||||
choose_least_loaded_server_in_category,
|
||||
)
|
||||
from app.database.crud.subscription import (
|
||||
add_subscription_servers,
|
||||
@@ -104,8 +103,6 @@ class PurchaseServerOption:
|
||||
original_price_label: Optional[str] = None
|
||||
discount_percent: int = 0
|
||||
is_available: bool = True
|
||||
category_id: Optional[int] = None
|
||||
is_category: bool = False
|
||||
|
||||
def to_payload(self) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
@@ -122,10 +119,6 @@ class PurchaseServerOption:
|
||||
payload["original_price_label"] = self.original_price_label
|
||||
if self.discount_percent:
|
||||
payload["discount_percent"] = self.discount_percent
|
||||
if self.category_id is not None:
|
||||
payload["category_id"] = self.category_id
|
||||
if self.is_category:
|
||||
payload["is_category"] = True
|
||||
return payload
|
||||
|
||||
|
||||
@@ -136,8 +129,6 @@ class PurchaseServersConfig:
|
||||
max_selectable: int
|
||||
default_selection: List[str]
|
||||
hint: Optional[str] = None
|
||||
category_mapping: Dict[str, List[str]] = field(default_factory=dict)
|
||||
is_category_based: bool = False
|
||||
|
||||
def to_payload(self) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
@@ -149,10 +140,6 @@ class PurchaseServersConfig:
|
||||
}
|
||||
if self.hint:
|
||||
payload["hint"] = self.hint
|
||||
if self.category_mapping:
|
||||
payload["category_mapping"] = self.category_mapping
|
||||
if self.is_category_based:
|
||||
payload["is_category_based"] = True
|
||||
return payload
|
||||
|
||||
|
||||
@@ -268,7 +255,6 @@ class PurchaseOptionsContext:
|
||||
default_period: PurchasePeriodConfig
|
||||
period_map: Dict[str, PurchasePeriodConfig]
|
||||
server_uuid_to_id: Dict[str, int]
|
||||
category_mapping: Dict[str, List[str]]
|
||||
payload: Dict[str, Any]
|
||||
|
||||
|
||||
@@ -427,7 +413,6 @@ class MiniAppSubscriptionPurchaseService:
|
||||
user,
|
||||
texts,
|
||||
period_days,
|
||||
available_servers,
|
||||
server_catalog,
|
||||
default_connected,
|
||||
)
|
||||
@@ -498,8 +483,6 @@ class MiniAppSubscriptionPurchaseService:
|
||||
"selection": default_selection,
|
||||
"summary": None,
|
||||
}
|
||||
if default_period.servers.category_mapping:
|
||||
payload["server_category_mapping"] = default_period.servers.category_mapping
|
||||
|
||||
return PurchaseOptionsContext(
|
||||
user=user,
|
||||
@@ -510,7 +493,6 @@ class MiniAppSubscriptionPurchaseService:
|
||||
default_period=default_period,
|
||||
period_map=period_map,
|
||||
server_uuid_to_id=server_uuid_to_id,
|
||||
category_mapping=default_period.servers.category_mapping,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
@@ -584,123 +566,25 @@ class MiniAppSubscriptionPurchaseService:
|
||||
user: User,
|
||||
texts,
|
||||
period_days: int,
|
||||
available_servers: List[ServerSquad],
|
||||
server_catalog: Dict[str, ServerSquad],
|
||||
default_selection: List[str],
|
||||
) -> PurchaseServersConfig:
|
||||
discount_percent = user.get_promo_discount("servers", period_days)
|
||||
default_selection_set = set(default_selection)
|
||||
available_server_ids = {server.squad_uuid for server in available_servers}
|
||||
|
||||
category_data: Dict[str, Dict[str, Any]] = {}
|
||||
uncategorized_servers: List[ServerSquad] = []
|
||||
options: List[PurchaseServerOption] = []
|
||||
|
||||
for uuid, server in server_catalog.items():
|
||||
include_server = uuid in available_server_ids or uuid in default_selection_set
|
||||
if not include_server:
|
||||
continue
|
||||
|
||||
if server.category_id and getattr(server, "category", None):
|
||||
key = f"category:{server.category_id}"
|
||||
data = category_data.setdefault(
|
||||
key,
|
||||
{
|
||||
"category": server.category,
|
||||
"servers": [],
|
||||
"min_price": None,
|
||||
"min_available_price": None,
|
||||
"has_available": False,
|
||||
},
|
||||
)
|
||||
data["servers"].append(server)
|
||||
|
||||
price = server.price_kopeks or 0
|
||||
if data["min_price"] is None:
|
||||
data["min_price"] = price
|
||||
else:
|
||||
data["min_price"] = min(data["min_price"], price)
|
||||
|
||||
if getattr(server, "is_available", True) and not getattr(server, "is_full", False):
|
||||
data["has_available"] = True
|
||||
if data["min_available_price"] is None:
|
||||
data["min_available_price"] = price
|
||||
else:
|
||||
data["min_available_price"] = min(data["min_available_price"], price)
|
||||
else:
|
||||
uncategorized_servers.append(server)
|
||||
|
||||
category_items = list(category_data.items())
|
||||
category_items.sort(
|
||||
key=lambda item: (
|
||||
getattr(item[1]["category"], "sort_order", 0),
|
||||
getattr(item[1]["category"], "name", item[0]),
|
||||
)
|
||||
)
|
||||
|
||||
options: List[PurchaseServerOption] = []
|
||||
category_mapping: Dict[str, List[str]] = {}
|
||||
|
||||
for key, data in category_items:
|
||||
category = data["category"]
|
||||
base_price = data["min_available_price"]
|
||||
if base_price is None:
|
||||
base_price = data["min_price"] or 0
|
||||
discounted_per_month, discount_value = _apply_percentage_discount(base_price, discount_percent)
|
||||
option = PurchaseServerOption(
|
||||
uuid=key,
|
||||
name=getattr(category, "name", key),
|
||||
price_per_month=discounted_per_month,
|
||||
price_label=texts.format_price(discounted_per_month),
|
||||
original_price_per_month=base_price,
|
||||
original_price_label=(
|
||||
texts.format_price(base_price)
|
||||
if discount_value and base_price != discounted_per_month
|
||||
else None
|
||||
),
|
||||
discount_percent=max(0, discount_percent),
|
||||
is_available=data["has_available"],
|
||||
category_id=getattr(category, "id", None),
|
||||
is_category=True,
|
||||
)
|
||||
options.append(option)
|
||||
category_mapping[key] = [srv.squad_uuid for srv in data["servers"]]
|
||||
|
||||
for server in uncategorized_servers:
|
||||
option = _build_server_option(server, discount_percent, texts)
|
||||
options.append(option)
|
||||
|
||||
if not options:
|
||||
default_values: List[str] = []
|
||||
else:
|
||||
option_lookup = {option.uuid for option in options}
|
||||
resolved_defaults: List[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for uuid in default_selection:
|
||||
server = server_catalog.get(uuid)
|
||||
if server and server.category_id:
|
||||
key = f"category:{server.category_id}"
|
||||
if key in option_lookup and key not in seen:
|
||||
resolved_defaults.append(key)
|
||||
seen.add(key)
|
||||
continue
|
||||
if uuid in option_lookup and uuid not in seen:
|
||||
resolved_defaults.append(uuid)
|
||||
seen.add(uuid)
|
||||
|
||||
if not resolved_defaults:
|
||||
resolved_defaults = [options[0].uuid]
|
||||
|
||||
default_values = resolved_defaults
|
||||
default_selection = []
|
||||
|
||||
return PurchaseServersConfig(
|
||||
options=options,
|
||||
min_selectable=1 if options else 0,
|
||||
max_selectable=len(options),
|
||||
default_selection=default_values,
|
||||
default_selection=default_selection if default_selection else [opt.uuid for opt in options[:1]],
|
||||
hint=None,
|
||||
category_mapping=category_mapping,
|
||||
is_category_based=bool(category_mapping),
|
||||
)
|
||||
|
||||
def _build_devices_config(
|
||||
@@ -739,40 +623,6 @@ class MiniAppSubscriptionPurchaseService:
|
||||
hint=None,
|
||||
)
|
||||
|
||||
async def _resolve_selected_servers(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
context: PurchaseOptionsContext,
|
||||
selected_servers: List[str],
|
||||
) -> List[str]:
|
||||
resolved: List[str] = []
|
||||
promo_group_id = getattr(context.user, "promo_group_id", None)
|
||||
|
||||
for identifier in selected_servers:
|
||||
if identifier.startswith("category:"):
|
||||
if identifier not in context.category_mapping:
|
||||
raise PurchaseValidationError("Invalid server category selection", code="invalid_servers")
|
||||
try:
|
||||
category_id = int(identifier.split(":", 1)[1])
|
||||
except (TypeError, ValueError) as error:
|
||||
raise PurchaseValidationError("Invalid server category selection", code="invalid_servers") from error
|
||||
|
||||
squad = await choose_least_loaded_server_in_category(
|
||||
db,
|
||||
category_id=category_id,
|
||||
promo_group_id=promo_group_id,
|
||||
)
|
||||
if not squad:
|
||||
raise PurchaseValidationError(
|
||||
"No available servers in selected category",
|
||||
code="no_servers_in_category",
|
||||
)
|
||||
resolved.append(squad.squad_uuid)
|
||||
else:
|
||||
resolved.append(identifier)
|
||||
|
||||
return resolved
|
||||
|
||||
def parse_selection(
|
||||
self,
|
||||
context: PurchaseOptionsContext,
|
||||
@@ -872,22 +722,14 @@ class MiniAppSubscriptionPurchaseService:
|
||||
texts = get_texts(getattr(context.user, "language", None))
|
||||
months = selection.period.months
|
||||
|
||||
resolved_servers = await self._resolve_selected_servers(db, context, selection.servers)
|
||||
resolved_selection = PurchaseSelection(
|
||||
period=selection.period,
|
||||
traffic_value=selection.traffic_value,
|
||||
servers=resolved_servers,
|
||||
devices=selection.devices,
|
||||
)
|
||||
|
||||
server_ids = await get_server_ids_by_uuids(db, resolved_selection.servers)
|
||||
if len(server_ids) != len(resolved_selection.servers):
|
||||
server_ids = await get_server_ids_by_uuids(db, selection.servers)
|
||||
if len(server_ids) != len(selection.servers):
|
||||
raise PurchaseValidationError("Some selected servers are not available", code="invalid_servers")
|
||||
|
||||
total_without_promo, details = await self._calculate_base_total(
|
||||
db,
|
||||
context.user,
|
||||
resolved_selection,
|
||||
selection,
|
||||
server_ids,
|
||||
)
|
||||
|
||||
@@ -926,7 +768,7 @@ class MiniAppSubscriptionPurchaseService:
|
||||
raise PurchaseValidationError("Failed to validate pricing", code="calculation_error")
|
||||
|
||||
return PurchasePricingResult(
|
||||
selection=resolved_selection,
|
||||
selection=selection,
|
||||
server_ids=server_ids,
|
||||
server_prices_for_period=list(details.get("servers_individual_prices", [])),
|
||||
base_original_total=base_original_total,
|
||||
|
||||
@@ -113,7 +113,6 @@ class AdminStates(StatesGroup):
|
||||
editing_server_limit = State()
|
||||
editing_server_description = State()
|
||||
editing_server_promo_groups = State()
|
||||
creating_server_category = State()
|
||||
|
||||
creating_server_uuid = State()
|
||||
creating_server_name = State()
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.database.crud import server_squad as server_squad_crud
|
||||
from app.services.subscription_auto_purchase_service import auto_purchase_saved_cart_after_topup
|
||||
from app.services.subscription_purchase_service import (
|
||||
MiniAppSubscriptionPurchaseService,
|
||||
PurchaseDevicesConfig,
|
||||
PurchaseOptionsContext,
|
||||
PurchasePeriodConfig,
|
||||
@@ -95,7 +92,6 @@ async def test_auto_purchase_saved_cart_after_topup_success(monkeypatch):
|
||||
default_period=period_config,
|
||||
period_map={"days:30": period_config},
|
||||
server_uuid_to_id={"ru": 1},
|
||||
category_mapping={},
|
||||
payload={},
|
||||
)
|
||||
|
||||
@@ -705,110 +701,3 @@ async def test_auto_purchase_trial_remaining_days_transferred(monkeypatch):
|
||||
expected_end = trial_end + timedelta(days=32) # trial_end + (30 + 2)
|
||||
actual_delta = (subscription.end_date - trial_end).days
|
||||
assert actual_delta == 32, f"Expected 32 days extension (30 + 2 bonus), got {actual_delta}"
|
||||
|
||||
|
||||
def test_build_servers_config_category_price_ignores_full_squads():
|
||||
service = MiniAppSubscriptionPurchaseService()
|
||||
|
||||
user = MagicMock(spec=User)
|
||||
user.get_promo_discount = MagicMock(return_value=0)
|
||||
|
||||
texts = DummyTexts()
|
||||
|
||||
category = MagicMock()
|
||||
category.id = 7
|
||||
category.name = "Category"
|
||||
category.sort_order = 1
|
||||
|
||||
full_server = MagicMock()
|
||||
full_server.squad_uuid = "srv-cheap"
|
||||
full_server.category_id = category.id
|
||||
full_server.category = category
|
||||
full_server.price_kopeks = 10_000
|
||||
full_server.is_available = True
|
||||
full_server.is_full = True
|
||||
|
||||
available_server = MagicMock()
|
||||
available_server.squad_uuid = "srv-available"
|
||||
available_server.category_id = category.id
|
||||
available_server.category = category
|
||||
available_server.price_kopeks = 20_000
|
||||
available_server.is_available = True
|
||||
available_server.is_full = False
|
||||
|
||||
available_servers = [full_server, available_server]
|
||||
server_catalog = {srv.squad_uuid: srv for srv in available_servers}
|
||||
|
||||
config = service._build_servers_config(
|
||||
user,
|
||||
texts,
|
||||
period_days=30,
|
||||
available_servers=available_servers,
|
||||
server_catalog=server_catalog,
|
||||
default_selection=[],
|
||||
)
|
||||
|
||||
category_option = next(option for option in config.options if option.is_category)
|
||||
|
||||
assert category_option.price_per_month == available_server.price_kopeks
|
||||
assert category_option.original_price_per_month == available_server.price_kopeks
|
||||
assert config.category_mapping[f"category:{category.id}"] == [
|
||||
full_server.squad_uuid,
|
||||
available_server.squad_uuid,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_choose_least_loaded_server_in_category_prefers_lowest_load(monkeypatch):
|
||||
capture: Dict[str, Any] = {}
|
||||
|
||||
async def fake_get_available(db, promo_group_id=None, category_id=None, only_with_capacity=False):
|
||||
capture["only_with_capacity"] = only_with_capacity
|
||||
high_load = MagicMock()
|
||||
high_load.max_users = 100
|
||||
high_load.current_users = 80
|
||||
high_load.id = 10
|
||||
|
||||
low_load = MagicMock()
|
||||
low_load.max_users = 200
|
||||
low_load.current_users = 20
|
||||
low_load.id = 5
|
||||
return [high_load, low_load]
|
||||
|
||||
monkeypatch.setattr(
|
||||
server_squad_crud,
|
||||
"get_available_server_squads",
|
||||
fake_get_available,
|
||||
)
|
||||
|
||||
dummy_db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
squad = await server_squad_crud.choose_least_loaded_server_in_category(
|
||||
dummy_db,
|
||||
category_id=1,
|
||||
promo_group_id=2,
|
||||
)
|
||||
|
||||
assert squad.current_users == 20
|
||||
assert capture["only_with_capacity"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_choose_least_loaded_server_in_category_returns_none_when_empty(monkeypatch):
|
||||
async def fake_get_available(db, promo_group_id=None, category_id=None, only_with_capacity=False):
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(
|
||||
server_squad_crud,
|
||||
"get_available_server_squads",
|
||||
fake_get_available,
|
||||
)
|
||||
|
||||
dummy_db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
squad = await server_squad_crud.choose_least_loaded_server_in_category(
|
||||
dummy_db,
|
||||
category_id=5,
|
||||
)
|
||||
|
||||
assert squad is None
|
||||
|
||||
Reference in New Issue
Block a user