Improve public pages

This commit is contained in:
Urtzi Alfaro
2025-10-17 18:14:28 +02:00
parent d4060962e4
commit 7e089b80cf
46 changed files with 5734 additions and 1084 deletions

View File

@@ -45,7 +45,7 @@ class CloneOrchestrator:
name="inventory",
url=os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000"),
required=False, # Optional - provides ingredients/recipes
timeout=10.0
timeout=5.0 # Reduced from 10s - optimized data volume
),
ServiceDefinition(
name="recipes",
@@ -63,7 +63,7 @@ class CloneOrchestrator:
name="sales",
url=os.getenv("SALES_SERVICE_URL", "http://sales-service:8000"),
required=False, # Optional - provides sales history
timeout=10.0
timeout=5.0 # Reduced from 10s - optimized to 30 days history
),
ServiceDefinition(
name="orders",

View File

@@ -112,7 +112,7 @@ async def create_stock_batches_for_ingredient(
base_date: datetime
) -> list:
"""
Create 3-5 stock batches for a single ingredient with varied properties
Create 1-2 stock batches for a single ingredient (optimized for demo performance)
Args:
db: Database session
@@ -124,7 +124,7 @@ async def create_stock_batches_for_ingredient(
List of created Stock instances
"""
stocks = []
num_batches = random.randint(3, 5)
num_batches = random.randint(1, 2) # Reduced from 3-5 for faster demo loading
for i in range(num_batches):
# Calculate expiration days offset

View File

@@ -236,24 +236,24 @@ async def seed_sales(sales_db: AsyncSession):
results = []
# Seed for San Pablo (Traditional Bakery) - 90 days of history
# Seed for San Pablo (Traditional Bakery) - 30 days of history (optimized for fast demo loading)
logger.info("")
result_san_pablo = await seed_sales_for_tenant(
sales_db,
DEMO_TENANT_SAN_PABLO,
"Panadería San Pablo (Traditional)",
SAN_PABLO_PRODUCTS,
days_of_history=90
days_of_history=30
)
results.append(result_san_pablo)
# Seed for La Espiga (Central Workshop) - 90 days of history
# Seed for La Espiga (Central Workshop) - 30 days of history (optimized for fast demo loading)
result_la_espiga = await seed_sales_for_tenant(
sales_db,
DEMO_TENANT_LA_ESPIGA,
"Panadería La Espiga (Central Workshop)",
LA_ESPIGA_PRODUCTS,
days_of_history=90
days_of_history=30
)
results.append(result_la_espiga)
@@ -331,7 +331,7 @@ async def main():
logger.info("🎉 Success! Sales history is ready for cloning.")
logger.info("")
logger.info("Sales data includes:")
logger.info("90 days of historical sales")
logger.info("30 days of historical sales (optimized for demo performance)")
logger.info(" • 4 product types per tenant")
logger.info(" • Realistic weekly patterns (higher on weekends)")
logger.info(" • Random variance and occasional closures")

View File

@@ -85,20 +85,77 @@ def get_payment_service():
async def register_bakery(
bakery_data: BakeryRegistration,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service),
payment_service: PaymentService = Depends(get_payment_service)
):
"""Register a new bakery/tenant with enhanced validation and features"""
try:
# Validate coupon if provided
coupon_validation = None
if bakery_data.coupon_code:
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
async with database_manager.get_session() as session:
# Temp tenant ID for validation (will be replaced with actual after creation)
temp_tenant_id = f"temp_{current_user['user_id']}"
coupon_validation = payment_service.validate_coupon_code(
bakery_data.coupon_code,
temp_tenant_id,
session
)
if not coupon_validation["valid"]:
logger.warning(
"Invalid coupon code provided during registration",
coupon_code=bakery_data.coupon_code,
error=coupon_validation["error_message"]
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=coupon_validation["error_message"]
)
# Create bakery/tenant
result = await tenant_service.create_bakery(
bakery_data,
current_user["user_id"]
)
# If coupon was validated, redeem it now with actual tenant_id
if coupon_validation and coupon_validation["valid"]:
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
async with database_manager.get_session() as session:
success, discount, error = payment_service.redeem_coupon(
bakery_data.coupon_code,
result.id,
session
)
if success:
logger.info(
"Coupon redeemed successfully",
tenant_id=result.id,
coupon_code=bakery_data.coupon_code,
discount=discount
)
else:
logger.warning(
"Failed to redeem coupon after registration",
tenant_id=result.id,
coupon_code=bakery_data.coupon_code,
error=error
)
logger.info("Bakery registered successfully",
name=bakery_data.name,
owner_email=current_user.get('email'),
tenant_id=result.id)
tenant_id=result.id,
coupon_applied=bakery_data.coupon_code is not None)
return result

