refactor(arch): use CustomerContext schema for dependency injection

Phase 5 of storefront restructure plan - fix direct model imports in
API routes by using schemas for dependency injection.

Created CustomerContext schema:
- Lightweight Pydantic model for customer data in API routes
- Populated from Customer DB model in auth dependency
- Contains all fields needed by storefront routes
- Includes from_db_model() factory method

Updated app/api/deps.py:
- _validate_customer_token now returns CustomerContext instead of Customer
- Updated docstrings for all customer auth functions

Updated module storefront routes:
- customers: Uses CustomerContext for profile/address endpoints
- orders: Uses CustomerContext for order history endpoints
- checkout: Uses CustomerContext for order placement
- messaging: Uses CustomerContext for messaging endpoints

This enforces the layered architecture (Routes → Services → Models)
by ensuring API routes never import database models directly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 23:06:21 +01:00
parent 2755c2f780
commit 4b8e1b1d88
8 changed files with 137 additions and 32 deletions

View File

@@ -0,0 +1,99 @@
# app/modules/customers/schemas/context.py
"""
Customer context schema for dependency injection in storefront routes.
This schema provides a clean interface for customer data in API routes,
avoiding direct database model imports in the API layer.
"""
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, ConfigDict
class CustomerContext(BaseModel):
"""
Customer context for dependency injection in storefront routes.
This is a lightweight schema that contains the customer information
needed by API routes. It's populated from the Customer database model
in the authentication dependency.
Usage:
@router.get("/profile")
def get_profile(
customer: CustomerContext = Depends(get_current_customer_api),
):
return {"email": customer.email}
"""
model_config = ConfigDict(from_attributes=True)
# Core identification
id: int
vendor_id: int
email: str
customer_number: str
# Profile info
first_name: str | None = None
last_name: str | None = None
phone: str | None = None
# Preferences
marketing_consent: bool = False
preferred_language: str | None = None
# Stats (for order placement)
total_orders: int = 0
total_spent: Decimal = Decimal("0.00")
last_order_date: datetime | None = None
# Status
is_active: bool = True
# Timestamps
created_at: datetime | None = None
updated_at: datetime | None = None
# Password hash (needed for password change endpoint)
# This is included but should not be exposed in API responses
hashed_password: str | None = None
@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
@classmethod
def from_db_model(cls, customer) -> "CustomerContext":
"""
Create CustomerContext from a Customer database model.
Args:
customer: Customer database model instance
Returns:
CustomerContext: Pydantic schema instance
"""
return cls(
id=customer.id,
vendor_id=customer.vendor_id,
email=customer.email,
customer_number=customer.customer_number,
first_name=customer.first_name,
last_name=customer.last_name,
phone=customer.phone,
marketing_consent=customer.marketing_consent,
preferred_language=customer.preferred_language,
total_orders=customer.total_orders or 0,
total_spent=customer.total_spent or Decimal("0.00"),
last_order_date=customer.last_order_date,
is_active=customer.is_active,
created_at=customer.created_at,
updated_at=customer.updated_at,
hashed_password=customer.hashed_password,
)