Files
orion/app/modules/orders/routes/api/storefront.py
Samir Boulahtit 4b8e1b1d88 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>
2026-01-29 23:06:21 +01:00

220 lines
6.4 KiB
Python

# app/modules/orders/routes/api/storefront.py
"""
Orders Module - Storefront API Routes
Authenticated endpoints for customer order operations:
- View order history
- View order details
- Download invoices
Uses vendor from middleware context (VendorContextMiddleware).
Requires customer authentication.
"""
import logging
from pathlib import Path as FilePath
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_customer_api
from app.core.database import get_db
from app.exceptions import OrderNotFoundException, VendorNotFoundException
from app.exceptions.invoice import InvoicePDFNotFoundException
from app.modules.customers.schemas import CustomerContext
from app.modules.orders.services import order_service
from app.services.invoice_service import invoice_service
from models.schema.order import (
OrderDetailResponse,
OrderListResponse,
OrderResponse,
)
router = APIRouter()
logger = logging.getLogger(__name__)
@router.get("/orders", response_model=OrderListResponse) # authenticated
def get_my_orders(
request: Request,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
Get order history for authenticated customer.
Vendor is automatically determined from request context.
Returns all orders placed by the authenticated customer.
Query Parameters:
- skip: Number of orders to skip (pagination)
- limit: Maximum number of orders to return
"""
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[ORDERS_STOREFRONT] get_my_orders for customer {customer.id}",
extra={
"vendor_id": vendor.id,
"vendor_code": vendor.subdomain,
"customer_id": customer.id,
"skip": skip,
"limit": limit,
},
)
orders, total = order_service.get_customer_orders(
db=db, vendor_id=vendor.id, customer_id=customer.id, skip=skip, limit=limit
)
return OrderListResponse(
orders=[OrderResponse.model_validate(o) for o in orders],
total=total,
skip=skip,
limit=limit,
)
@router.get("/orders/{order_id}", response_model=OrderDetailResponse)
def get_order_details(
request: Request,
order_id: int = Path(..., description="Order ID", gt=0),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
Get detailed order information for authenticated customer.
Vendor is automatically determined from request context.
Customer can only view their own orders.
Path Parameters:
- order_id: ID of the order to retrieve
"""
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[ORDERS_STOREFRONT] get_order_details: order {order_id}",
extra={
"vendor_id": vendor.id,
"vendor_code": vendor.subdomain,
"customer_id": customer.id,
"order_id": order_id,
},
)
order = order_service.get_order(db=db, vendor_id=vendor.id, order_id=order_id)
# Verify order belongs to customer
if order.customer_id != customer.id:
raise OrderNotFoundException(str(order_id))
return OrderDetailResponse.model_validate(order)
@router.get("/orders/{order_id}/invoice")
def download_order_invoice(
request: Request,
order_id: int = Path(..., description="Order ID", gt=0),
customer: CustomerContext = Depends(get_current_customer_api),
db: Session = Depends(get_db),
):
"""
Download invoice PDF for a customer's order.
Vendor is automatically determined from request context.
Customer can only download invoices for their own orders.
Invoice is auto-generated if it doesn't exist.
Path Parameters:
- order_id: ID of the order to get invoice for
"""
from app.exceptions import ValidationException
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorNotFoundException("context", identifier_type="subdomain")
logger.debug(
f"[ORDERS_STOREFRONT] download_order_invoice: order {order_id}",
extra={
"vendor_id": vendor.id,
"vendor_code": vendor.subdomain,
"customer_id": customer.id,
"order_id": order_id,
},
)
order = order_service.get_order(db=db, vendor_id=vendor.id, order_id=order_id)
# Verify order belongs to customer
if order.customer_id != customer.id:
raise OrderNotFoundException(str(order_id))
# Only allow invoice download for orders that are at least processing
allowed_statuses = [
"processing",
"partially_shipped",
"shipped",
"delivered",
"completed",
]
if order.status not in allowed_statuses:
raise ValidationException("Invoice not available for pending orders")
# Check if invoice exists for this order
invoice = invoice_service.get_invoice_by_order_id(
db=db, vendor_id=vendor.id, order_id=order_id
)
# Create invoice if it doesn't exist
if not invoice:
logger.info(f"Creating invoice for order {order_id} (customer download)")
invoice = invoice_service.create_invoice_from_order(
db=db,
vendor_id=vendor.id,
order_id=order_id,
)
db.commit()
# Get or generate PDF
pdf_path = invoice_service.get_pdf_path(
db=db,
vendor_id=vendor.id,
invoice_id=invoice.id,
)
if not pdf_path:
pdf_path = invoice_service.generate_pdf(
db=db,
vendor_id=vendor.id,
invoice_id=invoice.id,
)
# Verify file exists
if not FilePath(pdf_path).exists():
raise InvoicePDFNotFoundException(invoice.id)
filename = f"invoice-{invoice.invoice_number}.pdf"
logger.info(
f"Customer {customer.id} downloading invoice {invoice.invoice_number} for order {order.order_number}"
)
return FileResponse(
path=pdf_path,
media_type="application/pdf",
filename=filename,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)