from datetime import datetime from sqlalchemy import case, delete, func, insert, select, update from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession 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: try: 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, ) ) await session.commit() logger.info(f"[Coupon] ✅ Купон {code} успешно создан.") return True except SQLAlchemyError as e: await session.rollback() logger.error(f"[Coupon] ❌ Ошибка при создании купона {code}: {e}") return False 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_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) await session.commit() logger.info(f"🗑 Купон {code} удалён вместе с его использованиями") return True async def create_coupon_usage(session: AsyncSession, coupon_id: int, user_id: int): try: stmt = insert(CouponUsage).values(coupon_id=coupon_id, user_id=user_id, used_at=datetime.utcnow()) await session.execute(stmt) await session.commit() logger.info(f"✅ Купон {coupon_id} использован пользователем {user_id}") except SQLAlchemyError as e: logger.error(f"❌ Ошибка при сохранении использования купона: {e}") await session.rollback() async def check_coupon_usage(session: AsyncSession, coupon_id: int, user_id: int) -> bool: stmt = select(CouponUsage).where(CouponUsage.coupon_id == coupon_id, CouponUsage.user_id == user_id) result = await session.execute(stmt) return result.scalar_one_or_none() is not None async def update_coupon_usage_count(session: AsyncSession, coupon_id: int): try: 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), ) ) await session.commit() logger.info(f"🔁 Обновлён счётчик купона {coupon_id}") except SQLAlchemyError as e: logger.error(f"❌ Ошибка при обновлении купона {coupon_id}: {e}") await session.rollback() 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