# app/modules/loyalty/schemas/card.py """ Pydantic schemas for loyalty card operations. Merchant-based cards: - Cards belong to a merchant's loyalty program - One card per customer per merchant - Can be used at any store within the merchant """ from datetime import date, datetime from pydantic import BaseModel, ConfigDict, Field, field_validator class CardEnrollRequest(BaseModel): """Schema for enrolling a customer in a loyalty program.""" customer_id: int | None = Field( None, description="Customer ID (required for store API, optional for public enrollment)", ) email: str | None = Field( None, description="Customer email (for public enrollment without customer_id)", ) # Self-enrollment fields (used to create customer if not found) customer_name: str | None = Field( None, description="Full name for self-enrollment" ) customer_phone: str | None = Field( None, description="Phone number for self-enrollment" ) customer_birthday: date | None = Field( None, description="Birthday (YYYY-MM-DD) for self-enrollment" ) @field_validator("customer_birthday") @classmethod def birthday_sane(cls, v: date | None) -> date | None: """Birthday must be in the past and within a plausible age range.""" if v is None: return v today = date.today() if v >= today: raise ValueError("customer_birthday must be in the past") years = (today - v).days / 365.25 if years < 13 or years > 120: raise ValueError("customer_birthday implies an implausible age") return v class CardResponse(BaseModel): """Schema for loyalty card response (summary).""" model_config = ConfigDict(from_attributes=True) id: int card_number: str customer_id: int merchant_id: int program_id: int enrolled_at_store_id: int | None = None # Customer info (for list views) customer_name: str | None = None customer_email: str | None = None # Stamps stamp_count: int stamps_target: int # From program stamps_until_reward: int total_stamps_earned: int stamps_redeemed: int # Points points_balance: int total_points_earned: int points_redeemed: int # Status is_active: bool created_at: datetime # Wallet has_google_wallet: bool = False has_apple_wallet: bool = False class CardDetailResponse(CardResponse): """Schema for detailed loyalty card response.""" # QR code qr_code_data: str qr_code_url: str | None = None # Generated QR code image URL # Customer info customer_name: str | None = None customer_email: str | None = None # Merchant info merchant_name: str | None = None enrolled_at_store_name: str | None = None # Program info program_name: str program_type: str reward_description: str | None = None # Activity last_stamp_at: datetime | None = None last_points_at: datetime | None = None last_redemption_at: datetime | None = None last_activity_at: datetime | None = None # Wallet URLs google_wallet_url: str | None = None apple_wallet_url: str | None = None class CardListResponse(BaseModel): """Schema for listing loyalty cards.""" cards: list[CardResponse] total: int class CardLookupResponse(BaseModel): """Schema for card lookup by QR code or card number.""" # Card info id: int card_number: str # Customer customer_id: int customer_name: str | None = None customer_email: str # Merchant context merchant_id: int merchant_name: str | None = None # Current balances stamp_count: int stamps_target: int stamps_until_reward: int points_balance: int # Can redeem? can_redeem_stamps: bool = False stamp_reward_description: str | None = None # Available points rewards available_rewards: list[dict] = [] # Cooldown status can_stamp: bool = True cooldown_ends_at: datetime | None = None # Today's activity stamps_today: int = 0 max_daily_stamps: int = 5 can_earn_more_stamps: bool = True class TransactionResponse(BaseModel): """Schema for a loyalty transaction.""" model_config = ConfigDict(from_attributes=True) id: int card_id: int store_id: int | None = None store_name: str | None = None transaction_type: str # Deltas stamps_delta: int = 0 points_delta: int = 0 # Balances after stamps_balance_after: int | None = None points_balance_after: int | None = None # Context purchase_amount_cents: int | None = None order_reference: str | None = None reward_id: str | None = None reward_description: str | None = None notes: str | None = None # Customer customer_name: str | None = None # Staff staff_name: str | None = None # Timestamps transaction_at: datetime created_at: datetime class TransactionListResponse(BaseModel): """Schema for listing transactions.""" transactions: list[TransactionResponse] total: int