116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
from datetime import datetime
|
||
|
||
from sqlalchemy import func, insert, select, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from database.access.resolution import resolve_user_optional
|
||
from database.models import Gift, GiftUsage
|
||
from logger import logger
|
||
|
||
|
||
async def store_gift_link(
|
||
session: AsyncSession,
|
||
gift_id: str,
|
||
sender_legacy_ref: int,
|
||
selected_months: int,
|
||
expiry_time: datetime,
|
||
gift_link: str,
|
||
tariff_id: int | None = None,
|
||
is_unlimited: bool = False,
|
||
max_usages: int | None = None,
|
||
selected_device_limit: int | None = None,
|
||
selected_traffic_gb: int | None = None,
|
||
selected_price_rub: int | None = None,
|
||
) -> bool:
|
||
u = await resolve_user_optional(session, sender_legacy_ref)
|
||
if u is None:
|
||
raise ValueError(f"sender not found for gift: {sender_legacy_ref}")
|
||
stmt = insert(Gift).values(
|
||
gift_id=gift_id,
|
||
sender_user_id=u.id,
|
||
sender_tg_id=u.tg_id,
|
||
recipient_user_id=None,
|
||
selected_months=selected_months,
|
||
expiry_time=expiry_time,
|
||
gift_link=gift_link,
|
||
created_at=datetime.utcnow(),
|
||
is_used=False,
|
||
tariff_id=tariff_id,
|
||
is_unlimited=is_unlimited,
|
||
max_usages=max_usages,
|
||
selected_device_limit=selected_device_limit,
|
||
selected_traffic_gb=selected_traffic_gb,
|
||
selected_price_rub=selected_price_rub,
|
||
)
|
||
await session.execute(stmt)
|
||
logger.info(
|
||
f"🎁 Подарок {gift_id} сохранён "
|
||
f"(tariff_id={tariff_id}, max_usages={max_usages}, "
|
||
f"device={selected_device_limit}, traffic={selected_traffic_gb}, price={selected_price_rub})"
|
||
)
|
||
return True
|
||
|
||
|
||
async def get_gift_locked(session: AsyncSession, gift_id: str) -> Gift | None:
|
||
"""SELECT FOR UPDATE по gift_id — берёт row-lock для atomic redemption.
|
||
|
||
Используется в `services.gifts.redeem_gift` чтобы два параллельных запроса
|
||
на активацию одного и того же подарка не смогли обойти проверку `is_used`.
|
||
"""
|
||
result = await session.execute(select(Gift).where(Gift.gift_id == gift_id).with_for_update())
|
||
return result.scalar_one_or_none()
|
||
|
||
|
||
async def get_gift_usage(session: AsyncSession, gift_id: str, user_id: int) -> GiftUsage | None:
|
||
"""Возвращает запись об использовании подарка конкретным пользователем, если есть."""
|
||
result = await session.execute(
|
||
select(GiftUsage).where(
|
||
GiftUsage.gift_id == gift_id,
|
||
GiftUsage.user_id == user_id,
|
||
)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
|
||
async def count_gift_usages(session: AsyncSession, gift_id: str) -> int:
|
||
"""Сколько раз подарок был активирован (для `is_unlimited=False` с лимитом)."""
|
||
result = await session.execute(select(func.count()).select_from(GiftUsage).where(GiftUsage.gift_id == gift_id))
|
||
return int(result.scalar_one() or 0)
|
||
|
||
|
||
async def record_gift_usage(
|
||
session: AsyncSession,
|
||
gift_id: str,
|
||
user_id: int,
|
||
tg_id: int | None,
|
||
) -> None:
|
||
"""Вставляет запись о применении подарка. Композитный ключ (gift_id, user_id)."""
|
||
await session.execute(
|
||
insert(GiftUsage).values(
|
||
gift_id=gift_id,
|
||
user_id=user_id,
|
||
tg_id=tg_id,
|
||
)
|
||
)
|
||
|
||
|
||
async def mark_gift_fully_redeemed(
|
||
session: AsyncSession,
|
||
gift_id: str,
|
||
recipient_user_id: int,
|
||
recipient_tg_id: int | None,
|
||
) -> None:
|
||
"""Помечает подарок как полностью использованный (is_used=True) и фиксирует получателя.
|
||
|
||
Вызывается для non-unlimited подарков, когда набрали max_usages.
|
||
"""
|
||
await session.execute(
|
||
update(Gift)
|
||
.where(Gift.gift_id == gift_id)
|
||
.values(
|
||
is_used=True,
|
||
recipient_user_id=recipient_user_id,
|
||
recipient_tg_id=recipient_tg_id,
|
||
)
|
||
)
|