Files
remnawave-bedolaga-telegram…/app/cabinet/routes/admin_tariffs.py
2026-01-16 08:50:34 +03:00

521 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Admin routes for managing tariffs in cabinet."""
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.database.models import User, Tariff, Subscription, ServerSquad, PromoGroup
from app.database.crud.tariff import (
get_all_tariffs,
get_tariff_by_id,
create_tariff,
update_tariff,
delete_tariff,
get_tariff_subscriptions_count,
set_tariff_promo_groups,
load_period_prices_from_db,
)
from app.database.crud.server_squad import get_all_server_squads
from ..dependencies import get_cabinet_db, get_current_admin_user
from ..schemas.tariffs import (
TariffListResponse,
TariffListItem,
TariffDetailResponse,
TariffCreateRequest,
TariffUpdateRequest,
TariffToggleResponse,
TariffTrialResponse,
TariffStatsResponse,
PeriodPrice,
ServerInfo,
PromoGroupInfo,
ServerTrafficLimit,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/tariffs", tags=["Cabinet Admin Tariffs"])
async def _get_tariff_servers(
db: AsyncSession,
allowed_squads: List[str],
server_traffic_limits: dict = None
) -> List[ServerInfo]:
"""Get server info for tariff."""
servers, _ = await get_all_server_squads(db, available_only=False)
limits = server_traffic_limits or {}
result = []
for server in servers:
# Получаем индивидуальный лимит трафика для сервера
server_limit = None
if server.squad_uuid in limits:
limit_data = limits[server.squad_uuid]
if isinstance(limit_data, dict) and 'traffic_limit_gb' in limit_data:
server_limit = limit_data['traffic_limit_gb']
elif isinstance(limit_data, int):
server_limit = limit_data
result.append(ServerInfo(
id=server.id,
squad_uuid=server.squad_uuid,
display_name=server.display_name,
country_code=server.country_code,
is_selected=server.squad_uuid in allowed_squads,
traffic_limit_gb=server_limit,
))
return result
async def _get_tariff_promo_groups(db: AsyncSession, tariff: Tariff) -> List[PromoGroupInfo]:
"""Get promo group info for tariff."""
result = await db.execute(select(PromoGroup).order_by(PromoGroup.name))
all_groups = result.scalars().all()
selected_ids = {pg.id for pg in tariff.allowed_promo_groups} if tariff.allowed_promo_groups else set()
return [
PromoGroupInfo(
id=pg.id,
name=pg.name,
is_selected=pg.id in selected_ids,
)
for pg in all_groups
]
def _period_prices_to_list(period_prices: dict) -> List[PeriodPrice]:
"""Convert period_prices dict to list."""
if not period_prices:
return []
return [
PeriodPrice(days=int(days), price_kopeks=price)
for days, price in sorted(period_prices.items(), key=lambda x: int(x[0]))
]
def _period_prices_to_dict(period_prices: List[PeriodPrice]) -> dict:
"""Convert period_prices list to dict."""
return {str(pp.days): pp.price_kopeks for pp in period_prices}
@router.get("", response_model=TariffListResponse)
async def list_tariffs(
include_inactive: bool = True,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get list of all tariffs."""
tariffs = await get_all_tariffs(db, include_inactive=include_inactive)
items = []
for tariff in tariffs:
subs_count = await get_tariff_subscriptions_count(db, tariff.id)
items.append(TariffListItem(
id=tariff.id,
name=tariff.name,
description=tariff.description,
is_active=tariff.is_active,
is_trial_available=tariff.is_trial_available,
is_daily=tariff.is_daily,
daily_price_kopeks=tariff.daily_price_kopeks,
allow_traffic_topup=tariff.allow_traffic_topup,
traffic_limit_gb=tariff.traffic_limit_gb,
device_limit=tariff.device_limit,
tier_level=tariff.tier_level,
display_order=tariff.display_order,
servers_count=len(tariff.allowed_squads or []),
subscriptions_count=subs_count,
created_at=tariff.created_at,
))
return TariffListResponse(tariffs=items, total=len(items))
@router.get("/available-servers", response_model=List[ServerInfo])
async def get_available_servers(
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get list of all servers for tariff selection."""
servers, _ = await get_all_server_squads(db, available_only=False)
return [
ServerInfo(
id=server.id,
squad_uuid=server.squad_uuid,
display_name=server.display_name,
country_code=server.country_code,
is_selected=False,
)
for server in servers
]
@router.get("/{tariff_id}", response_model=TariffDetailResponse)
async def get_tariff(
tariff_id: int,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get detailed tariff info."""
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tariff not found",
)
allowed_squads = tariff.allowed_squads or []
server_traffic_limits = tariff.server_traffic_limits or {}
servers = await _get_tariff_servers(db, allowed_squads, server_traffic_limits)
promo_groups = await _get_tariff_promo_groups(db, tariff)
subs_count = await get_tariff_subscriptions_count(db, tariff.id)
# Преобразуем server_traffic_limits в формат для схемы
server_limits_response = {}
for uuid, limit_data in server_traffic_limits.items():
if isinstance(limit_data, dict):
server_limits_response[uuid] = ServerTrafficLimit(**limit_data)
elif isinstance(limit_data, int):
server_limits_response[uuid] = ServerTrafficLimit(traffic_limit_gb=limit_data)
return TariffDetailResponse(
id=tariff.id,
name=tariff.name,
description=tariff.description,
is_active=tariff.is_active,
is_trial_available=tariff.is_trial_available,
allow_traffic_topup=tariff.allow_traffic_topup,
traffic_topup_enabled=tariff.traffic_topup_enabled,
traffic_topup_packages=tariff.traffic_topup_packages or {},
max_topup_traffic_gb=tariff.max_topup_traffic_gb,
traffic_limit_gb=tariff.traffic_limit_gb,
device_limit=tariff.device_limit,
device_price_kopeks=tariff.device_price_kopeks,
max_device_limit=tariff.max_device_limit,
tier_level=tariff.tier_level,
display_order=tariff.display_order,
period_prices=_period_prices_to_list(tariff.period_prices),
allowed_squads=allowed_squads,
server_traffic_limits=server_limits_response,
servers=servers,
promo_groups=promo_groups,
subscriptions_count=subs_count,
# Произвольное количество дней
custom_days_enabled=tariff.custom_days_enabled,
price_per_day_kopeks=tariff.price_per_day_kopeks,
min_days=tariff.min_days,
max_days=tariff.max_days,
# Произвольный трафик при покупке
custom_traffic_enabled=tariff.custom_traffic_enabled,
traffic_price_per_gb_kopeks=tariff.traffic_price_per_gb_kopeks,
min_traffic_gb=tariff.min_traffic_gb,
max_traffic_gb=tariff.max_traffic_gb,
# Дневной тариф
is_daily=tariff.is_daily,
daily_price_kopeks=tariff.daily_price_kopeks,
# Режим сброса трафика
traffic_reset_mode=tariff.traffic_reset_mode,
created_at=tariff.created_at,
updated_at=tariff.updated_at,
)
@router.post("", response_model=TariffDetailResponse)
async def create_new_tariff(
request: TariffCreateRequest,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Create a new tariff."""
period_prices_dict = _period_prices_to_dict(request.period_prices)
# Преобразуем ServerTrafficLimit в dict для хранения
server_limits_dict = {
uuid: limit.model_dump() for uuid, limit in request.server_traffic_limits.items()
} if request.server_traffic_limits else {}
tariff = await create_tariff(
db=db,
name=request.name,
description=request.description,
is_active=request.is_active,
allow_traffic_topup=request.allow_traffic_topup,
traffic_topup_enabled=request.traffic_topup_enabled,
traffic_topup_packages=request.traffic_topup_packages,
max_topup_traffic_gb=request.max_topup_traffic_gb,
traffic_limit_gb=request.traffic_limit_gb,
device_limit=request.device_limit,
device_price_kopeks=request.device_price_kopeks,
max_device_limit=request.max_device_limit,
tier_level=request.tier_level,
period_prices=period_prices_dict,
allowed_squads=request.allowed_squads,
server_traffic_limits=server_limits_dict,
promo_group_ids=request.promo_group_ids if request.promo_group_ids else None,
# Произвольное количество дней
custom_days_enabled=request.custom_days_enabled,
price_per_day_kopeks=request.price_per_day_kopeks,
min_days=request.min_days,
max_days=request.max_days,
# Произвольный трафик при покупке
custom_traffic_enabled=request.custom_traffic_enabled,
traffic_price_per_gb_kopeks=request.traffic_price_per_gb_kopeks,
min_traffic_gb=request.min_traffic_gb,
max_traffic_gb=request.max_traffic_gb,
# Дневной тариф
is_daily=request.is_daily,
daily_price_kopeks=request.daily_price_kopeks,
# Режим сброса трафика
traffic_reset_mode=request.traffic_reset_mode,
)
logger.info(f"Admin {admin.id} created tariff {tariff.id}: {tariff.name}")
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
# Return full detail
return await get_tariff(tariff.id, admin, db)
@router.put("/{tariff_id}", response_model=TariffDetailResponse)
async def update_existing_tariff(
tariff_id: int,
request: TariffUpdateRequest,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Update an existing tariff."""
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tariff not found",
)
# Build updates dict
updates = {}
if request.name is not None:
updates["name"] = request.name
if request.description is not None:
updates["description"] = request.description
if request.is_active is not None:
updates["is_active"] = request.is_active
if request.allow_traffic_topup is not None:
updates["allow_traffic_topup"] = request.allow_traffic_topup
if request.traffic_topup_enabled is not None:
updates["traffic_topup_enabled"] = request.traffic_topup_enabled
if request.traffic_topup_packages is not None:
updates["traffic_topup_packages"] = request.traffic_topup_packages
if request.max_topup_traffic_gb is not None:
updates["max_topup_traffic_gb"] = request.max_topup_traffic_gb
if request.traffic_limit_gb is not None:
updates["traffic_limit_gb"] = request.traffic_limit_gb
if request.device_limit is not None:
updates["device_limit"] = request.device_limit
if request.device_price_kopeks is not None:
updates["device_price_kopeks"] = request.device_price_kopeks
if request.max_device_limit is not None:
updates["max_device_limit"] = request.max_device_limit
if request.tier_level is not None:
updates["tier_level"] = request.tier_level
if request.display_order is not None:
updates["display_order"] = request.display_order
if request.period_prices is not None:
updates["period_prices"] = _period_prices_to_dict(request.period_prices)
if request.allowed_squads is not None:
updates["allowed_squads"] = request.allowed_squads
if request.server_traffic_limits is not None:
# Преобразуем ServerTrafficLimit в dict для хранения
updates["server_traffic_limits"] = {
uuid: limit.model_dump() for uuid, limit in request.server_traffic_limits.items()
}
# Произвольное количество дней
if request.custom_days_enabled is not None:
updates["custom_days_enabled"] = request.custom_days_enabled
if request.price_per_day_kopeks is not None:
updates["price_per_day_kopeks"] = request.price_per_day_kopeks
if request.min_days is not None:
updates["min_days"] = request.min_days
if request.max_days is not None:
updates["max_days"] = request.max_days
# Произвольный трафик при покупке
if request.custom_traffic_enabled is not None:
updates["custom_traffic_enabled"] = request.custom_traffic_enabled
if request.traffic_price_per_gb_kopeks is not None:
updates["traffic_price_per_gb_kopeks"] = request.traffic_price_per_gb_kopeks
if request.min_traffic_gb is not None:
updates["min_traffic_gb"] = request.min_traffic_gb
if request.max_traffic_gb is not None:
updates["max_traffic_gb"] = request.max_traffic_gb
# Дневной тариф
if request.is_daily is not None:
updates["is_daily"] = request.is_daily
if request.daily_price_kopeks is not None:
updates["daily_price_kopeks"] = request.daily_price_kopeks
# Режим сброса трафика (None допускается как значение для сброса к глобальной настройке)
if 'traffic_reset_mode' in request.model_fields_set:
updates["traffic_reset_mode"] = request.traffic_reset_mode
if updates:
await update_tariff(db, tariff, **updates)
# Update promo groups separately
if request.promo_group_ids is not None:
await set_tariff_promo_groups(db, tariff_id, request.promo_group_ids)
logger.info(f"Admin {admin.id} updated tariff {tariff_id}")
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
return await get_tariff(tariff_id, admin, db)
@router.delete("/{tariff_id}")
async def delete_existing_tariff(
tariff_id: int,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Delete a tariff."""
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tariff not found",
)
# Check if tariff has subscriptions
subs_count = await get_tariff_subscriptions_count(db, tariff_id)
if subs_count > 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot delete tariff with {subs_count} active subscriptions",
)
await delete_tariff(db, tariff)
logger.info(f"Admin {admin.id} deleted tariff {tariff_id}: {tariff.name}")
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
return {"message": "Tariff deleted successfully"}
@router.post("/{tariff_id}/toggle", response_model=TariffToggleResponse)
async def toggle_tariff(
tariff_id: int,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Toggle tariff active status."""
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tariff not found",
)
new_status = not tariff.is_active
await update_tariff(db, tariff, is_active=new_status)
status_text = "activated" if new_status else "deactivated"
logger.info(f"Admin {admin.id} {status_text} tariff {tariff_id}")
# Перезагружаем периоды из БД для синхронизации с ботом
await load_period_prices_from_db(db)
return TariffToggleResponse(
id=tariff_id,
is_active=new_status,
message=f"Tariff {status_text}",
)
@router.post("/{tariff_id}/trial", response_model=TariffTrialResponse)
async def toggle_trial_tariff(
tariff_id: int,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Toggle tariff trial availability."""
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tariff not found",
)
new_status = not tariff.is_trial_available
await update_tariff(db, tariff, is_trial_available=new_status)
status_text = "set as trial" if new_status else "removed from trial"
logger.info(f"Admin {admin.id} {status_text} tariff {tariff_id}")
return TariffTrialResponse(
id=tariff_id,
is_trial_available=new_status,
message=f"Tariff {status_text}",
)
@router.get("/{tariff_id}/stats", response_model=TariffStatsResponse)
async def get_tariff_stats(
tariff_id: int,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Get tariff statistics."""
tariff = await get_tariff_by_id(db, tariff_id)
if not tariff:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tariff not found",
)
# Count subscriptions
total_result = await db.execute(
select(func.count(Subscription.id))
.where(Subscription.tariff_id == tariff_id)
)
total_count = total_result.scalar() or 0
# Count active subscriptions
active_result = await db.execute(
select(func.count(Subscription.id))
.where(
Subscription.tariff_id == tariff_id,
Subscription.status == "active",
)
)
active_count = active_result.scalar() or 0
# Count trial subscriptions
trial_result = await db.execute(
select(func.count(Subscription.id))
.where(
Subscription.tariff_id == tariff_id,
Subscription.is_trial == True,
)
)
trial_count = trial_result.scalar() or 0
# TODO: Calculate revenue from transactions
revenue_kopeks = 0
return TariffStatsResponse(
id=tariff_id,
name=tariff.name,
subscriptions_count=total_count,
active_subscriptions=active_count,
trial_subscriptions=trial_count,
revenue_kopeks=revenue_kopeks,
revenue_rubles=revenue_kopeks / 100,
)