mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-02 08:11:32 +00:00
- 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
228 lines
7.8 KiB
Python
228 lines
7.8 KiB
Python
"""Сервис для обработки заявок на партнёрский статус."""
|
||
|
||
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()
|