Files
remnawave-bedolaga-telegram…/app/services/partner_application_service.py
Fringg 58bfaeaddb feat: add partner system and withdrawal management to cabinet
- Partner application flow: user applies, admin reviews/approves/rejects
- Individual commission % per partner with admin management
- Campaign assignment/unassignment to partners
- Withdrawal system: balance check, create request, cancel
- Admin withdrawal management with risk scoring and fraud analysis
- Database migration: partner_applications table, user partner fields, campaign partner_user_id
- Pydantic schemas with proper validation bounds
- Batch user fetching to prevent N+1 queries
- Row locking on cancel to prevent race conditions
2026-02-17 09:51:36 +03:00

228 lines
7.8 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.

"""Сервис для обработки заявок на партнёрский статус."""
import secrets
import string
from datetime import UTC, datetime
import structlog
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import PartnerApplication, PartnerStatus, User
logger = structlog.get_logger(__name__)
class PartnerApplicationService:
"""Сервис управления партнёрскими заявками."""
async def submit_application(
self,
db: AsyncSession,
user_id: int,
company_name: str | None = None,
website_url: str | None = None,
telegram_channel: str | None = None,
description: str | None = None,
expected_monthly_referrals: int | None = None,
) -> tuple[PartnerApplication | None, str]:
"""
Подаёт заявку на партнёрский статус.
Возвращает (application, error_message).
"""
user = await db.get(User, user_id)
if not user:
return None, 'Пользователь не найден'
if user.partner_status == PartnerStatus.APPROVED.value:
return None, 'Вы уже являетесь партнёром'
if user.partner_status == PartnerStatus.PENDING.value:
return None, 'У вас уже есть заявка на рассмотрении'
application = PartnerApplication(
user_id=user_id,
company_name=company_name,
website_url=website_url,
telegram_channel=telegram_channel,
description=description,
expected_monthly_referrals=expected_monthly_referrals,
)
user.partner_status = PartnerStatus.PENDING.value
db.add(application)
await db.commit()
await db.refresh(application)
logger.info(
'📝 Подана заявка на партнёрство',
user_id=user_id,
application_id=application.id,
)
return application, ''
async def approve_application(
self,
db: AsyncSession,
application_id: int,
admin_id: int,
commission_percent: int,
comment: str | None = None,
) -> tuple[bool, str]:
"""
Одобряет заявку на партнёрство.
Возвращает (success, error_message).
"""
application = await db.get(PartnerApplication, application_id)
if not application:
return False, 'Заявка не найдена'
if application.status != PartnerStatus.PENDING.value:
return False, 'Заявка уже обработана'
user = await db.get(User, application.user_id)
if not user:
return False, 'Пользователь не найден'
# Генерируем реферальный код, если его нет
if not user.referral_code:
user.referral_code = self._generate_referral_code()
user.partner_status = PartnerStatus.APPROVED.value
user.referral_commission_percent = commission_percent
application.status = PartnerStatus.APPROVED.value
application.approved_commission_percent = commission_percent
application.admin_comment = comment
application.processed_by = admin_id
application.processed_at = datetime.now(UTC)
await db.commit()
logger.info(
'✅ Партнёрская заявка одобрена',
application_id=application_id,
user_id=application.user_id,
commission_percent=commission_percent,
admin_id=admin_id,
)
return True, ''
async def reject_application(
self,
db: AsyncSession,
application_id: int,
admin_id: int,
comment: str | None = None,
) -> tuple[bool, str]:
"""Отклоняет заявку на партнёрство."""
application = await db.get(PartnerApplication, application_id)
if not application:
return False, 'Заявка не найдена'
if application.status != PartnerStatus.PENDING.value:
return False, 'Заявка уже обработана'
user = await db.get(User, application.user_id)
if user:
user.partner_status = PartnerStatus.REJECTED.value
application.status = PartnerStatus.REJECTED.value
application.admin_comment = comment
application.processed_by = admin_id
application.processed_at = datetime.now(UTC)
await db.commit()
logger.info(
'❌ Партнёрская заявка отклонена',
application_id=application_id,
user_id=application.user_id,
admin_id=admin_id,
)
return True, ''
async def revoke_partner(
self,
db: AsyncSession,
user_id: int,
admin_id: int,
) -> tuple[bool, str]:
"""Отзывает партнёрский статус."""
user = await db.get(User, user_id)
if not user:
return False, 'Пользователь не найден'
if user.partner_status != PartnerStatus.APPROVED.value:
return False, 'Пользователь не является партнёром'
user.partner_status = PartnerStatus.NONE.value
user.referral_commission_percent = None
await db.commit()
logger.info(
'🚫 Партнёрский статус отозван',
user_id=user_id,
admin_id=admin_id,
)
return True, ''
async def get_pending_applications(self, db: AsyncSession) -> list[PartnerApplication]:
"""Получает все заявки на рассмотрении."""
result = await db.execute(
select(PartnerApplication)
.where(PartnerApplication.status == PartnerStatus.PENDING.value)
.order_by(PartnerApplication.created_at.asc())
)
return list(result.scalars().all())
async def get_all_applications(
self,
db: AsyncSession,
status: str | None = None,
limit: int = 50,
offset: int = 0,
) -> tuple[list[PartnerApplication], int]:
"""Получает заявки с фильтрацией. Возвращает (items, total)."""
query = select(PartnerApplication)
count_query = select(func.count()).select_from(PartnerApplication)
if status:
query = query.where(PartnerApplication.status == status)
count_query = count_query.where(PartnerApplication.status == status)
total_result = await db.execute(count_query)
total = total_result.scalar() or 0
query = query.order_by(desc(PartnerApplication.created_at)).offset(offset).limit(limit)
result = await db.execute(query)
return list(result.scalars().all()), total
async def get_latest_application(self, db: AsyncSession, user_id: int) -> PartnerApplication | None:
"""Получает последнюю заявку пользователя."""
result = await db.execute(
select(PartnerApplication)
.where(PartnerApplication.user_id == user_id)
.order_by(desc(PartnerApplication.created_at))
.limit(1)
)
return result.scalar_one_or_none()
@staticmethod
def _generate_referral_code() -> str:
"""Генерирует уникальный реферальный код."""
chars = string.ascii_lowercase + string.digits
return ''.join(secrets.choice(chars) for _ in range(8))
# Синглтон сервиса
partner_application_service = PartnerApplicationService()