from __future__ import annotations import html import logging from dataclasses import dataclass from datetime import datetime, timedelta, timezone from math import ceil from uuid import uuid4 from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from app.config import PaymentPlanSettings, Settings from app.db.base import utcnow from app.db.models import PaymentOrder, ReferralBonus, ReferralInvite, TelegramUser from app.schemas.remnawave import RemnawaveUser from app.services.bot_config_service import BotConfigSnapshot, StaticBotConfigService from app.services.remnawave_client import RemnawaveApiClient, RemnawaveApiError from app.services.sync_service import SyncService PENDING_PAYMENT_STATUS = "PENDING" REVIEW_PAYMENT_STATUS = "REVIEW" PROCESSING_PAYMENT_STATUS = "PROCESSING" REJECTED_PAYMENT_STATUS = "REJECTED" FULFILLED_PAYMENT_STATUS = "FULFILLED" REFERRAL_BONUS_PENDING_STATUS = "PENDING" REFERRAL_BONUS_PROCESSING_STATUS = "PROCESSING" REFERRAL_BONUS_APPLIED_STATUS = "APPLIED" logger = logging.getLogger(__name__) @dataclass(slots=True) class PaymentPlan: code: str title: str description: str duration_days: int amount_rub: int discount_percent: int original_amount_rub: int applied_referral_code: str = "" traffic_limit_bytes: int = 0 traffic_limit_strategy: str = "NO_RESET" internal_squad_uuids: list[str] | None = None external_squad_uuid: str | None = None @property def is_ready(self) -> bool: return bool(self.internal_squad_uuids) and self.duration_days > 0 and self.amount_rub > 0 @dataclass(slots=True) class CreatedPaymentOrder: order_uuid: str provision_username: str plan: PaymentPlan transfer_text: str extends_existing_access: bool = False @dataclass(slots=True) class StoredPaymentOrder: order_uuid: str telegram_id: int plan_title: str plan_duration_days: int amount_rub: int provision_username: str status: str @dataclass(slots=True) class IssuedAccess: telegram_id: int username: str subscription_url: str expire_at: datetime short_uuid: str remnawave_user_uuid: str traffic_limit_bytes: int referral_bonus: ReferralBonusGrant | None = None is_renewal: bool = False @dataclass(slots=True) class ReferralBonusGrant: inviter_telegram_id: int bonus_days: int status: str total_applied_days: int = 0 expire_at: datetime | None = None applied_to_user_uuid: str | None = None @dataclass(slots=True) class AppliedReferralBonus: telegram_id: int total_bonus_days: int expire_at: datetime applied_to_user_uuid: str @dataclass(slots=True) class PaymentAdminStats: pending_orders: int = 0 review_orders: int = 0 pending_referral_bonuses: int = 0 class PaymentService: def __init__( self, *, settings: Settings, session_factory: async_sessionmaker[AsyncSession], remnawave_client: RemnawaveApiClient, sync_service: SyncService, config_service: StaticBotConfigService | None = None, ) -> None: self._settings = settings self._session_factory = session_factory self._remnawave_client = remnawave_client self._sync_service = sync_service self._config_service = config_service or StaticBotConfigService(settings) async def _get_config(self) -> BotConfigSnapshot: return await self._config_service.get_snapshot() async def get_available_plans(self, *, telegram_id: int) -> list[PaymentPlan]: config = await self._get_config() applied_referral_code = await self._get_applied_referral_code( telegram_id, config=config, ) return [ self._build_plan( plan, applied_referral_code=applied_referral_code, config=config, ) for plan in config.payment_plans ] async def create_order( self, *, telegram_id: int, username: str | None, first_name: str | None, last_name: str | None, language_code: str | None, plan_code: str, ) -> CreatedPaymentOrder: config = await self._get_config() applied_referral_code = await self._get_applied_referral_code( telegram_id, config=config, ) plan = self._get_plan_by_code( plan_code, applied_referral_code=applied_referral_code, config=config, ) if not plan.is_ready: raise ValueError( "Покупка не настроена. Проверьте PAYMENT_PLANS и PAYMENT_INTERNAL_SQUAD_UUIDS." ) existing_accesses = await self._sync_service.get_cached_users_for_telegram(telegram_id) order_uuid = str(uuid4()) invoice_payload = f"manual:{plan.code}:{order_uuid}" provision_username = self._build_provision_username( config=config, order_uuid=order_uuid, telegram_id=telegram_id, username=username, first_name=first_name, ) async with self._session_factory() as session: user = await self._upsert_telegram_user( session, telegram_id=telegram_id, username=username, first_name=first_name, last_name=last_name, language_code=language_code, ) session.add( PaymentOrder( order_uuid=order_uuid, telegram_user_id=user.id, telegram_id=telegram_id, plan_code=plan.code, plan_title=plan.title, plan_duration_days=plan.duration_days, traffic_limit_bytes=plan.traffic_limit_bytes, traffic_limit_strategy=plan.traffic_limit_strategy, amount_stars=plan.amount_rub, currency="RUB", status=PENDING_PAYMENT_STATUS, invoice_payload=invoice_payload, provision_username=provision_username, error_message=applied_referral_code or None, ) ) await session.commit() return CreatedPaymentOrder( order_uuid=order_uuid, provision_username=provision_username, plan=plan, transfer_text=config.payment_transfer_text.strip(), extends_existing_access=bool(existing_accesses), ) async def get_order_for_user(self, *, order_uuid: str, telegram_id: int) -> StoredPaymentOrder: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder).where( PaymentOrder.order_uuid == order_uuid, PaymentOrder.telegram_id == telegram_id, ) ) if order is None: raise ValueError("Заказ не найден.") return StoredPaymentOrder( order_uuid=order.order_uuid, telegram_id=order.telegram_id, plan_title=order.plan_title, plan_duration_days=order.plan_duration_days, amount_rub=order.amount_stars, provision_username=order.provision_username, status=order.status, ) async def mark_order_under_review(self, *, order_uuid: str, telegram_id: int) -> StoredPaymentOrder: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder) .where( PaymentOrder.order_uuid == order_uuid, PaymentOrder.telegram_id == telegram_id, ) .with_for_update() ) if order is None: raise ValueError("Заказ не найден.") if order.status == REVIEW_PAYMENT_STATUS: raise ValueError("Чек уже отправлен на проверку.") if order.status == PROCESSING_PAYMENT_STATUS: raise ValueError("Заказ уже обрабатывается модератором.") if order.status == FULFILLED_PAYMENT_STATUS: raise ValueError("Этот заказ уже подтверждён.") if order.status == REJECTED_PAYMENT_STATUS: raise ValueError("Этот заказ уже отклонён. Создайте новый платёж.") if order.status != PENDING_PAYMENT_STATUS: raise ValueError("Некорректный статус заказа для отправки чека.") order.status = REVIEW_PAYMENT_STATUS order.updated_at = utcnow() await session.commit() return StoredPaymentOrder( order_uuid=order.order_uuid, telegram_id=order.telegram_id, plan_title=order.plan_title, plan_duration_days=order.plan_duration_days, amount_rub=order.amount_stars, provision_username=order.provision_username, status=order.status, ) async def revert_order_to_pending(self, *, order_uuid: str, telegram_id: int) -> None: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder) .where( PaymentOrder.order_uuid == order_uuid, PaymentOrder.telegram_id == telegram_id, ) .with_for_update() ) if order is None or order.status != REVIEW_PAYMENT_STATUS: return order.status = PENDING_PAYMENT_STATUS order.updated_at = utcnow() await session.commit() async def approve_order(self, *, order_uuid: str) -> IssuedAccess: order = await self._claim_order_for_approval(order_uuid) config = await self._get_config() is_renewal = False try: remote_user: RemnawaveUser | None = None if order.remnawave_user_uuid: try: remote_user = await self._remnawave_client.get_user_by_uuid( order.remnawave_user_uuid ) except RemnawaveApiError as exc: if exc.status_code != 404: raise remote_user = None if remote_user is None: try: remote_user = await self._remnawave_client.get_user_by_username( order.provision_username ) except RemnawaveApiError as exc: if exc.status_code != 404: raise if remote_user is None: remote_users = await self._remnawave_client.get_users_by_telegram_id( order.telegram_id ) remote_user = self._select_referral_bonus_target(remote_users) if remote_user is None: remote_user = await self._create_remnawave_user(order, config=config) else: is_renewal = True remote_user = await self._extend_remnawave_user( order, remote_user, config=config, ) except RemnawaveApiError as exc: await self._restore_review_after_failed_approval(order.order_uuid, exc.message) raise await self._save_fulfilled_order(order.order_uuid, remote_user) own_bonus = None try: own_bonus = await self.apply_pending_referral_bonuses( telegram_id=order.telegram_id, refresh_cache=False, ) except RemnawaveApiError as exc: logger.warning( "Could not apply pending referral bonus for telegram_id=%s: %s", order.telegram_id, exc.message, ) try: await self._sync_service.refresh_cached_users_for_telegram( telegram_id=order.telegram_id, ) except RemnawaveApiError: # Access is already issued at this point; cache can be refreshed later. pass expire_at = remote_user.expire_at if own_bonus is not None and own_bonus.applied_to_user_uuid == str(remote_user.uuid): expire_at = own_bonus.expire_at referral_bonus = await self._grant_referral_bonus(order, config=config) return IssuedAccess( telegram_id=order.telegram_id, username=remote_user.username, subscription_url=remote_user.subscription_url, expire_at=expire_at, short_uuid=remote_user.short_uuid, remnawave_user_uuid=str(remote_user.uuid), traffic_limit_bytes=remote_user.traffic_limit_bytes, referral_bonus=referral_bonus, is_renewal=is_renewal, ) async def reject_order(self, *, order_uuid: str, reason: str | None = None) -> StoredPaymentOrder: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder) .where(PaymentOrder.order_uuid == order_uuid) .with_for_update() ) if order is None: raise ValueError("Заказ не найден.") if order.status == FULFILLED_PAYMENT_STATUS: raise ValueError("Подтверждённый заказ нельзя отклонить.") if order.status == PROCESSING_PAYMENT_STATUS: raise ValueError("Заказ уже обрабатывается другим модератором.") if order.status == PENDING_PAYMENT_STATUS: raise ValueError("Чек ещё не отправлен на проверку.") if order.status == REJECTED_PAYMENT_STATUS: raise ValueError("Заказ уже отклонён.") order.status = REJECTED_PAYMENT_STATUS order.error_message = reason or "Платёж отклонён" order.updated_at = utcnow() await session.commit() return StoredPaymentOrder( order_uuid=order.order_uuid, telegram_id=order.telegram_id, plan_title=order.plan_title, plan_duration_days=order.plan_duration_days, amount_rub=order.amount_stars, provision_username=order.provision_username, status=order.status, ) async def get_referral_code_for_order(self, *, order_uuid: str) -> str: order = await self._get_order(order_uuid) return order.error_message or "" def build_transfer_text(self, *, order: CreatedPaymentOrder) -> str: lines = [ "Оплата тарифа", "", f"{html.escape(order.plan.title)}", html.escape(order.plan.description), "", f"Сумма к переводу: {order.plan.amount_rub} ₽", ] if order.plan.applied_referral_code: lines.append( f"Реферальный код: {order.plan.applied_referral_code} " f"(-{order.plan.discount_percent}%)" ) if order.extends_existing_access: lines.extend( [ "", ( "
" "У вас уже есть доступ. После подтверждения оплаты бот автоматически " "продлит текущую подписку на выбранный срок." "
" ), ] ) lines.extend( [ "", "Реквизиты", ( "
" f"{html.escape(order.transfer_text or 'Укажите реквизиты в PAYMENT_TRANSFER_TEXT.')}" "
" ), "", "После перевода нажмите кнопку ниже и отправьте чек следующим сообщением.", ] ) return "\n".join(lines) def _get_plan_by_code( self, plan_code: str, *, applied_referral_code: str, config: BotConfigSnapshot, ) -> PaymentPlan: for plan in config.payment_plans: if plan.code == plan_code: return self._build_plan( plan, applied_referral_code=applied_referral_code, config=config, ) raise ValueError("Тариф не найден.") def _build_plan( self, plan_settings: PaymentPlanSettings, *, applied_referral_code: str, config: BotConfigSnapshot, ) -> PaymentPlan: discount_percent = max(config.referral_discount_percent, 0) if applied_referral_code else 0 discounted_amount = self._apply_discount( amount_rub=plan_settings.amount_rub, discount_percent=discount_percent, ) traffic_limit_bytes = max(config.payment_plan_traffic_limit_gb, 0) * 1024**3 return PaymentPlan( code=plan_settings.code, title=f"{config.bot_brand_name} на {plan_settings.days} дней", description=f"Доступ к VPN на {plan_settings.days} дней", duration_days=plan_settings.days, amount_rub=discounted_amount, discount_percent=discount_percent, original_amount_rub=plan_settings.amount_rub, applied_referral_code=applied_referral_code, traffic_limit_bytes=traffic_limit_bytes, traffic_limit_strategy=config.payment_plan_traffic_reset_period.strip() or "NO_RESET", internal_squad_uuids=config.payment_internal_squad_uuids, external_squad_uuid=config.payment_external_squad_uuid_normalized, ) @staticmethod def _apply_discount(*, amount_rub: int, discount_percent: int) -> int: if discount_percent <= 0: return amount_rub return max(1, ceil(amount_rub * (100 - discount_percent) / 100)) async def _get_applied_referral_code( self, telegram_id: int, *, config: BotConfigSnapshot | None = None, ) -> str: resolved_config = config or await self._get_config() if not resolved_config.referral_enabled: return "" summary = await self._sync_service.get_referral_summary(telegram_id) return summary.applied_referral_code.strip().upper() async def apply_pending_referral_bonuses( self, *, telegram_id: int, refresh_cache: bool = True, ) -> AppliedReferralBonus | None: config = await self._get_config() if not config.referral_enabled: return None claimed_bonuses = await self._claim_pending_referral_bonuses(telegram_id) if not claimed_bonuses: return None total_bonus_days = sum(max(bonus.bonus_days, 0) for bonus in claimed_bonuses) if total_bonus_days <= 0: await self._restore_claimed_referral_bonuses( [bonus.id for bonus in claimed_bonuses], status=REFERRAL_BONUS_PENDING_STATUS, ) return None remote_users = await self._remnawave_client.get_users_by_telegram_id(telegram_id) target_user = self._select_referral_bonus_target(remote_users) if target_user is None: await self._restore_claimed_referral_bonuses( [bonus.id for bonus in claimed_bonuses], status=REFERRAL_BONUS_PENDING_STATUS, ) return None current_expire_at = target_user.expire_at if current_expire_at.tzinfo is None: current_expire_at = current_expire_at.replace(tzinfo=timezone.utc) next_expire_at = max(current_expire_at, datetime.now(timezone.utc)) + timedelta( days=total_bonus_days ) try: updated_user = await self._remnawave_client.update_user( { "uuid": str(target_user.uuid), "expireAt": next_expire_at.isoformat().replace("+00:00", "Z"), "description": ( f"Referral bonus applied by Telegram bot " f"({total_bonus_days} days)" ), } ) except RemnawaveApiError: await self._restore_claimed_referral_bonuses( [bonus.id for bonus in claimed_bonuses], status=REFERRAL_BONUS_PENDING_STATUS, ) raise await self._mark_referral_bonuses_applied( [bonus.id for bonus in claimed_bonuses], applied_to_user_uuid=str(updated_user.uuid), ) if refresh_cache: try: await self._sync_service.refresh_cached_users_for_telegram( telegram_id=telegram_id, ) except RemnawaveApiError: pass return AppliedReferralBonus( telegram_id=telegram_id, total_bonus_days=total_bonus_days, expire_at=updated_user.expire_at, applied_to_user_uuid=str(updated_user.uuid), ) async def get_admin_stats(self) -> PaymentAdminStats: async with self._session_factory() as session: pending_orders = await session.scalar( select(func.count(PaymentOrder.id)).where( PaymentOrder.status == PENDING_PAYMENT_STATUS ) ) review_orders = await session.scalar( select(func.count(PaymentOrder.id)).where( PaymentOrder.status == REVIEW_PAYMENT_STATUS ) ) pending_referral_bonuses = await session.scalar( select(func.count(ReferralBonus.id)).where( ReferralBonus.status == REFERRAL_BONUS_PENDING_STATUS ) ) return PaymentAdminStats( pending_orders=int(pending_orders or 0), review_orders=int(review_orders or 0), pending_referral_bonuses=int(pending_referral_bonuses or 0), ) async def _get_order(self, order_uuid: str) -> PaymentOrder: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder).where(PaymentOrder.order_uuid == order_uuid) ) if order is None: raise ValueError("Заказ не найден.") return order async def _claim_order_for_approval(self, order_uuid: str) -> PaymentOrder: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder) .where(PaymentOrder.order_uuid == order_uuid) .with_for_update() ) if order is None: raise ValueError("Заказ не найден.") if order.status == PENDING_PAYMENT_STATUS: raise ValueError("Чек ещё не отправлен на проверку.") if order.status == PROCESSING_PAYMENT_STATUS: raise ValueError("Заказ уже обрабатывается другим модератором.") if order.status == REJECTED_PAYMENT_STATUS: raise ValueError("Заказ уже отклонён.") if order.status == FULFILLED_PAYMENT_STATUS: raise ValueError("Этот заказ уже подтверждён.") if order.status != REVIEW_PAYMENT_STATUS: raise ValueError("Некорректный статус заказа для подтверждения.") order.status = PROCESSING_PAYMENT_STATUS order.updated_at = utcnow() await session.commit() return order async def _create_remnawave_user( self, order: PaymentOrder, *, config: BotConfigSnapshot, ) -> RemnawaveUser: expire_at = datetime.now(timezone.utc) + timedelta(days=order.plan_duration_days) body: dict[str, object] = { "username": order.provision_username, "expireAt": expire_at.isoformat().replace("+00:00", "Z"), "trafficLimitBytes": order.traffic_limit_bytes, "trafficLimitStrategy": order.traffic_limit_strategy, "telegramId": order.telegram_id, "description": f"Created by Telegram bot manual payment {order.order_uuid}", "activeInternalSquads": config.payment_internal_squad_uuids, } if config.payment_user_tag_normalized: body["tag"] = config.payment_user_tag_normalized if config.payment_external_squad_uuid_normalized: body["externalSquadUuid"] = config.payment_external_squad_uuid_normalized return await self._remnawave_client.create_user(body) async def _extend_remnawave_user( self, order: PaymentOrder, remote_user: RemnawaveUser, *, config: BotConfigSnapshot, ) -> RemnawaveUser: current_expire_at = remote_user.expire_at if current_expire_at.tzinfo is None: current_expire_at = current_expire_at.replace(tzinfo=timezone.utc) base_expire_at = max(current_expire_at, datetime.now(timezone.utc)) next_expire_at = base_expire_at + timedelta(days=order.plan_duration_days) traffic_limit_bytes = order.traffic_limit_bytes if traffic_limit_bytes <= 0: traffic_limit_bytes = remote_user.traffic_limit_bytes body: dict[str, object] = { "uuid": str(remote_user.uuid), "username": remote_user.username, "expireAt": next_expire_at.isoformat().replace("+00:00", "Z"), "trafficLimitBytes": traffic_limit_bytes, "trafficLimitStrategy": order.traffic_limit_strategy, "telegramId": order.telegram_id, "description": f"Updated by Telegram bot manual payment {order.order_uuid}", "activeInternalSquads": config.payment_internal_squad_uuids, } if config.payment_user_tag_normalized: body["tag"] = config.payment_user_tag_normalized if config.payment_external_squad_uuid_normalized: body["externalSquadUuid"] = config.payment_external_squad_uuid_normalized return await self._remnawave_client.update_user(body) async def _save_fulfilled_order(self, order_uuid: str, remote_user: RemnawaveUser) -> None: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder).where(PaymentOrder.order_uuid == order_uuid) ) if order is None: return order.status = FULFILLED_PAYMENT_STATUS order.remnawave_user_uuid = str(remote_user.uuid) order.subscription_url = remote_user.subscription_url order.error_message = None order.paid_at = utcnow() order.fulfilled_at = utcnow() order.updated_at = utcnow() await session.commit() async def _save_order_error(self, order_uuid: str, error_message: str) -> None: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder).where(PaymentOrder.order_uuid == order_uuid) ) if order is None: return order.error_message = error_message order.updated_at = utcnow() await session.commit() async def _restore_review_after_failed_approval( self, order_uuid: str, error_message: str, ) -> None: async with self._session_factory() as session: order = await session.scalar( select(PaymentOrder) .where(PaymentOrder.order_uuid == order_uuid) .with_for_update() ) if order is None: return if order.status == PROCESSING_PAYMENT_STATUS: order.status = REVIEW_PAYMENT_STATUS order.error_message = error_message order.updated_at = utcnow() await session.commit() async def _grant_referral_bonus( self, order: PaymentOrder, *, config: BotConfigSnapshot, ) -> ReferralBonusGrant | None: if not config.referral_enabled: return None bonus_days = max(config.referral_bonus_days, 0) if bonus_days <= 0: return None inviter_telegram_id = await self._get_inviter_telegram_id(order.telegram_id) if inviter_telegram_id is None or inviter_telegram_id == order.telegram_id: return None await self._create_referral_bonus( inviter_telegram_id=inviter_telegram_id, invited_telegram_id=order.telegram_id, source_order_uuid=order.order_uuid, bonus_days=bonus_days, ) try: applied_bonus = await self.apply_pending_referral_bonuses( telegram_id=inviter_telegram_id, ) except RemnawaveApiError as exc: logger.warning( "Could not apply referral bonus for inviter=%s after order=%s: %s", inviter_telegram_id, order.order_uuid, exc.message, ) applied_bonus = None if applied_bonus is None: return ReferralBonusGrant( inviter_telegram_id=inviter_telegram_id, bonus_days=bonus_days, status=REFERRAL_BONUS_PENDING_STATUS, ) return ReferralBonusGrant( inviter_telegram_id=inviter_telegram_id, bonus_days=bonus_days, status=REFERRAL_BONUS_APPLIED_STATUS, total_applied_days=applied_bonus.total_bonus_days, expire_at=applied_bonus.expire_at, applied_to_user_uuid=applied_bonus.applied_to_user_uuid, ) async def _get_inviter_telegram_id(self, invited_telegram_id: int) -> int | None: async with self._session_factory() as session: return await session.scalar( select(ReferralInvite.inviter_telegram_id).where( ReferralInvite.invited_telegram_id == invited_telegram_id ) ) async def _create_referral_bonus( self, *, inviter_telegram_id: int, invited_telegram_id: int, source_order_uuid: str, bonus_days: int, ) -> None: async with self._session_factory() as session: existing = await session.scalar( select(ReferralBonus).where( ReferralBonus.source_order_uuid == source_order_uuid ) ) if existing is not None: return session.add( ReferralBonus( inviter_telegram_id=inviter_telegram_id, invited_telegram_id=invited_telegram_id, source_order_uuid=source_order_uuid, bonus_days=bonus_days, status=REFERRAL_BONUS_PENDING_STATUS, ) ) await session.commit() async def _claim_pending_referral_bonuses(self, telegram_id: int) -> list[ReferralBonus]: async with self._session_factory() as session: bonuses = list( ( await session.scalars( select(ReferralBonus) .where( ReferralBonus.inviter_telegram_id == telegram_id, ReferralBonus.status == REFERRAL_BONUS_PENDING_STATUS, ) .order_by(ReferralBonus.created_at.asc(), ReferralBonus.id.asc()) .with_for_update() ) ).all() ) if not bonuses: return [] for bonus in bonuses: bonus.status = REFERRAL_BONUS_PROCESSING_STATUS bonus.updated_at = utcnow() await session.commit() return bonuses async def _restore_claimed_referral_bonuses( self, bonus_ids: list[int], *, status: str, ) -> None: if not bonus_ids: return async with self._session_factory() as session: bonuses = list( ( await session.scalars( select(ReferralBonus) .where(ReferralBonus.id.in_(bonus_ids)) .with_for_update() ) ).all() ) for bonus in bonuses: bonus.status = status bonus.updated_at = utcnow() await session.commit() async def _mark_referral_bonuses_applied( self, bonus_ids: list[int], *, applied_to_user_uuid: str, ) -> None: if not bonus_ids: return async with self._session_factory() as session: bonuses = list( ( await session.scalars( select(ReferralBonus) .where(ReferralBonus.id.in_(bonus_ids)) .with_for_update() ) ).all() ) for bonus in bonuses: bonus.status = REFERRAL_BONUS_APPLIED_STATUS bonus.applied_to_user_uuid = applied_to_user_uuid bonus.applied_at = utcnow() bonus.updated_at = utcnow() await session.commit() @staticmethod def _select_referral_bonus_target( remote_users: list[RemnawaveUser], ) -> RemnawaveUser | None: if not remote_users: return None def _sort_key(user: RemnawaveUser) -> tuple[int, datetime, str]: expire_at = user.expire_at if expire_at.tzinfo is None: expire_at = expire_at.replace(tzinfo=timezone.utc) return (0 if user.status.upper() == "ACTIVE" else 1, expire_at, user.username) return sorted(remote_users, key=_sort_key)[0] async def _upsert_telegram_user( self, session: AsyncSession, *, telegram_id: int, username: str | None, first_name: str | None, last_name: str | None, language_code: str | None, ) -> TelegramUser: record = await session.scalar( select(TelegramUser).where(TelegramUser.telegram_id == telegram_id) ) if record is None: record = TelegramUser(telegram_id=telegram_id) session.add(record) record.username = username record.first_name = first_name record.last_name = last_name record.language_code = language_code record.is_admin = self._settings.is_admin(telegram_id) record.last_seen_at = utcnow() record.updated_at = utcnow() await session.flush() return record def _build_provision_username( self, *, config: BotConfigSnapshot, order_uuid: str, telegram_id: int, username: str | None, first_name: str | None, ) -> str: base_name = username or first_name or "user" normalized_name = "".join( ch for ch in base_name if ch.isalnum() or ch in "_-" ).strip("-_") if not normalized_name: normalized_name = "user" prefix = config.payment_username_prefix_normalized or "Oreol" unique_suffix = order_uuid.replace("-", "")[:6] provision_username = f"{prefix}-{telegram_id}-{unique_suffix}-{normalized_name}" return provision_username[:36]