# app/modules/loyalty/schemas/program.py """ Pydantic schemas for loyalty program operations. """ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field class PointsRewardConfig(BaseModel): """Configuration for a points-based reward.""" id: str = Field(..., description="Unique reward identifier") name: str = Field(..., max_length=100, description="Reward name") points_required: int = Field(..., gt=0, description="Points needed to redeem") description: str | None = Field(None, max_length=255, description="Reward description") is_active: bool = Field(True, description="Whether reward is currently available") class ProgramCreate(BaseModel): """Schema for creating a loyalty program.""" # Program type loyalty_type: str = Field( "stamps", pattern="^(stamps|points|hybrid)$", description="Program type: stamps, points, or hybrid", ) # Stamps configuration stamps_target: int = Field(10, ge=1, le=50, description="Stamps needed for reward") stamps_reward_description: str = Field( "Free item", max_length=255, description="Description of stamp reward", ) stamps_reward_value_cents: int | None = Field( None, ge=0, description="Value of reward in cents (for analytics)", ) # Points configuration points_per_euro: int = Field(10, ge=1, le=1000, description="Points per euro spent") points_rewards: list[PointsRewardConfig] = Field( default_factory=list, description="Available point rewards", ) # Anti-fraud cooldown_minutes: int = Field(15, ge=0, le=1440, description="Minutes between stamps") max_daily_stamps: int = Field(5, ge=1, le=50, description="Max stamps per card per day") require_staff_pin: bool = Field(True, description="Require staff PIN for operations") # Branding card_name: str | None = Field(None, max_length=100, description="Display name for card") card_color: str = Field( "#4F46E5", pattern="^#[0-9A-Fa-f]{6}$", description="Primary color (hex)", ) card_secondary_color: str | None = Field( None, pattern="^#[0-9A-Fa-f]{6}$", description="Secondary color (hex)", ) logo_url: str | None = Field(None, max_length=500, description="Logo URL") hero_image_url: str | None = Field(None, max_length=500, description="Hero image URL") # Terms terms_text: str | None = Field(None, description="Terms and conditions") privacy_url: str | None = Field(None, max_length=500, description="Privacy policy URL") class ProgramUpdate(BaseModel): """Schema for updating a loyalty program.""" model_config = ConfigDict(from_attributes=True) # Program type (cannot change from stamps to points after cards exist) loyalty_type: str | None = Field( None, pattern="^(stamps|points|hybrid)$", ) # Stamps configuration stamps_target: int | None = Field(None, ge=1, le=50) stamps_reward_description: str | None = Field(None, max_length=255) stamps_reward_value_cents: int | None = Field(None, ge=0) # Points configuration points_per_euro: int | None = Field(None, ge=1, le=1000) points_rewards: list[PointsRewardConfig] | None = None # Anti-fraud cooldown_minutes: int | None = Field(None, ge=0, le=1440) max_daily_stamps: int | None = Field(None, ge=1, le=50) require_staff_pin: bool | None = None # Branding card_name: str | None = Field(None, max_length=100) card_color: str | None = Field(None, pattern="^#[0-9A-Fa-f]{6}$") card_secondary_color: str | None = Field(None, pattern="^#[0-9A-Fa-f]{6}$") logo_url: str | None = Field(None, max_length=500) hero_image_url: str | None = Field(None, max_length=500) # Terms terms_text: str | None = None privacy_url: str | None = Field(None, max_length=500) # Wallet integration google_issuer_id: str | None = Field(None, max_length=100) apple_pass_type_id: str | None = Field(None, max_length=100) # Status is_active: bool | None = None class ProgramResponse(BaseModel): """Schema for loyalty program response.""" model_config = ConfigDict(from_attributes=True) id: int vendor_id: int loyalty_type: str # Stamps stamps_target: int stamps_reward_description: str stamps_reward_value_cents: int | None = None # Points points_per_euro: int points_rewards: list[PointsRewardConfig] = [] # Anti-fraud cooldown_minutes: int max_daily_stamps: int require_staff_pin: bool # Branding card_name: str | None = None card_color: str card_secondary_color: str | None = None logo_url: str | None = None hero_image_url: str | None = None # Terms terms_text: str | None = None privacy_url: str | None = None # Wallet google_issuer_id: str | None = None google_class_id: str | None = None apple_pass_type_id: str | None = None # Status is_active: bool activated_at: datetime | None = None created_at: datetime updated_at: datetime # Computed is_stamps_enabled: bool = False is_points_enabled: bool = False display_name: str = "Loyalty Card" class ProgramListResponse(BaseModel): """Schema for listing loyalty programs (admin).""" programs: list[ProgramResponse] total: int class ProgramStatsResponse(BaseModel): """Schema for program statistics.""" # Cards total_cards: int = 0 active_cards: int = 0 # Stamps (if enabled) total_stamps_issued: int = 0 total_stamps_redeemed: int = 0 stamps_this_month: int = 0 redemptions_this_month: int = 0 # Points (if enabled) total_points_issued: int = 0 total_points_redeemed: int = 0 points_this_month: int = 0 points_redeemed_this_month: int = 0 # Engagement cards_with_activity_30d: int = 0 average_stamps_per_card: float = 0.0 average_points_per_card: float = 0.0 # Value estimated_liability_cents: int = 0 # Unredeemed stamps/points value