Improve public pages
This commit is contained in:
277
services/tenant/app/repositories/coupon_repository.py
Normal file
277
services/tenant/app/repositories/coupon_repository.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
Repository for coupon data access and validation
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
|
||||
from app.models.coupon import CouponModel, CouponRedemptionModel
|
||||
from shared.subscription.coupons import (
|
||||
Coupon,
|
||||
CouponRedemption,
|
||||
CouponValidationResult,
|
||||
DiscountType,
|
||||
calculate_trial_end_date,
|
||||
format_discount_description
|
||||
)
|
||||
|
||||
|
||||
class CouponRepository:
|
||||
"""Data access layer for coupon operations"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_coupon_by_code(self, code: str) -> Optional[Coupon]:
|
||||
"""
|
||||
Retrieve coupon by code.
|
||||
Returns None if not found.
|
||||
"""
|
||||
coupon_model = self.db.query(CouponModel).filter(
|
||||
CouponModel.code == code.upper()
|
||||
).first()
|
||||
|
||||
if not coupon_model:
|
||||
return None
|
||||
|
||||
return self._model_to_dataclass(coupon_model)
|
||||
|
||||
def validate_coupon(
|
||||
self,
|
||||
code: str,
|
||||
tenant_id: str
|
||||
) -> CouponValidationResult:
|
||||
"""
|
||||
Validate a coupon code for a specific tenant.
|
||||
Checks: existence, validity, redemption limits, and if tenant already used it.
|
||||
"""
|
||||
# Get coupon
|
||||
coupon = self.get_coupon_by_code(code)
|
||||
if not coupon:
|
||||
return CouponValidationResult(
|
||||
valid=False,
|
||||
coupon=None,
|
||||
error_message="Código de cupón inválido",
|
||||
discount_preview=None
|
||||
)
|
||||
|
||||
# Check if coupon can be redeemed
|
||||
can_redeem, reason = coupon.can_be_redeemed()
|
||||
if not can_redeem:
|
||||
error_messages = {
|
||||
"Coupon is inactive": "Este cupón no está activo",
|
||||
"Coupon is not yet valid": "Este cupón aún no es válido",
|
||||
"Coupon has expired": "Este cupón ha expirado",
|
||||
"Coupon has reached maximum redemptions": "Este cupón ha alcanzado su límite de usos"
|
||||
}
|
||||
return CouponValidationResult(
|
||||
valid=False,
|
||||
coupon=coupon,
|
||||
error_message=error_messages.get(reason, reason),
|
||||
discount_preview=None
|
||||
)
|
||||
|
||||
# Check if tenant already redeemed this coupon
|
||||
existing_redemption = self.db.query(CouponRedemptionModel).filter(
|
||||
and_(
|
||||
CouponRedemptionModel.tenant_id == tenant_id,
|
||||
CouponRedemptionModel.coupon_code == code.upper()
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing_redemption:
|
||||
return CouponValidationResult(
|
||||
valid=False,
|
||||
coupon=coupon,
|
||||
error_message="Ya has utilizado este cupón",
|
||||
discount_preview=None
|
||||
)
|
||||
|
||||
# Generate discount preview
|
||||
discount_preview = self._generate_discount_preview(coupon)
|
||||
|
||||
return CouponValidationResult(
|
||||
valid=True,
|
||||
coupon=coupon,
|
||||
error_message=None,
|
||||
discount_preview=discount_preview
|
||||
)
|
||||
|
||||
def redeem_coupon(
|
||||
self,
|
||||
code: str,
|
||||
tenant_id: str,
|
||||
base_trial_days: int = 14
|
||||
) -> tuple[bool, Optional[CouponRedemption], Optional[str]]:
|
||||
"""
|
||||
Redeem a coupon for a tenant.
|
||||
Returns (success, redemption, error_message)
|
||||
"""
|
||||
# Validate first
|
||||
validation = self.validate_coupon(code, tenant_id)
|
||||
if not validation.valid:
|
||||
return False, None, validation.error_message
|
||||
|
||||
coupon = validation.coupon
|
||||
|
||||
# Calculate discount applied
|
||||
discount_applied = self._calculate_discount_applied(
|
||||
coupon,
|
||||
base_trial_days
|
||||
)
|
||||
|
||||
# Create redemption record
|
||||
redemption_model = CouponRedemptionModel(
|
||||
tenant_id=tenant_id,
|
||||
coupon_code=code.upper(),
|
||||
redeemed_at=datetime.utcnow(),
|
||||
discount_applied=discount_applied,
|
||||
extra_data={
|
||||
"coupon_type": coupon.discount_type.value,
|
||||
"coupon_value": coupon.discount_value
|
||||
}
|
||||
)
|
||||
|
||||
self.db.add(redemption_model)
|
||||
|
||||
# Increment coupon redemption count
|
||||
coupon_model = self.db.query(CouponModel).filter(
|
||||
CouponModel.code == code.upper()
|
||||
).first()
|
||||
if coupon_model:
|
||||
coupon_model.current_redemptions += 1
|
||||
|
||||
try:
|
||||
self.db.commit()
|
||||
self.db.refresh(redemption_model)
|
||||
|
||||
redemption = CouponRedemption(
|
||||
id=str(redemption_model.id),
|
||||
tenant_id=redemption_model.tenant_id,
|
||||
coupon_code=redemption_model.coupon_code,
|
||||
redeemed_at=redemption_model.redeemed_at,
|
||||
discount_applied=redemption_model.discount_applied,
|
||||
extra_data=redemption_model.extra_data
|
||||
)
|
||||
|
||||
return True, redemption, None
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
return False, None, f"Error al aplicar el cupón: {str(e)}"
|
||||
|
||||
def get_redemption_by_tenant_and_code(
|
||||
self,
|
||||
tenant_id: str,
|
||||
code: str
|
||||
) -> Optional[CouponRedemption]:
|
||||
"""Get existing redemption for tenant and coupon code"""
|
||||
redemption_model = self.db.query(CouponRedemptionModel).filter(
|
||||
and_(
|
||||
CouponRedemptionModel.tenant_id == tenant_id,
|
||||
CouponRedemptionModel.coupon_code == code.upper()
|
||||
)
|
||||
).first()
|
||||
|
||||
if not redemption_model:
|
||||
return None
|
||||
|
||||
return CouponRedemption(
|
||||
id=str(redemption_model.id),
|
||||
tenant_id=redemption_model.tenant_id,
|
||||
coupon_code=redemption_model.coupon_code,
|
||||
redeemed_at=redemption_model.redeemed_at,
|
||||
discount_applied=redemption_model.discount_applied,
|
||||
extra_data=redemption_model.extra_data
|
||||
)
|
||||
|
||||
def get_coupon_usage_stats(self, code: str) -> Optional[dict]:
|
||||
"""Get usage statistics for a coupon"""
|
||||
coupon_model = self.db.query(CouponModel).filter(
|
||||
CouponModel.code == code.upper()
|
||||
).first()
|
||||
|
||||
if not coupon_model:
|
||||
return None
|
||||
|
||||
redemptions_count = self.db.query(CouponRedemptionModel).filter(
|
||||
CouponRedemptionModel.coupon_code == code.upper()
|
||||
).count()
|
||||
|
||||
return {
|
||||
"code": coupon_model.code,
|
||||
"current_redemptions": coupon_model.current_redemptions,
|
||||
"max_redemptions": coupon_model.max_redemptions,
|
||||
"redemptions_remaining": (
|
||||
coupon_model.max_redemptions - coupon_model.current_redemptions
|
||||
if coupon_model.max_redemptions
|
||||
else None
|
||||
),
|
||||
"active": coupon_model.active,
|
||||
"valid_from": coupon_model.valid_from.isoformat(),
|
||||
"valid_until": coupon_model.valid_until.isoformat() if coupon_model.valid_until else None
|
||||
}
|
||||
|
||||
def _model_to_dataclass(self, model: CouponModel) -> Coupon:
|
||||
"""Convert SQLAlchemy model to dataclass"""
|
||||
return Coupon(
|
||||
id=str(model.id),
|
||||
code=model.code,
|
||||
discount_type=DiscountType(model.discount_type),
|
||||
discount_value=model.discount_value,
|
||||
max_redemptions=model.max_redemptions,
|
||||
current_redemptions=model.current_redemptions,
|
||||
valid_from=model.valid_from,
|
||||
valid_until=model.valid_until,
|
||||
active=model.active,
|
||||
created_at=model.created_at,
|
||||
extra_data=model.extra_data
|
||||
)
|
||||
|
||||
def _generate_discount_preview(self, coupon: Coupon) -> dict:
|
||||
"""Generate a preview of the discount to be applied"""
|
||||
description = format_discount_description(coupon)
|
||||
|
||||
preview = {
|
||||
"description": description,
|
||||
"discount_type": coupon.discount_type.value,
|
||||
"discount_value": coupon.discount_value
|
||||
}
|
||||
|
||||
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
|
||||
trial_end = calculate_trial_end_date(14, coupon.discount_value)
|
||||
preview["trial_end_date"] = trial_end.isoformat()
|
||||
preview["total_trial_days"] = 14 + coupon.discount_value
|
||||
|
||||
return preview
|
||||
|
||||
def _calculate_discount_applied(
|
||||
self,
|
||||
coupon: Coupon,
|
||||
base_trial_days: int
|
||||
) -> dict:
|
||||
"""Calculate the actual discount that will be applied"""
|
||||
discount = {
|
||||
"type": coupon.discount_type.value,
|
||||
"value": coupon.discount_value,
|
||||
"description": format_discount_description(coupon)
|
||||
}
|
||||
|
||||
if coupon.discount_type == DiscountType.TRIAL_EXTENSION:
|
||||
total_trial_days = base_trial_days + coupon.discount_value
|
||||
trial_end = calculate_trial_end_date(base_trial_days, coupon.discount_value)
|
||||
|
||||
discount["base_trial_days"] = base_trial_days
|
||||
discount["extension_days"] = coupon.discount_value
|
||||
discount["total_trial_days"] = total_trial_days
|
||||
discount["trial_end_date"] = trial_end.isoformat()
|
||||
|
||||
elif coupon.discount_type == DiscountType.PERCENTAGE:
|
||||
discount["percentage_off"] = coupon.discount_value
|
||||
|
||||
elif coupon.discount_type == DiscountType.FIXED_AMOUNT:
|
||||
discount["amount_off_cents"] = coupon.discount_value
|
||||
discount["amount_off_euros"] = coupon.discount_value / 100
|
||||
|
||||
return discount
|
||||
Reference in New Issue
Block a user