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