Files
remnawave-bedolaga-telegram…/app/services/promocode_service.py
libkit 1793775fe8 feat(services): реализовать логику активации DISCOUNT промокодов
Добавлена обработка нового типа промокода DISCOUNT:
- Проверка конфликта с активными скидками пользователя
- Запись скидки в профиль (promo_offer_discount_percent, promo_offer_discount_expires_at)
- Обработка срока действия скидки (0 часов = бессрочно до первой покупки)
- Логирование активации и ошибок
- Выброс ValueError при попытке активировать скидку при наличии активной
2026-01-17 11:18:46 +05:00

297 lines
14 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 logging
from datetime import datetime
from typing import Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.crud.promocode import (
get_promocode_by_code, use_promocode, check_user_promocode_usage,
create_promocode_use, get_promocode_use_by_user_and_code
)
from app.database.crud.user import add_user_balance, get_user_by_id
from app.database.crud.subscription import extend_subscription, get_subscription_by_user_id
from app.database.crud.user_promo_group import (
has_user_promo_group, add_user_to_promo_group
)
from app.database.crud.promo_group import get_promo_group_by_id
from app.database.models import PromoCodeType, SubscriptionStatus, User, PromoCode
from app.services.remnawave_service import RemnaWaveService
from app.services.subscription_service import SubscriptionService
logger = logging.getLogger(__name__)
class PromoCodeService:
def __init__(self):
self.remnawave_service = RemnaWaveService()
self.subscription_service = SubscriptionService()
async def activate_promocode(
self,
db: AsyncSession,
user_id: int,
code: str
) -> Dict[str, Any]:
try:
user = await get_user_by_id(db, user_id)
if not user:
return {"success": False, "error": "user_not_found"}
promocode = await get_promocode_by_code(db, code)
if not promocode:
return {"success": False, "error": "not_found"}
if not promocode.is_valid:
if promocode.current_uses >= promocode.max_uses:
return {"success": False, "error": "used"}
else:
return {"success": False, "error": "expired"}
existing_use = await check_user_promocode_usage(db, user_id, promocode.id)
if existing_use:
return {"success": False, "error": "already_used_by_user"}
# Проверка "только для первой покупки"
if getattr(promocode, 'first_purchase_only', False):
if getattr(user, 'has_had_paid_subscription', False):
return {"success": False, "error": "not_first_purchase"}
balance_before_kopeks = user.balance_kopeks
try:
result_description = await self._apply_promocode_effects(db, user, promocode)
except ValueError as e:
if str(e) == "active_discount_exists":
return {"success": False, "error": "active_discount_exists"}
raise
balance_after_kopeks = user.balance_kopeks
if promocode.type == PromoCodeType.SUBSCRIPTION_DAYS.value and promocode.subscription_days > 0:
from app.utils.user_utils import mark_user_as_had_paid_subscription
await mark_user_as_had_paid_subscription(db, user)
logger.info(f"🎯 Пользователь {user.telegram_id} получил платную подписку через промокод {code}")
# Assign promo group if promocode has one
if promocode.promo_group_id:
try:
# Check if user already has this promo group
has_group = await has_user_promo_group(db, user_id, promocode.promo_group_id)
if not has_group:
# Get promo group details
promo_group = await get_promo_group_by_id(db, promocode.promo_group_id)
if promo_group:
# Add promo group to user
await add_user_to_promo_group(
db,
user_id,
promocode.promo_group_id,
assigned_by="promocode"
)
logger.info(
f"🎯 Пользователю {user.telegram_id} назначена промогруппа '{promo_group.name}' "
f"(приоритет: {promo_group.priority}) через промокод {code}"
)
# Add to result description
result_description += f"\n🎁 Назначена промогруппа: {promo_group.name}"
else:
logger.warning(
f"⚠️ Промогруппа ID {promocode.promo_group_id} не найдена для промокода {code}"
)
else:
logger.info(
f" Пользователь {user.telegram_id} уже имеет промогруппу ID {promocode.promo_group_id}"
)
except Exception as pg_error:
logger.error(
f"❌ Ошибка назначения промогруппы для пользователя {user.telegram_id} "
f"при активации промокода {code}: {pg_error}"
)
# Don't fail the whole promocode activation if promo group assignment fails
await create_promocode_use(db, promocode.id, user_id)
promocode.current_uses += 1
await db.commit()
logger.info(f"✅ Пользователь {user.telegram_id} активировал промокод {code}")
promocode_data = {
"code": promocode.code,
"type": promocode.type,
"balance_bonus_kopeks": promocode.balance_bonus_kopeks,
"subscription_days": promocode.subscription_days,
"max_uses": promocode.max_uses,
"current_uses": promocode.current_uses,
"valid_until": promocode.valid_until,
"promo_group_id": promocode.promo_group_id,
}
return {
"success": True,
"description": result_description,
"promocode": promocode_data,
"balance_before_kopeks": balance_before_kopeks,
"balance_after_kopeks": balance_after_kopeks,
}
except Exception as e:
logger.error(f"Ошибка активации промокода {code} для пользователя {user_id}: {e}")
await db.rollback()
return {"success": False, "error": "server_error"}
async def _apply_promocode_effects(self, db: AsyncSession, user: User, promocode: PromoCode) -> str:
"""
Применяет эффекты промокода к пользователю.
Args:
db: Сессия базы данных
user: Пользователь
promocode: Промокод
Returns:
Описание примененных эффектов
Raises:
ValueError: Если у пользователя уже есть активная скидка (для DISCOUNT типа)
"""
effects = []
# Обработка DISCOUNT типа (одноразовая скидка)
if promocode.type == PromoCodeType.DISCOUNT.value:
from datetime import datetime, timedelta
# Проверка на наличие активной скидки
current_discount = getattr(user, 'promo_offer_discount_percent', 0) or 0
expires_at = getattr(user, 'promo_offer_discount_expires_at', None)
# Если есть активная скидка (процент > 0 и срок не истек)
if current_discount > 0:
if expires_at is None or expires_at > datetime.utcnow():
logger.warning(
f"⚠️ Пользователь {user.telegram_id} попытался активировать промокод {promocode.code}, "
f"но у него уже есть активная скидка {current_discount}% до {expires_at}"
)
raise ValueError("active_discount_exists")
# balance_bonus_kopeks хранит процент скидки (1-100)
discount_percent = promocode.balance_bonus_kopeks
# subscription_days хранит срок действия скидки в часах (0 = бессрочно до первой покупки)
discount_hours = promocode.subscription_days
# Устанавливаем процент скидки
user.promo_offer_discount_percent = discount_percent
user.promo_offer_discount_source = f"promocode:{promocode.code}"
# Устанавливаем срок действия скидки
if discount_hours > 0:
user.promo_offer_discount_expires_at = datetime.utcnow() + timedelta(hours=discount_hours)
effects.append(f"💸 Получена скидка {discount_percent}% (действует {discount_hours} ч.)")
else:
# 0 часов = бессрочно до первой покупки
user.promo_offer_discount_expires_at = None
effects.append(f"💸 Получена скидка {discount_percent}% до первой покупки")
await db.flush()
logger.info(
f"✅ Пользователю {user.telegram_id} назначена скидка {discount_percent}% "
f"(срок: {discount_hours} ч.) по промокоду {promocode.code}"
)
if promocode.type == PromoCodeType.BALANCE.value and promocode.balance_bonus_kopeks > 0:
await add_user_balance(
db, user, promocode.balance_bonus_kopeks,
f"Бонус по промокоду {promocode.code}"
)
balance_bonus_rubles = promocode.balance_bonus_kopeks / 100
effects.append(f"💰 Баланс пополнен на {balance_bonus_rubles}")
if promocode.type == PromoCodeType.SUBSCRIPTION_DAYS.value and promocode.subscription_days > 0:
from app.config import settings
subscription = await get_subscription_by_user_id(db, user.id)
if subscription:
await extend_subscription(db, subscription, promocode.subscription_days)
await self.subscription_service.update_remnawave_user(db, subscription)
effects.append(f"⏰ Подписка продлена на {promocode.subscription_days} дней")
logger.info(f"✅ Подписка пользователя {user.telegram_id} продлена на {promocode.subscription_days} дней в RemnaWave с текущими сквадами")
else:
from app.database.crud.subscription import create_paid_subscription
trial_squads = []
try:
from app.database.crud.server_squad import get_random_trial_squad_uuid
trial_uuid = await get_random_trial_squad_uuid(db)
if trial_uuid:
trial_squads = [trial_uuid]
except Exception as error:
logger.error(
"Не удалось подобрать сквад для подписки по промокоду %s: %s",
promocode.code,
error,
)
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_disabled_mode_device_limit()
device_limit = settings.DEFAULT_DEVICE_LIMIT
if forced_devices is not None:
device_limit = forced_devices
new_subscription = await create_paid_subscription(
db=db,
user_id=user.id,
duration_days=promocode.subscription_days,
traffic_limit_gb=0,
device_limit=device_limit,
connected_squads=trial_squads,
update_server_counters=True,
)
await self.subscription_service.create_remnawave_user(db, new_subscription)
effects.append(f"🎉 Получена подписка на {promocode.subscription_days} дней")
logger.info(f"✅ Создана новая подписка для пользователя {user.telegram_id} на {promocode.subscription_days} дней с триал сквадом {trial_squads}")
if promocode.type == PromoCodeType.TRIAL_SUBSCRIPTION.value:
from app.database.crud.subscription import create_trial_subscription
from app.config import settings
subscription = await get_subscription_by_user_id(db, user.id)
if not subscription:
trial_days = promocode.subscription_days if promocode.subscription_days > 0 else settings.TRIAL_DURATION_DAYS
forced_devices = None
if not settings.is_devices_selection_enabled():
forced_devices = settings.get_disabled_mode_device_limit()
trial_subscription = await create_trial_subscription(
db,
user.id,
duration_days=trial_days,
device_limit=forced_devices,
)
await self.subscription_service.create_remnawave_user(db, trial_subscription)
effects.append(f"🎁 Активирована тестовая подписка на {trial_days} дней")
logger.info(f"✅ Создана триал подписка для пользователя {user.telegram_id} на {trial_days} дней")
else:
effects.append(" У вас уже есть активная подписка")
return "\n".join(effects) if effects else "✅ Промокод активирован"