Files
remnawave-bedolaga-telegram…/app/webapi/routes/partners.py

377 lines
13 KiB
Python

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