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