208 lines
7.2 KiB
Python
208 lines
7.2 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import case, delete, func, insert, or_, select, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database.access.resolution import resolve_user_optional
|
|
from database.models import Coupon, CouponUsage
|
|
from logger import logger
|
|
|
|
|
|
async def create_coupon(
|
|
session: AsyncSession,
|
|
code: str,
|
|
amount: int | None,
|
|
usage_limit: int,
|
|
days: int | None = None,
|
|
new_users_only: bool = False,
|
|
percent: int | None = None,
|
|
max_discount_amount: int | None = None,
|
|
min_order_amount: int | None = None,
|
|
) -> bool:
|
|
exists = await session.scalar(select(Coupon.id).where(Coupon.code == code))
|
|
if exists:
|
|
logger.warning(f"[Coupon] ⚠️ Купон с кодом {code} уже существует.")
|
|
return False
|
|
|
|
if percent is not None:
|
|
try:
|
|
percent_value = int(percent)
|
|
except (TypeError, ValueError):
|
|
logger.warning(f"[Coupon] ⚠️ Некорректный процент для купона {code}.")
|
|
return False
|
|
|
|
if percent_value <= 0 or percent_value > 100:
|
|
logger.warning(f"[Coupon] ⚠️ процент должен быть в диапазоне 1..100 для купона {code}.")
|
|
return False
|
|
|
|
if (amount or 0) > 0 or (days or 0) > 0:
|
|
logger.warning(f"[Coupon] ⚠️ Купон {code} не может одновременно иметь percent и amount/days.")
|
|
return False
|
|
|
|
await session.execute(
|
|
insert(Coupon).values(
|
|
code=code,
|
|
amount=int(amount) if amount is not None else 0,
|
|
usage_limit=usage_limit,
|
|
usage_count=0,
|
|
is_used=False,
|
|
days=days,
|
|
new_users_only=new_users_only,
|
|
percent=percent,
|
|
max_discount_amount=max_discount_amount,
|
|
min_order_amount=min_order_amount,
|
|
)
|
|
)
|
|
logger.info(f"[Coupon] ✅ Купон {code} успешно создан.")
|
|
return True
|
|
|
|
|
|
async def get_coupon_by_code(session: AsyncSession, code: str) -> Coupon | None:
|
|
stmt = select(Coupon).where(Coupon.code == code)
|
|
result = await session.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def get_coupon_by_code_ci(session: AsyncSession, code: str) -> Coupon | None:
|
|
normalized = str(code or "").strip()
|
|
if not normalized:
|
|
return None
|
|
stmt = select(Coupon).where(func.lower(Coupon.code) == normalized.lower())
|
|
result = await session.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def get_all_coupons(session: AsyncSession, page: int = 1, per_page: int = 10) -> dict:
|
|
offset = (page - 1) * per_page
|
|
|
|
stmt = select(Coupon).order_by(Coupon.id.desc()).offset(offset).limit(per_page)
|
|
result = await session.execute(stmt)
|
|
coupons = result.scalars().all()
|
|
|
|
count_stmt = select(func.count()).select_from(Coupon)
|
|
total = await session.scalar(count_stmt)
|
|
pages = -(-total // per_page)
|
|
|
|
return {
|
|
"coupons": [c.to_dict() for c in coupons],
|
|
"total": total,
|
|
"pages": pages,
|
|
"current_page": page,
|
|
}
|
|
|
|
|
|
async def delete_coupon(session: AsyncSession, code: str) -> bool:
|
|
result = await session.execute(select(Coupon).where(Coupon.code == code))
|
|
coupon = result.scalar_one_or_none()
|
|
|
|
if not coupon:
|
|
logger.info(f"❌ Купон {code} не найден")
|
|
return False
|
|
|
|
await session.execute(delete(CouponUsage).where(CouponUsage.coupon_id == coupon.id))
|
|
|
|
await session.delete(coupon)
|
|
logger.info(f"🗑 Купон {code} удалён вместе с его использованиями")
|
|
return True
|
|
|
|
|
|
async def _coupon_usage_billing_match(session: AsyncSession, legacy_user_ref: int):
|
|
u = await resolve_user_optional(session, legacy_user_ref)
|
|
if u is not None:
|
|
opts = [CouponUsage.user_id == u.id]
|
|
if u.tg_id is not None:
|
|
opts.append(CouponUsage.tg_id == u.tg_id)
|
|
return or_(*opts)
|
|
return or_(CouponUsage.user_id == legacy_user_ref, CouponUsage.tg_id == legacy_user_ref)
|
|
|
|
|
|
async def create_coupon_usage(session: AsyncSession, coupon_id: int, user_id: int):
|
|
u = await resolve_user_optional(session, user_id)
|
|
uid = u.id if u is not None else user_id
|
|
stmt = insert(CouponUsage).values(
|
|
coupon_id=coupon_id,
|
|
user_id=uid,
|
|
tg_id=u.tg_id if u is not None else None,
|
|
used_at=datetime.utcnow(),
|
|
)
|
|
await session.execute(stmt)
|
|
logger.info(f"✅ Купон {coupon_id} использован пользователем {user_id}")
|
|
|
|
|
|
async def check_coupon_usage(session: AsyncSession, coupon_id: int, legacy_user_ref: int) -> bool:
|
|
m = await _coupon_usage_billing_match(session, legacy_user_ref)
|
|
stmt = select(CouponUsage).where(CouponUsage.coupon_id == coupon_id).where(m)
|
|
result = await session.execute(stmt)
|
|
return result.scalar_one_or_none() is not None
|
|
|
|
|
|
async def has_any_coupon_usage(session: AsyncSession, legacy_user_ref: int) -> bool:
|
|
m = await _coupon_usage_billing_match(session, legacy_user_ref)
|
|
stmt = select(CouponUsage.coupon_id).where(m).limit(1)
|
|
result = await session.execute(stmt)
|
|
return result.first() is not None
|
|
|
|
|
|
async def update_coupon_usage_count(session: AsyncSession, coupon_id: int):
|
|
await session.execute(
|
|
update(Coupon)
|
|
.where(Coupon.id == coupon_id)
|
|
.values(
|
|
usage_count=Coupon.usage_count + 1,
|
|
is_used=case((Coupon.usage_count + 1 >= Coupon.usage_limit, True), else_=False),
|
|
)
|
|
)
|
|
logger.info(f"🔁 Обновлён счётчик купона {coupon_id}")
|
|
|
|
|
|
async def mark_coupon_used(session: AsyncSession, coupon_id: int, legacy_user_ref: int):
|
|
u = await resolve_user_optional(session, legacy_user_ref)
|
|
uid = u.id if u is not None else legacy_user_ref
|
|
match = [CouponUsage.user_id == int(uid)]
|
|
if u is not None and u.tg_id is not None:
|
|
match.append(CouponUsage.tg_id == int(u.tg_id))
|
|
existing = await session.execute(
|
|
select(CouponUsage).where(
|
|
CouponUsage.coupon_id == int(coupon_id),
|
|
or_(*match),
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none() is not None:
|
|
return
|
|
await session.execute(
|
|
insert(CouponUsage).values(
|
|
coupon_id=coupon_id,
|
|
user_id=uid,
|
|
tg_id=u.tg_id if u is not None else None,
|
|
used_at=datetime.utcnow(),
|
|
)
|
|
)
|
|
await session.execute(
|
|
update(Coupon)
|
|
.where(Coupon.id == coupon_id)
|
|
.values(
|
|
usage_count=Coupon.usage_count + 1,
|
|
is_used=case((Coupon.usage_count + 1 >= Coupon.usage_limit, True), else_=False),
|
|
)
|
|
)
|
|
|
|
|
|
def apply_percent_coupon(price_rub: int, coupon: Coupon) -> tuple[int, int]:
|
|
percent = coupon.percent
|
|
if percent is None:
|
|
return price_rub, 0
|
|
|
|
if coupon.min_order_amount is not None and price_rub < int(coupon.min_order_amount):
|
|
return price_rub, 0
|
|
|
|
discount = (price_rub * int(percent)) // 100
|
|
|
|
if coupon.max_discount_amount is not None:
|
|
discount = min(discount, int(coupon.max_discount_amount))
|
|
|
|
final_price = price_rub - discount
|
|
if final_price < 0:
|
|
final_price = 0
|
|
|
|
return final_price, discount
|