Files
Solo_bot/services/coupons.py
T
Capybara-z 0893b8cc7a fix cupon
2026-04-29 16:15:12 +00:00

143 lines
5.0 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
from database.coupons import (
apply_percent_coupon,
check_coupon_usage,
create_coupon_usage,
get_coupon_by_code_ci,
update_coupon_usage_count,
)
from database.keys import count_active_keys_for_user
from database.models import Coupon
from database.payments import add_payment, count_successful_payments
from database.users import get_balance, update_balance
from .errors import LimitExceededError, NotFoundError, ValidationError
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
async def resolve_percent_coupon(
session: AsyncSession,
billing_user_id: int,
base_price_rub: int,
coupon_code: str | None,
) -> tuple[int, int, int | None, str | None]:
"""Применяет процентный купон к цене.
Returns: (final_price, discount, coupon_id, coupon_code)
Raises: NotFoundError, LimitExceededError, ValidationError
"""
normalized = (coupon_code or "").strip()
if not normalized:
return int(base_price_rub), 0, None, None
coupon = await get_coupon_by_code_ci(session, normalized)
if coupon is None:
raise NotFoundError("Купон не найден")
_check_coupon_limits(coupon)
if await check_coupon_usage(session, int(coupon.id), int(billing_user_id)):
raise LimitExceededError("Вы уже использовали этот купон")
percent = getattr(coupon, "percent", None)
if percent is None:
raise ValidationError("Поддерживаются только процентные купоны")
if bool(getattr(coupon, "new_users_only", False)):
await _check_new_user(session, billing_user_id)
discounted_price, discount_rub = apply_percent_coupon(int(base_price_rub), coupon)
if int(discount_rub) <= 0:
raise ValidationError("Купон не применим к текущей сумме")
return int(discounted_price), int(discount_rub), int(coupon.id), str(coupon.code or normalized)
class CouponApplyResult:
__slots__ = ("coupon_code", "amount", "balance")
def __init__(self, coupon_code: str, amount: int, balance: float) -> None:
self.coupon_code = coupon_code
self.amount = amount
self.balance = balance
async def apply_fixed_coupon(
session: AsyncSession,
user_id: int,
tg_id: int | None,
code: str,
) -> CouponApplyResult:
"""Активирует купон с фиксированной суммой — зачисляет на баланс.
Raises: NotFoundError, LimitExceededError, ValidationError
"""
normalized = code.strip()
if not normalized:
raise ValidationError("Введите код купона")
coupon = await get_coupon_by_code_ci(session, normalized)
if coupon is None:
raise NotFoundError("Купон не найден")
_check_coupon_limits(coupon)
if await check_coupon_usage(session, int(coupon.id), int(user_id)):
raise LimitExceededError("Вы уже использовали этот купон")
percent = getattr(coupon, "percent", None)
if percent is not None:
raise ValidationError("Этот купон применяется при оплате тарифа")
days = int(getattr(coupon, "days", 0) or 0)
if days > 0:
raise ValidationError("Купон на продление применяйте через Telegram-бота")
amount = int(getattr(coupon, "amount", 0) or 0)
if amount <= 0:
raise ValidationError("Купон недействителен")
if bool(getattr(coupon, "new_users_only", False)):
await _check_new_user(session, user_id)
await update_balance(session, int(user_id), float(amount))
await create_coupon_usage(session, int(coupon.id), int(user_id))
await update_coupon_usage_count(session, int(coupon.id))
await add_payment(
session=session,
legacy_user_ref=int(user_id),
amount=float(amount),
payment_system="coupon",
status="success",
currency="RUB",
)
balance = float(await get_balance(session, int(user_id)))
return CouponApplyResult(
coupon_code=str(coupon.code or normalized),
amount=amount,
balance=balance,
)
def _check_coupon_limits(coupon: Coupon) -> None:
usage_limit = int(getattr(coupon, "usage_limit", 0) or 0)
usage_count = int(getattr(coupon, "usage_count", 0) or 0)
is_used = bool(getattr(coupon, "is_used", False))
if usage_limit > 0 and (is_used or usage_count >= usage_limit):
raise LimitExceededError("Лимит активаций купона исчерпан")
async def _check_new_user(session: AsyncSession, user_id: int) -> None:
payments_count = await count_successful_payments(session, int(user_id))
keys_count = await count_active_keys_for_user(session, int(user_id))
if payments_count > 0 or keys_count > 0:
raise ValidationError("Этот купон доступен только для новых пользователей")