View File

@@ -13,6 +13,7 @@ AuditLog = create_audit_log_model(Base)
# Import all models to register them with the Base metadata
from .tenants import Tenant, TenantMember, Subscription
from .coupon import CouponModel, CouponRedemptionModel
# List all models for easier access
__all__ = [
@@ -20,4 +21,6 @@ __all__ = [
"TenantMember",
"Subscription",
"AuditLog",
"CouponModel",
"CouponRedemptionModel",
]

View File

@@ -0,0 +1,64 @@
"""
SQLAlchemy models for coupon system
"""
from datetime import datetime
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, JSON, Index
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
import uuid
from shared.database import Base
class CouponModel(Base):
"""Coupon configuration table"""
__tablename__ = "coupons"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
code = Column(String(50), unique=True, nullable=False, index=True)
discount_type = Column(String(20), nullable=False) # trial_extension, percentage, fixed_amount
discount_value = Column(Integer, nullable=False) # Days/percentage/cents depending on type
max_redemptions = Column(Integer, nullable=True) # None = unlimited
current_redemptions = Column(Integer, nullable=False, default=0)
valid_from = Column(DateTime(timezone=True), nullable=False)
valid_until = Column(DateTime(timezone=True), nullable=True) # None = no expiry
active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
extra_data = Column(JSON, nullable=True) # Renamed from metadata to avoid SQLAlchemy reserved name
# Relationships
redemptions = relationship("CouponRedemptionModel", back_populates="coupon")
# Indexes for performance
__table_args__ = (
Index('idx_coupon_code_active', 'code', 'active'),
Index('idx_coupon_valid_dates', 'valid_from', 'valid_until'),
)
def __repr__(self):
return f"<Coupon(code='{self.code}', type='{self.discount_type}', value={self.discount_value})>"
class CouponRedemptionModel(Base):
"""Coupon redemption history table"""
__tablename__ = "coupon_redemptions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(String(255), nullable=False, index=True)
coupon_code = Column(String(50), ForeignKey('coupons.code'), nullable=False)
redeemed_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow)
discount_applied = Column(JSON, nullable=False) # Details of discount applied
extra_data = Column(JSON, nullable=True) # Renamed from metadata to avoid SQLAlchemy reserved name
# Relationships
coupon = relationship("CouponModel", back_populates="redemptions")
# Constraints
__table_args__ = (
Index('idx_redemption_tenant', 'tenant_id'),
Index('idx_redemption_coupon', 'coupon_code'),
Index('idx_redemption_tenant_coupon', 'tenant_id', 'coupon_code'), # Prevent duplicate redemptions
)
def __repr__(self):
return f"<CouponRedemption(tenant_id='{self.tenant_id}', code='{self.coupon_code}')>"

View 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

View File

@@ -18,6 +18,7 @@ class BakeryRegistration(BaseModel):
phone: str = Field(..., min_length=9, max_length=20)
business_type: str = Field(default="bakery")
business_model: Optional[str] = Field(default="individual_bakery")
coupon_code: Optional[str] = Field(None, max_length=50, description="Promotional coupon code")
@validator('phone')
def validate_spanish_phone(cls, v):

View File

