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

@@ -24,13 +24,13 @@ from app.api.deps import get_current_customer_api
from app.core.database import get_db
from app.core.environment import should_use_secure_cookies
from app.exceptions import ValidationException, VendorNotFoundException
from app.modules.customers.schemas import CustomerContext
from app.modules.customers.services import (
customer_address_service,
customer_service,
)
from app.services.auth_service import AuthService
from app.services.email_service import EmailService
from models.database.customer import Customer
from models.database.password_reset_token import PasswordResetToken
from models.schema.auth import (
LogoutResponse,
@@ -371,7 +371,7 @@ def reset_password(
@router.get("/profile", response_model=CustomerResponse) # authenticated
def get_profile(
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -390,7 +390,7 @@ def get_profile(
@router.put("/profile", response_model=CustomerResponse)
def update_profile(
update_data: CustomerUpdate,
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -444,7 +444,7 @@ def update_profile(
@router.put("/profile/password", response_model=dict)
def change_password(
password_data: CustomerPasswordChange,
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -492,7 +492,7 @@ def change_password(
@router.get("/addresses", response_model=CustomerAddressListResponse) # authenticated
def list_addresses(
request: Request,
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -528,7 +528,7 @@ def list_addresses(
def get_address(
request: Request,
address_id: int = Path(..., description="Address ID", gt=0),
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -561,7 +561,7 @@ def get_address(
def create_address(
request: Request,
address_data: CustomerAddressCreate,
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -610,7 +610,7 @@ def update_address(
request: Request,
address_data: CustomerAddressUpdate,
address_id: int = Path(..., description="Address ID", gt=0),
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -654,7 +654,7 @@ def update_address(
def delete_address(
request: Request,
address_id: int = Path(..., description="Address ID", gt=0),
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
@@ -691,7 +691,7 @@ def delete_address(
def set_address_default(
request: Request,
address_id: int = Path(..., description="Address ID", gt=0),
customer: Customer = Depends(get_current_customer_api),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""

View File

@@ -9,9 +9,11 @@ Usage:
CustomerRegister,
CustomerUpdate,
CustomerResponse,
CustomerContext,
)
"""
from app.modules.customers.schemas.context import CustomerContext
from app.modules.customers.schemas.customer import (
# Registration & Authentication
CustomerRegister,
@@ -41,6 +43,8 @@ from app.modules.customers.schemas.customer import (
)
__all__ = [
# Context (for dependency injection)
"CustomerContext",
# Registration & Authentication
"CustomerRegister",
"CustomerUpdate",

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,
)