refactor: migrate modules from re-exports to canonical implementations
Move actual code implementations into module directories: - orders: 5 services, 4 models, order/invoice schemas - inventory: 3 services, 2 models, 30+ schemas - customers: 3 services, 2 models, customer schemas - messaging: 3 services, 2 models, message/notification schemas - monitoring: background_tasks_service - marketplace: 5+ services including letzshop submodule - dev_tools: code_quality_service, test_runner_service - billing: billing_service - contracts: definition.py Legacy files in app/services/, models/database/, models/schema/ now re-export from canonical module locations for backwards compatibility. Architecture validator passes with 0 errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
340
app/modules/customers/schemas/customer.py
Normal file
340
app/modules/customers/schemas/customer.py
Normal file
@@ -0,0 +1,340 @@
|
||||
# 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 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)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
vendor_id: int
|
||||
email: str
|
||||
first_name: str | None
|
||||
last_name: str | None
|
||||
phone: str | None
|
||||
customer_number: str
|
||||
marketing_consent: bool
|
||||
preferred_language: str | None
|
||||
last_order_date: datetime | None
|
||||
total_orders: int
|
||||
total_spent: Decimal
|
||||
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
|
||||
vendor_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
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Vendor Customer Management Response Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CustomerMessageResponse(BaseModel):
|
||||
"""Simple message response for customer operations."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
class VendorCustomerListResponse(BaseModel):
|
||||
"""Schema for vendor 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 vendor management."""
|
||||
|
||||
id: int | None = None
|
||||
vendor_id: int | None = None
|
||||
email: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
phone: str | None = None
|
||||
customer_number: str | None = None
|
||||
marketing_consent: bool | None = None
|
||||
preferred_language: str | None = None
|
||||
last_order_date: datetime | None = None
|
||||
total_orders: int | None = None
|
||||
total_spent: Decimal | 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
|
||||
with_orders: int = 0
|
||||
total_spent: float = 0.0
|
||||
total_orders: int = 0
|
||||
avg_order_value: float = 0.0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Admin Customer Management Response Schemas
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AdminCustomerItem(BaseModel):
|
||||
"""Admin customer list item with vendor info."""
|
||||
|
||||
id: int
|
||||
vendor_id: int
|
||||
email: str
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
phone: str | None = None
|
||||
customer_number: str
|
||||
marketing_consent: bool = False
|
||||
preferred_language: str | None = None
|
||||
last_order_date: datetime | None = None
|
||||
total_orders: int = 0
|
||||
total_spent: float = 0.0
|
||||
is_active: bool = True
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
vendor_name: str | None = None
|
||||
vendor_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."""
|
||||
|
||||
pass
|
||||
Reference in New Issue
Block a user