@@ -6,12 +6,14 @@ This service abstracts payment provider interactions and makes the system paymen
import structlog
from typing import Dict, Any, Optional
import uuid
from sqlalchemy.orm import Session
from app.core.config import settings
from shared.clients.payment_client import PaymentProvider, PaymentCustomer, Subscription, PaymentMethod
from shared.clients.stripe_client import StripeProvider
from shared.database.base import create_database_manager
from app.repositories.subscription_repository import SubscriptionRepository
from app.repositories.coupon_repository import CouponRepository
from app.models.tenants import Subscription as SubscriptionModel
logger = structlog.get_logger()
@@ -19,18 +21,19 @@ logger = structlog.get_logger()
class PaymentService:
"""Service for handling payment provider interactions"""
def __init__(self):
def __init__(self, db_session: Optional[Session] = None):
# Initialize payment provider based on configuration
# For now, we'll use Stripe, but this can be swapped for other providers
self.payment_provider: PaymentProvider = StripeProvider(
api_key=settings.STRIPE_SECRET_KEY,
webhook_secret=settings.STRIPE_WEBHOOK_SECRET
)
# Initialize database components
self.database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
self.subscription_repo = SubscriptionRepository(SubscriptionModel, None) # Will be set in methods
self.db_session = db_session # Optional session for coupon operations
async def create_customer(self, user_data: Dict[str, Any]) -> PaymentCustomer:
"""Create a customer in the payment provider system"""
@@ -68,35 +71,121 @@ class PaymentService:
logger.error("Failed to create subscription in payment provider", error=str(e))
raise e
def validate_coupon_code(
self,
coupon_code: str,
tenant_id: str,
db_session: Session
) -> Dict[str, Any]:
"""
Validate a coupon code for a tenant.
Returns validation result with discount preview.
"""
try:
coupon_repo = CouponRepository(db_session)
validation = coupon_repo.validate_coupon(coupon_code, tenant_id)
return {
"valid": validation.valid,
"error_message": validation.error_message,
"discount_preview": validation.discount_preview,
"coupon": {
"code": validation.coupon.code,
"discount_type": validation.coupon.discount_type.value,
"discount_value": validation.coupon.discount_value
} if validation.coupon else None
}
except Exception as e:
logger.error("Failed to validate coupon", error=str(e), coupon_code=coupon_code)
return {
"valid": False,
"error_message": "Error al validar el cupón",
"discount_preview": None,
"coupon": None
}
def redeem_coupon(
self,
coupon_code: str,
tenant_id: str,
db_session: Session,
base_trial_days: int = 14
) -> tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
"""
Redeem a coupon for a tenant.
Returns (success, discount_applied, error_message)
"""
try:
coupon_repo = CouponRepository(db_session)
success, redemption, error = coupon_repo.redeem_coupon(
coupon_code,
tenant_id,
base_trial_days
)
if success and redemption:
return True, redemption.discount_applied, None
else:
return False, None, error
except Exception as e:
logger.error("Failed to redeem coupon", error=str(e), coupon_code=coupon_code)
return False, None, f"Error al aplicar el cupón: {str(e)}"
async def process_registration_with_subscription(
self,
user_data: Dict[str, Any],
plan_id: str,
self,
user_data: Dict[str, Any],
plan_id: str,
payment_method_id: str,
use_trial: bool = False
use_trial: bool = False,
coupon_code: Optional[str] = None,
db_session: Optional[Session] = None
) -> Dict[str, Any]:
"""Process user registration with subscription creation"""
try:
# Create customer in payment provider
customer = await self.create_customer(user_data)
# Determine trial period
trial_period_days = None
if use_trial:
trial_period_days = 90 # 3 months trial for pilot users
# Determine trial period (default 14 days)
trial_period_days = 14 if use_trial else 0
# Apply coupon if provided
coupon_discount = None
if coupon_code and db_session:
# Redeem coupon
success, discount, error = self.redeem_coupon(
coupon_code,
user_data.get('tenant_id'),
db_session,
trial_period_days
)
if success and discount:
coupon_discount = discount
# Update trial period if coupon extends it
if discount.get("type") == "trial_extension":
trial_period_days = discount.get("total_trial_days", trial_period_days)
logger.info(
"Coupon applied successfully",
coupon_code=coupon_code,
extended_trial_days=trial_period_days
)
else:
logger.warning("Failed to apply coupon", error=error, coupon_code=coupon_code)
# Create subscription
subscription = await self.create_subscription(
customer.id,
plan_id,
payment_method_id,
trial_period_days
trial_period_days if trial_period_days > 0 else None
)
# Save subscription to database
async with self.database_manager.get_session() as session:
self.subscription_repo.session = session
subscription_record = await self.subscription_repo.create({
subscription_data = {
'id': str(uuid.uuid4()),
'tenant_id': user_data.get('tenant_id'),
'customer_id': customer.id,
@@ -106,15 +195,26 @@ class PaymentService:
'current_period_start': subscription.current_period_start,
'current_period_end': subscription.current_period_end,
'created_at': subscription.created_at,
'trial_period_days': trial_period_days
})
return {
'trial_period_days': trial_period_days if trial_period_days > 0 else None
}
subscription_record = await self.subscription_repo.create(subscription_data)
result = {
'customer_id': customer.id,
'subscription_id': subscription.id,
'status': subscription.status,
'trial_period_days': trial_period_days
}
# Include coupon info if applied
if coupon_discount:
result['coupon_applied'] = {
'code': coupon_code,
'discount': coupon_discount
}
return result
except Exception as e:
logger.error("Failed to process registration with subscription", error=str(e))
raise e

View File

@@ -0,0 +1,69 @@
"""add_coupon_system
Revision ID: 20251017_0000
Revises: 20251016_0000
Create Date: 2025-10-17 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import uuid
# revision identifiers, used by Alembic.
revision = '20251017_0000'
down_revision = '20251016_0000'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create coupons table
op.create_table(
'coupons',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
sa.Column('code', sa.String(50), nullable=False, unique=True),
sa.Column('discount_type', sa.String(20), nullable=False),
sa.Column('discount_value', sa.Integer(), nullable=False),
sa.Column('max_redemptions', sa.Integer(), nullable=True),
sa.Column('current_redemptions', sa.Integer(), nullable=False, server_default='0'),
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=False),
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
sa.Column('active', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column('extra_data', postgresql.JSON(astext_type=sa.Text()), nullable=True),
)
# Create indexes for coupons table
op.create_index('idx_coupon_code_active', 'coupons', ['code', 'active'])
op.create_index('idx_coupon_valid_dates', 'coupons', ['valid_from', 'valid_until'])
# Create coupon_redemptions table
op.create_table(
'coupon_redemptions',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
sa.Column('tenant_id', sa.String(255), nullable=False),
sa.Column('coupon_code', sa.String(50), nullable=False),
sa.Column('redeemed_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
sa.Column('discount_applied', postgresql.JSON(astext_type=sa.Text()), nullable=False),
sa.Column('extra_data', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.ForeignKeyConstraint(['coupon_code'], ['coupons.code'], name='fk_coupon_redemption_code'),
)
# Create indexes for coupon_redemptions table
op.create_index('idx_redemption_tenant', 'coupon_redemptions', ['tenant_id'])
op.create_index('idx_redemption_coupon', 'coupon_redemptions', ['coupon_code'])
op.create_index('idx_redemption_tenant_coupon', 'coupon_redemptions', ['tenant_id', 'coupon_code'])
def downgrade() -> None:
# Drop indexes first
op.drop_index('idx_redemption_tenant_coupon', table_name='coupon_redemptions')
op.drop_index('idx_redemption_coupon', table_name='coupon_redemptions')
op.drop_index('idx_redemption_tenant', table_name='coupon_redemptions')
op.drop_index('idx_coupon_valid_dates', table_name='coupons')
op.drop_index('idx_coupon_code_active', table_name='coupons')
# Drop tables
op.drop_table('coupon_redemptions')
op.drop_table('coupons')

View File

@@ -0,0 +1,102 @@
"""
Seed script to create the PILOT2025 coupon for the pilot customer program.
This coupon provides 3 months (90 days) free trial extension for the first 20 customers.
"""
import sys
import os
from datetime import datetime, timedelta
import uuid
# Add project root to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
from sqlalchemy.orm import Session
from app.models.coupon import CouponModel
from shared.database import get_db
def seed_pilot_coupon(db: Session):
"""Create or update the PILOT2025 coupon"""
coupon_code = "PILOT2025"
# Check if coupon already exists
existing_coupon = db.query(CouponModel).filter(
CouponModel.code == coupon_code
).first()
if existing_coupon:
print(f"✓ Coupon {coupon_code} already exists")
print(f" Current redemptions: {existing_coupon.current_redemptions}/{existing_coupon.max_redemptions}")
print(f" Active: {existing_coupon.active}")
print(f" Valid from: {existing_coupon.valid_from}")
print(f" Valid until: {existing_coupon.valid_until}")
return existing_coupon
# Create new coupon
now = datetime.utcnow()
valid_until = now + timedelta(days=180) # Valid for 6 months
coupon = CouponModel(
id=uuid.uuid4(),
code=coupon_code,
discount_type="trial_extension",
discount_value=90, # 90 days = 3 months
max_redemptions=20, # First 20 pilot customers
current_redemptions=0,
valid_from=now,
valid_until=valid_until,
active=True,
created_at=now,
extra_data={
"program": "pilot_launch_2025",
"description": "Programa piloto - 3 meses gratis para los primeros 20 clientes",
"terms": "Válido para nuevos registros únicamente. Un cupón por cliente."
}
)
db.add(coupon)
db.commit()
db.refresh(coupon)
print(f"✓ Successfully created coupon: {coupon_code}")
print(f" Type: Trial Extension")
print(f" Value: 90 days (3 months)")
print(f" Max redemptions: 20")
print(f" Valid from: {coupon.valid_from}")
print(f" Valid until: {coupon.valid_until}")
print(f" ID: {coupon.id}")
return coupon
def main():
"""Main execution function"""
print("=" * 60)
print("Seeding PILOT2025 Coupon for Pilot Customer Program")
print("=" * 60)
print()
try:
# Get database session
db = next(get_db())
# Seed the coupon
seed_pilot_coupon(db)
print()
print("=" * 60)
print("✓ Coupon seeding completed successfully!")
print("=" * 60)
except Exception as e:
print(f"✗ Error seeding coupon: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
main()