mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-26 22:31:44 +00:00
- Add pyproject.toml with uv and ruff configuration - Pin Python version to 3.13 via .python-version - Add Makefile commands: lint, format, fix - Apply ruff formatting to entire codebase - Remove unused imports (base64 in yookassa/simple_subscription) - Update .gitignore for new config files
372 lines
13 KiB
Python
372 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
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)
|
|
|
|
# Handle email-only users (telegram_id=None)
|
|
raw_telegram_id = referral.get('telegram_id')
|
|
telegram_id = int(raw_telegram_id) if raw_telegram_id is not None else None
|
|
|
|
return PartnerReferralItem(
|
|
id=int(referral.get('id')),
|
|
telegram_id=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: str | None = 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: int | None = 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,
|
|
)
|