Files
orion/app/modules/loyalty/schemas/card.py
Samir Boulahtit bb3d6f0012
Some checks failed
CI / pytest (push) Failing after 2h22m22s
CI / validate (push) Successful in 27s
CI / dependency-scanning (push) Successful in 31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 12s
fix(loyalty): card detail — enrolled store name + copy buttons
- Fix "Enrolled at: Unknown" by resolving enrolled_at_store_name from
  the store service and adding it to CardDetailResponse schema.
- Add clipboard-copy buttons next to card number, customer name,
  email, and phone fields using the shared Utils.copyToClipboard()
  utility with toast feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:31:53 +02:00

209 lines
5.1 KiB
Python

# 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