"""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, )