Files
bakery-ia/shared/subscription/coupons.py

124 lines
3.8 KiB
Python
Raw Normal View History

2026-01-21 17:17:16 +01:00
"""
Coupon system for subscription discounts and promotions.
Supports trial extensions, percentage discounts, and fixed amount discounts.
"""
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from typing import Optional
class DiscountType(str, Enum):
"""Type of discount offered by a coupon"""
TRIAL_EXTENSION = "trial_extension" # Extends trial period by X days
PERCENTAGE = "percentage" # X% off subscription price
FIXED_AMOUNT = "fixed_amount" # Fixed amount off subscription price
@dataclass
class Coupon:
"""Coupon configuration"""
id: str
code: str
discount_type: DiscountType
discount_value: int # Days for trial_extension, percentage for percentage, cents for fixed_amount
max_redemptions: Optional[int] # None = unlimited
current_redemptions: int
valid_from: datetime
valid_until: Optional[datetime] # None = no expiry
active: bool
created_at: datetime
extra_data: Optional[dict] = None
def is_valid(self) -> bool:
"""Check if coupon is currently valid"""
now = datetime.now(timezone.utc)
# Check if active
if not self.active:
return False
# Check if started
if now < self.valid_from:
return False
# Check if expired
if self.valid_until and now > self.valid_until:
return False
# Check if max redemptions reached
if self.max_redemptions and self.current_redemptions >= self.max_redemptions:
return False
return True
def can_be_redeemed(self) -> tuple[bool, str]:
"""
Check if coupon can be redeemed.
Returns (can_redeem, reason_if_not)
"""
if not self.active:
return False, "Coupon is inactive"
now = datetime.now(timezone.utc)
if now < self.valid_from:
return False, "Coupon is not yet valid"
if self.valid_until and now > self.valid_until:
return False, "Coupon has expired"
if self.max_redemptions and self.current_redemptions >= self.max_redemptions:
return False, "Coupon has reached maximum redemptions"
return True, ""
@dataclass
class CouponRedemption:
"""Record of a coupon redemption"""
id: str
tenant_id: str
coupon_code: str
redeemed_at: datetime
discount_applied: dict # Details of the discount applied
extra_data: Optional[dict] = None
@dataclass
class CouponValidationResult:
"""Result of coupon validation"""
valid: bool
coupon: Optional[Coupon]
error_message: Optional[str]
discount_preview: Optional[dict] # Preview of discount to be applied
def calculate_trial_end_date(base_trial_days: int, extension_days: int) -> datetime:
"""Calculate trial end date with coupon extension"""
from datetime import timedelta
total_days = base_trial_days + extension_days
return datetime.now(timezone.utc) + timedelta(days=total_days)
def format_discount_description(coupon: Coupon) -> str:
"""Format human-readable discount description"""
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
months = coupon.discount_value // 30
days = coupon.discount_value % 30
if months > 0 and days == 0:
return f"{months} {'mes' if months == 1 else 'meses'} gratis"
elif months > 0:
return f"{months} {'mes' if months == 1 else 'meses'} y {days} días gratis"
else:
return f"{coupon.discount_value} días gratis"
elif coupon.discount_type == DiscountType.PERCENTAGE:
return f"{coupon.discount_value}% de descuento"
elif coupon.discount_type == DiscountType.FIXED_AMOUNT:
amount = coupon.discount_value / 100 # Convert cents to euros
return f"{amount:.2f} de descuento"
return "Descuento aplicado"