# app/modules/customers/schemas/customer.py """ Pydantic schemas for customer-related operations. Provides schemas for: - Customer registration and authentication - Customer profile management - Customer addresses - Admin customer management """ from datetime import date, datetime from decimal import Decimal from pydantic import BaseModel, EmailStr, Field, field_validator # ============================================================================ # Customer Registration & Authentication # ============================================================================ class CustomerRegister(BaseModel): """Schema for customer registration.""" email: EmailStr = Field(..., description="Customer email address") password: str = Field( ..., min_length=8, description="Password (minimum 8 characters)" ) first_name: str = Field(..., min_length=1, max_length=100) last_name: str = Field(..., min_length=1, max_length=100) phone: str | None = Field(None, max_length=50) marketing_consent: bool = Field(default=False) preferred_language: str | None = Field( None, description="Preferred language (en, fr, de, lb)" ) @field_validator("email") @classmethod def email_lowercase(cls, v: str) -> str: """Convert email to lowercase.""" return v.lower() @field_validator("password") @classmethod def password_strength(cls, v: str) -> str: """Validate password strength.""" if len(v) < 8: raise ValueError("Password must be at least 8 characters") if not any(char.isdigit() for char in v): raise ValueError("Password must contain at least one digit") if not any(char.isalpha() for char in v): raise ValueError("Password must contain at least one letter") return v class CustomerUpdate(BaseModel): """Schema for updating customer profile.""" email: EmailStr | None = None first_name: str | None = Field(None, min_length=1, max_length=100) last_name: str | None = Field(None, min_length=1, max_length=100) phone: str | None = Field(None, max_length=50) birth_date: date | None = Field( None, description="Date of birth (YYYY-MM-DD)" ) marketing_consent: bool | None = None preferred_language: str | None = Field( None, description="Preferred language (en, fr, de, lb)" ) @field_validator("email") @classmethod def email_lowercase(cls, v: str | None) -> str | None: """Convert email to lowercase.""" return v.lower() if v else None @field_validator("birth_date") @classmethod def birth_date_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("birth_date must be in the past") # Plausible human age range — guards against typos like 0001-01-01 years = (today - v).days / 365.25 if years < 13 or years > 120: raise ValueError("birth_date implies an implausible age") return v class CustomerPasswordChange(BaseModel): """Schema for customer password change.""" current_password: str = Field(..., description="Current password") new_password: str = Field( ..., min_length=8, description="New password (minimum 8 characters)" ) confirm_password: str = Field(..., description="Confirm new password") @field_validator("new_password") @classmethod def password_strength(cls, v: str) -> str: """Validate password strength.""" if len(v) < 8: raise ValueError("Password must be at least 8 characters") if not any(char.isdigit() for char in v): raise ValueError("Password must contain at least one digit") if not any(char.isalpha() for char in v): raise ValueError("Password must contain at least one letter") return v # ============================================================================ # Customer Response # ============================================================================ class CustomerResponse(BaseModel): """Schema for customer response (excludes password).""" id: int store_id: int email: str first_name: str | None last_name: str | None phone: str | None birth_date: date | None = None customer_number: str marketing_consent: bool preferred_language: str | None is_active: bool created_at: datetime updated_at: datetime model_config = {"from_attributes": True} @property def full_name(self) -> str: """Get customer full name.""" if self.first_name and self.last_name: return f"{self.first_name} {self.last_name}" return self.email class CustomerListResponse(BaseModel): """Schema for paginated customer list.""" customers: list[CustomerResponse] total: int page: int per_page: int total_pages: int # ============================================================================ # Customer Address # ============================================================================ class CustomerAddressCreate(BaseModel): """Schema for creating customer address.""" address_type: str = Field(..., pattern="^(billing|shipping)$") first_name: str = Field(..., min_length=1, max_length=100) last_name: str = Field(..., min_length=1, max_length=100) company: str | None = Field(None, max_length=200) address_line_1: str = Field(..., min_length=1, max_length=255) address_line_2: str | None = Field(None, max_length=255) city: str = Field(..., min_length=1, max_length=100) postal_code: str = Field(..., min_length=1, max_length=20) country_name: str = Field(..., min_length=2, max_length=100) country_iso: str = Field(..., min_length=2, max_length=5) is_default: bool = Field(default=False) class CustomerAddressUpdate(BaseModel): """Schema for updating customer address.""" address_type: str | None = Field(None, pattern="^(billing|shipping)$") first_name: str | None = Field(None, min_length=1, max_length=100) last_name: str | None = Field(None, min_length=1, max_length=100) company: str | None = Field(None, max_length=200) address_line_1: str | None = Field(None, min_length=1, max_length=255) address_line_2: str | None = Field(None, max_length=255) city: str | None = Field(None, min_length=1, max_length=100) postal_code: str | None = Field(None, min_length=1, max_length=20) country_name: str | None = Field(None, min_length=2, max_length=100) country_iso: str | None = Field(None, min_length=2, max_length=5) is_default: bool | None = None class CustomerAddressResponse(BaseModel): """Schema for customer address response.""" id: int store_id: int customer_id: int address_type: str first_name: str last_name: str company: str | None address_line_1: str address_line_2: str | None city: str postal_code: str country_name: str country_iso: str is_default: bool created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class CustomerAddressListResponse(BaseModel): """Schema for customer address list response.""" addresses: list[CustomerAddressResponse] total: int # ============================================================================ # Customer Preferences # ============================================================================ class CustomerPreferencesUpdate(BaseModel): """Schema for updating customer preferences.""" marketing_consent: bool | None = None preferred_language: str | None = Field( None, description="Preferred language (en, fr, de, lb)" ) currency: str | None = Field(None, max_length=3) notification_preferences: dict[str, bool] | None = None # ============================================================================ # Store Customer Management Response Schemas # ============================================================================ class CustomerMessageResponse(BaseModel): """Simple message response for customer operations.""" message: str class StoreCustomerListResponse(BaseModel): """Schema for store customer list with skip/limit pagination.""" customers: list[CustomerResponse] = [] total: int = 0 skip: int = 0 limit: int = 100 message: str | None = None class CustomerDetailResponse(BaseModel): """Detailed customer response for store management. Note: Order-related statistics (total_orders, total_spent, last_order_date) are available via the orders module endpoint: GET /api/store/customers/{customer_id}/order-stats """ id: int | None = None store_id: int | None = None email: str | None = None first_name: str | None = None last_name: str | None = None phone: str | None = None birth_date: date | None = None customer_number: str | None = None marketing_consent: bool | None = None preferred_language: str | None = None is_active: bool | None = None created_at: datetime | None = None updated_at: datetime | None = None message: str | None = None model_config = {"from_attributes": True} class CustomerOrderInfo(BaseModel): """Basic order info for customer order history.""" id: int order_number: str status: str total: Decimal created_at: datetime class CustomerOrdersResponse(BaseModel): """Response for customer order history.""" orders: list[CustomerOrderInfo] = [] total: int = 0 message: str | None = None class CustomerStatisticsResponse(BaseModel): """Response for customer statistics.""" total: int = 0 active: int = 0 inactive: int = 0 # ============================================================================ # Admin Customer Management Response Schemas # ============================================================================ class AdminCustomerItem(BaseModel): """Admin customer list item with store info.""" id: int store_id: int email: str first_name: str | None = None last_name: str | None = None phone: str | None = None birth_date: date | None = None customer_number: str marketing_consent: bool = False preferred_language: str | None = None is_active: bool = True created_at: datetime updated_at: datetime store_name: str | None = None store_code: str | None = None model_config = {"from_attributes": True} class AdminCustomerListResponse(BaseModel): """Admin paginated customer list with skip/limit.""" customers: list[AdminCustomerItem] = [] total: int = 0 skip: int = 0 limit: int = 20 class AdminCustomerDetailResponse(AdminCustomerItem): """Detailed customer response for admin."""