Files
remnawave-bedolaga-telegram…/app/services/partner_application_service.py
Fringg 7ea8fbd584 feat: add desired commission percent to partner application
Allow partners to specify their desired commission percentage (1-100%)
when applying. Field is optional and shown to admins during review.

Includes DB model, Alembic migration 0013, schema, route, and service changes.
2026-02-27 04:02:17 +03:00

238 lines
8.5 KiB
Python
Raw Permalink 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.

"""Сервис для обработки заявок на партнёрский статус."""
from datetime import UTC, datetime
import structlog
from sqlalchemy import desc, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import AdvertisingCampaign, PartnerApplication, PartnerStatus, User
from app.utils.user_utils import generate_unique_referral_code
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,
desired_commission_percent: 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,
desired_commission_percent=desired_commission_percent,
)
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).
"""
result = await db.execute(
select(PartnerApplication).where(PartnerApplication.id == application_id).with_for_update()
)
application = result.scalar_one_or_none()
if not application:
return False, 'Заявка не найдена'
if application.status != PartnerStatus.PENDING.value:
return False, 'Заявка уже обработана'
user_result = await db.execute(select(User).where(User.id == application.user_id).with_for_update())
user = user_result.scalar_one_or_none()
if not user:
return False, 'Пользователь не найден'
# Генерируем реферальный код, если его нет
if not user.referral_code:
user.referral_code = await generate_unique_referral_code(db, user.telegram_id or 0)
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]:
"""Отклоняет заявку на партнёрство."""
result = await db.execute(
select(PartnerApplication).where(PartnerApplication.id == application_id).with_for_update()
)
application = result.scalar_one_or_none()
if not application:
return False, 'Заявка не найдена'
if application.status != PartnerStatus.PENDING.value:
return False, 'Заявка уже обработана'
user_result = await db.execute(select(User).where(User.id == application.user_id).with_for_update())
user = user_result.scalar_one_or_none()
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.execute(
update(AdvertisingCampaign)
.where(AdvertisingCampaign.partner_user_id == user_id)
.values(partner_user_id=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()
# Синглтон сервиса
partner_application_service = PartnerApplicationService()