from __future__ import annotations import logging from typing import Any, Optional from fastapi import APIRouter, Depends, HTTPException, Query, Security, status from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased, selectinload from app.database.crud.referral import get_user_referral_stats from app.database.crud.user import ( get_user_by_id, get_user_by_telegram_id, update_user, ) from app.database.models import User from app.services.partner_stats_service import PartnerStatsService from app.utils.user_utils import ( get_detailed_referral_list, get_effective_referral_commission_percent, ) from ..dependencies import get_db_session, require_api_token from ..schemas.partners import ( ChangeData, DailyStats, DailyStatsResponse, EarningsByPeriod, GlobalPartnerStats, GlobalPartnerSummary, NewReferralsByPeriod, PartnerReferralCommissionUpdate, PartnerReferralItem, PartnerReferralList, PartnerReferrerDetail, PartnerReferrerItem, PartnerReferrerListResponse, PayoutsByPeriod, PeriodChange, PeriodComparisonResponse, PeriodData, ReferralsCountByPeriod, ReferrerDetailedStats, ReferrerSummary, TopReferralItem, TopReferralsResponse, TopReferrerItem, TopReferrersResponse, ) logger = logging.getLogger(__name__) router = APIRouter() def _apply_search_filter(query, search: str): search_lower = f"%{search.lower()}%" conditions = [ func.lower(User.username).like(search_lower), func.lower(User.first_name).like(search_lower), func.lower(User.last_name).like(search_lower), func.lower(User.referral_code).like(search_lower), ] if search.isdigit(): conditions.append(User.telegram_id == int(search)) conditions.append(User.id == int(search)) return query.where(or_(*conditions)) def _serialize_referrer(user: User, stats: dict) -> PartnerReferrerItem: total_earned_kopeks = int(stats.get("total_earned_kopeks") or 0) month_earned_kopeks = int(stats.get("month_earned_kopeks") or 0) return PartnerReferrerItem( id=user.id, telegram_id=user.telegram_id, username=user.username, first_name=user.first_name, last_name=user.last_name, referral_code=user.referral_code, referral_commission_percent=getattr(user, "referral_commission_percent", None), effective_referral_commission_percent=get_effective_referral_commission_percent(user), invited_count=int(stats.get("invited_count") or 0), active_referrals=int(stats.get("active_referrals") or 0), total_earned_kopeks=total_earned_kopeks, total_earned_rubles=round(total_earned_kopeks / 100, 2), month_earned_kopeks=month_earned_kopeks, month_earned_rubles=round(month_earned_kopeks / 100, 2), created_at=user.created_at, last_activity=user.last_activity, ) def _serialize_referral_item(referral: dict) -> PartnerReferralItem: balance_kopeks = int(referral.get("balance_kopeks") or 0) total_earned_kopeks = int(referral.get("total_earned_kopeks") or 0) return PartnerReferralItem( id=int(referral.get("id")), telegram_id=int(referral.get("telegram_id")), full_name=str(referral.get("full_name")), username=referral.get("username"), created_at=referral.get("created_at"), last_activity=referral.get("last_activity"), has_made_first_topup=bool(referral.get("has_made_first_topup", False)), balance_kopeks=balance_kopeks, balance_rubles=round(balance_kopeks / 100, 2), total_earned_kopeks=total_earned_kopeks, total_earned_rubles=round(total_earned_kopeks / 100, 2), topups_count=int(referral.get("topups_count") or 0), days_since_registration=int(referral.get("days_since_registration") or 0), days_since_activity=referral.get("days_since_activity"), status=str(referral.get("status") or "inactive"), ) @router.get("/referrers", response_model=PartnerReferrerListResponse) async def list_referrers( _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), search: Optional[str] = Query(default=None), ) -> PartnerReferrerListResponse: referral_alias = aliased(User) has_referrals = ( select(referral_alias.id) .where(referral_alias.referred_by_id == User.id) .exists() ) base_query = select(User).options(selectinload(User.referrer)).where( or_(User.referral_code.isnot(None), has_referrals) ) if search: base_query = _apply_search_filter(base_query, search) total_query = base_query.with_only_columns(func.count()).order_by(None) total = await db.scalar(total_query) or 0 result = await db.execute( base_query.order_by(User.created_at.desc()).offset(offset).limit(limit) ) referrers = result.scalars().unique().all() items: list[PartnerReferrerItem] = [] for referrer in referrers: stats = await get_user_referral_stats(db, referrer.id) items.append(_serialize_referrer(referrer, stats)) return PartnerReferrerListResponse( items=items, total=int(total), limit=limit, offset=offset, ) @router.get("/referrers/{user_id}", response_model=PartnerReferrerDetail) async def get_referrer_detail( user_id: int, _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ) -> PartnerReferrerDetail: user = await get_user_by_telegram_id(db, user_id) if not user: user = await get_user_by_id(db, user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") stats = await get_user_referral_stats(db, user.id) referrer_item = _serialize_referrer(user, stats) referrals_data = await get_detailed_referral_list(db, user.id, limit=limit, offset=offset) referral_items = [ _serialize_referral_item(referral) for referral in referrals_data.get("referrals", []) ] referrals_list = PartnerReferralList( items=referral_items, total=int(referrals_data.get("total_count") or 0), limit=limit, offset=offset, has_next=bool(referrals_data.get("has_next")), has_prev=bool(referrals_data.get("has_prev")), current_page=int(referrals_data.get("current_page") or 1), total_pages=int(referrals_data.get("total_pages") or 1), ) return PartnerReferrerDetail(referrer=referrer_item, referrals=referrals_list) @router.patch("/referrers/{user_id}/commission", response_model=PartnerReferrerItem) async def update_referrer_commission( user_id: int, payload: PartnerReferralCommissionUpdate, _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> PartnerReferrerItem: user = await get_user_by_telegram_id(db, user_id) if not user: user = await get_user_by_id(db, user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") await update_user( db, user, referral_commission_percent=payload.referral_commission_percent, ) stats = await get_user_referral_stats(db, user.id) return _serialize_referrer(user, stats) # ============================================================================ # РАСШИРЕННАЯ СТАТИСТИКА ПАРТНЁРОВ # ============================================================================ @router.get("/stats", response_model=GlobalPartnerStats) async def get_global_partner_stats( days: int = Query(30, ge=1, le=365), _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> GlobalPartnerStats: """Глобальная статистика партнёрской программы.""" data = await PartnerStatsService.get_global_partner_stats(db, days) return GlobalPartnerStats( summary=GlobalPartnerSummary(**data["summary"]), payouts=PayoutsByPeriod(**data["payouts"]), new_referrals=NewReferralsByPeriod(**data["new_referrals"]), ) @router.get("/stats/daily", response_model=DailyStatsResponse) async def get_global_daily_stats( days: int = Query(30, ge=1, le=365), _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> DailyStatsResponse: """Глобальная статистика по дням.""" data = await PartnerStatsService.get_global_daily_stats(db, days) return DailyStatsResponse( items=[DailyStats(**item) for item in data], days=days, user_id=None, ) @router.get("/stats/top-referrers", response_model=TopReferrersResponse) async def get_top_referrers( limit: int = Query(10, ge=1, le=100), days: Optional[int] = Query(None, ge=1, le=365), _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> TopReferrersResponse: """Топ рефереров по заработку.""" data = await PartnerStatsService.get_top_referrers(db, limit, days) return TopReferrersResponse( items=[TopReferrerItem(**item) for item in data], days=days, ) @router.get("/referrers/{user_id}/stats", response_model=ReferrerDetailedStats) async def get_referrer_detailed_stats( user_id: int, _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> ReferrerDetailedStats: """Детальная статистика реферера.""" user = await get_user_by_telegram_id(db, user_id) if not user: user = await get_user_by_id(db, user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") data = await PartnerStatsService.get_referrer_detailed_stats(db, user.id) return ReferrerDetailedStats( user_id=data["user_id"], summary=ReferrerSummary(**data["summary"]), earnings=EarningsByPeriod(**data["earnings"]), referrals_count=ReferralsCountByPeriod(**data["referrals_count"]), ) @router.get("/referrers/{user_id}/stats/daily", response_model=DailyStatsResponse) async def get_referrer_daily_stats( user_id: int, days: int = Query(30, ge=1, le=365), _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> DailyStatsResponse: """Статистика реферера по дням.""" user = await get_user_by_telegram_id(db, user_id) if not user: user = await get_user_by_id(db, user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") data = await PartnerStatsService.get_referrer_daily_stats(db, user.id, days) return DailyStatsResponse( items=[DailyStats(**item) for item in data], days=days, user_id=user.id, ) @router.get("/referrers/{user_id}/stats/top-referrals", response_model=TopReferralsResponse) async def get_referrer_top_referrals( user_id: int, limit: int = Query(10, ge=1, le=100), _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> TopReferralsResponse: """Топ рефералов реферера по принесённому доходу.""" user = await get_user_by_telegram_id(db, user_id) if not user: user = await get_user_by_id(db, user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") data = await PartnerStatsService.get_referrer_top_referrals(db, user.id, limit) return TopReferralsResponse( items=[TopReferralItem(**item) for item in data], user_id=user.id, ) @router.get("/referrers/{user_id}/stats/compare", response_model=PeriodComparisonResponse) async def get_referrer_period_comparison( user_id: int, current_days: int = Query(7, ge=1, le=365), previous_days: int = Query(7, ge=1, le=365), _: Any = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> PeriodComparisonResponse: """Сравнение периодов для реферера.""" user = await get_user_by_telegram_id(db, user_id) if not user: user = await get_user_by_id(db, user_id) if not user: raise HTTPException(status.HTTP_404_NOT_FOUND, "User not found") data = await PartnerStatsService.get_referrer_period_comparison( db, user.id, current_days, previous_days ) return PeriodComparisonResponse( current_period=PeriodData(**data["current_period"]), previous_period=PeriodData(**data["previous_period"]), change=PeriodChange( referrals=ChangeData(**data["change"]["referrals"]), earnings=ChangeData(**data["change"]["earnings"]), ), user_id=user.id, )