Improve public pages
This commit is contained in:
123
shared/subscription/coupons.py
Normal file
123
shared/subscription/coupons.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Coupon system for subscription discounts and promotions.
|
||||
Supports trial extensions, percentage discounts, and fixed amount discounts.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
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.utcnow()
|
||||
|
||||
# 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.utcnow()
|
||||
|
||||
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.utcnow() + 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"
|
||||
Reference in New Issue
Block a user