feat(promo): Добавлены промокоды на скидку в процентах
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy import delete
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from db.models import ActiveDiscount, PromoCode
|
||||
|
||||
|
||||
async def set_active_discount(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
promo_code_id: int,
|
||||
discount_percentage: int
|
||||
) -> Optional[ActiveDiscount]:
|
||||
"""
|
||||
Set active discount for user.
|
||||
Returns None if user already has an active discount (enforce one-at-a-time rule).
|
||||
"""
|
||||
# Check if user already has an active discount
|
||||
existing = await get_active_discount(session, user_id)
|
||||
if existing:
|
||||
logging.warning(
|
||||
f"User {user_id} already has active discount (promo_code_id: {existing.promo_code_id}). "
|
||||
f"Cannot activate new discount {promo_code_id}."
|
||||
)
|
||||
return None
|
||||
|
||||
# Create new active discount
|
||||
new_discount = ActiveDiscount(
|
||||
user_id=user_id,
|
||||
promo_code_id=promo_code_id,
|
||||
discount_percentage=discount_percentage,
|
||||
activated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
session.add(new_discount)
|
||||
await session.flush()
|
||||
await session.refresh(new_discount)
|
||||
logging.info(
|
||||
f"Active discount set for user {user_id}: promo_code_id={promo_code_id}, "
|
||||
f"discount={discount_percentage}%"
|
||||
)
|
||||
return new_discount
|
||||
|
||||
|
||||
async def get_active_discount(
|
||||
session: AsyncSession,
|
||||
user_id: int
|
||||
) -> Optional[ActiveDiscount]:
|
||||
"""Get active discount for user if exists."""
|
||||
stmt = select(ActiveDiscount).where(ActiveDiscount.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def clear_active_discount(
|
||||
session: AsyncSession,
|
||||
user_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Clear active discount for user.
|
||||
Returns True if discount was cleared, False if no discount was found.
|
||||
"""
|
||||
stmt = delete(ActiveDiscount).where(ActiveDiscount.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
await session.flush()
|
||||
cleared = result.rowcount > 0
|
||||
if cleared:
|
||||
logging.info(f"Active discount cleared for user {user_id}")
|
||||
return cleared
|
||||
@@ -43,6 +43,20 @@ async def get_active_promo_code_by_code_str(
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_active_discount_promo_code_by_code_str(
|
||||
session: AsyncSession, code_str: str) -> Optional[PromoCode]:
|
||||
"""Get active discount-type promo code by code string"""
|
||||
stmt = select(PromoCode).where(
|
||||
PromoCode.code == code_str.upper(),
|
||||
PromoCode.promo_type == "discount",
|
||||
PromoCode.is_active == True,
|
||||
PromoCode.current_activations < PromoCode.max_activations,
|
||||
or_(PromoCode.valid_until == None, PromoCode.valid_until
|
||||
> datetime.now(timezone.utc)))
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_all_active_promo_codes(session: AsyncSession,
|
||||
limit: int = 20,
|
||||
offset: int = 0) -> List[PromoCode]:
|
||||
|
||||
+30
-2
@@ -100,7 +100,12 @@ class Payment(Base):
|
||||
provider_payment_id = Column(String, unique=True, nullable=True)
|
||||
provider = Column(String, nullable=False, default="yookassa", index=True)
|
||||
idempotence_key = Column(String, unique=True, nullable=True)
|
||||
amount = Column(Float, nullable=False)
|
||||
amount = Column(Float, nullable=False) # Final amount paid (after discount if any)
|
||||
|
||||
# Discount tracking fields
|
||||
original_amount = Column(Float, nullable=True) # Amount before discount
|
||||
discount_applied = Column(Float, nullable=True) # Discount amount (not percentage)
|
||||
|
||||
currency = Column(String, nullable=False)
|
||||
status = Column(String, nullable=False, index=True)
|
||||
description = Column(String, nullable=True)
|
||||
@@ -154,7 +159,17 @@ class PromoCode(Base):
|
||||
|
||||
promo_code_id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
code = Column(String, unique=True, nullable=False, index=True)
|
||||
bonus_days = Column(Integer, nullable=False)
|
||||
|
||||
# Type field to distinguish promo code types
|
||||
promo_type = Column(String, nullable=False, default="bonus_days", index=True)
|
||||
# Values: "bonus_days" or "discount"
|
||||
|
||||
# For bonus_days type: number of days to add to subscription
|
||||
bonus_days = Column(Integer, nullable=True)
|
||||
|
||||
# For discount type: percentage discount (1-100)
|
||||
discount_percentage = Column(Integer, nullable=True)
|
||||
|
||||
max_activations = Column(Integer, nullable=False)
|
||||
current_activations = Column(Integer, default=0)
|
||||
is_active = Column(Boolean, default=True)
|
||||
@@ -191,6 +206,19 @@ class PromoCodeActivation(Base):
|
||||
name='uq_promo_user_activation'), )
|
||||
|
||||
|
||||
class ActiveDiscount(Base):
|
||||
"""Tracks pending discount promo codes awaiting payment (permanent until used)"""
|
||||
__tablename__ = "active_discounts"
|
||||
|
||||
user_id = Column(BigInteger, ForeignKey("users.user_id"), primary_key=True)
|
||||
promo_code_id = Column(Integer, ForeignKey("promo_codes.promo_code_id"), nullable=False)
|
||||
discount_percentage = Column(Integer, nullable=False)
|
||||
activated_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
promo_code = relationship("PromoCode")
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class MessageLog(Base):
|
||||
__tablename__ = "message_logs"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user