Files
orion/app/api/v1/admin/order_item_exceptions.py
Samir Boulahtit cad862f469 refactor(api): introduce UserContext schema for API dependency injection
Replace direct User database model imports in API endpoints with UserContext
schema, following the architecture principle that API routes should not import
database models directly.

Changes:
- Create UserContext schema in models/schema/auth.py with from_user() factory
- Update app/api/deps.py to return UserContext from all auth dependencies
- Add _get_user_model() helper for functions needing User model access
- Update 58 API endpoint files to use UserContext instead of User
- Add noqa comments for 4 legitimate edge cases (enums, internal helpers)

Architecture validation: 0 errors (down from 61), 11 warnings remain

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:47:33 +01:00

253 lines
7.9 KiB
Python

# app/api/v1/admin/order_item_exceptions.py
"""
Admin API endpoints for order item exception management.
Provides admin-level management of:
- Listing exceptions across all vendors
- Resolving exceptions by assigning products
- Bulk resolution by GTIN
- Exception statistics
"""
import logging
from fastapi import APIRouter, Depends, Path, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.core.database import get_db
from app.services.order_item_exception_service import order_item_exception_service
from models.schema.auth import UserContext
from app.modules.orders.schemas import (
BulkResolveRequest,
BulkResolveResponse,
IgnoreExceptionRequest,
OrderItemExceptionListResponse,
OrderItemExceptionResponse,
OrderItemExceptionStats,
ResolveExceptionRequest,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/order-exceptions", tags=["Order Item Exceptions"])
# ============================================================================
# Exception Listing and Stats
# ============================================================================
@router.get("", response_model=OrderItemExceptionListResponse)
def list_exceptions(
vendor_id: int | None = Query(None, description="Filter by vendor"),
status: str | None = Query(
None,
pattern="^(pending|resolved|ignored)$",
description="Filter by status"
),
search: str | None = Query(
None,
description="Search in GTIN, product name, SKU, or order number"
),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
List order item exceptions with filtering and pagination.
Returns exceptions for unmatched products during marketplace order imports.
"""
exceptions, total = order_item_exception_service.get_pending_exceptions(
db=db,
vendor_id=vendor_id,
status=status,
search=search,
skip=skip,
limit=limit,
)
# Enrich with order and vendor info
response_items = []
for exc in exceptions:
item = OrderItemExceptionResponse.model_validate(exc)
if exc.order_item and exc.order_item.order:
order = exc.order_item.order
item.order_number = order.order_number
item.order_id = order.id
item.order_date = order.order_date
item.order_status = order.status
# Add vendor name for cross-vendor view
if order.vendor:
item.vendor_name = order.vendor.name
response_items.append(item)
return OrderItemExceptionListResponse(
exceptions=response_items,
total=total,
skip=skip,
limit=limit,
)
@router.get("/stats", response_model=OrderItemExceptionStats)
def get_exception_stats(
vendor_id: int | None = Query(None, description="Filter by vendor"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get exception statistics.
Returns counts of pending, resolved, and ignored exceptions.
"""
stats = order_item_exception_service.get_exception_stats(db, vendor_id)
return OrderItemExceptionStats(**stats)
# ============================================================================
# Exception Details
# ============================================================================
@router.get("/{exception_id}", response_model=OrderItemExceptionResponse)
def get_exception(
exception_id: int = Path(..., description="Exception ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Get details of a single exception.
"""
exception = order_item_exception_service.get_exception_by_id(db, exception_id)
response = OrderItemExceptionResponse.model_validate(exception)
if exception.order_item and exception.order_item.order:
order = exception.order_item.order
response.order_number = order.order_number
response.order_id = order.id
response.order_date = order.order_date
response.order_status = order.status
return response
# ============================================================================
# Exception Resolution
# ============================================================================
@router.post("/{exception_id}/resolve", response_model=OrderItemExceptionResponse)
def resolve_exception(
exception_id: int = Path(..., description="Exception ID"),
request: ResolveExceptionRequest = ...,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Resolve an exception by assigning a product.
This updates the order item's product_id and marks the exception as resolved.
"""
exception = order_item_exception_service.resolve_exception(
db=db,
exception_id=exception_id,
product_id=request.product_id,
resolved_by=current_admin.id,
notes=request.notes,
)
db.commit()
response = OrderItemExceptionResponse.model_validate(exception)
if exception.order_item and exception.order_item.order:
order = exception.order_item.order
response.order_number = order.order_number
response.order_id = order.id
response.order_date = order.order_date
response.order_status = order.status
logger.info(
f"Admin {current_admin.id} resolved exception {exception_id} "
f"with product {request.product_id}"
)
return response
@router.post("/{exception_id}/ignore", response_model=OrderItemExceptionResponse)
def ignore_exception(
exception_id: int = Path(..., description="Exception ID"),
request: IgnoreExceptionRequest = ...,
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Mark an exception as ignored.
Note: Ignored exceptions still block order confirmation.
Use this when a product will never be matched (e.g., discontinued).
"""
exception = order_item_exception_service.ignore_exception(
db=db,
exception_id=exception_id,
resolved_by=current_admin.id,
notes=request.notes,
)
db.commit()
response = OrderItemExceptionResponse.model_validate(exception)
if exception.order_item and exception.order_item.order:
order = exception.order_item.order
response.order_number = order.order_number
response.order_id = order.id
response.order_date = order.order_date
response.order_status = order.status
logger.info(
f"Admin {current_admin.id} ignored exception {exception_id}: {request.notes}"
)
return response
# ============================================================================
# Bulk Operations
# ============================================================================
@router.post("/bulk-resolve", response_model=BulkResolveResponse)
def bulk_resolve_by_gtin(
request: BulkResolveRequest,
vendor_id: int = Query(..., description="Vendor ID"),
db: Session = Depends(get_db),
current_admin: UserContext = Depends(get_current_admin_api),
):
"""
Bulk resolve all pending exceptions for a GTIN.
Useful when a new product is imported and multiple orders have
items with the same unmatched GTIN.
"""
resolved_count = order_item_exception_service.bulk_resolve_by_gtin(
db=db,
vendor_id=vendor_id,
gtin=request.gtin,
product_id=request.product_id,
resolved_by=current_admin.id,
notes=request.notes,
)
db.commit()
logger.info(
f"Admin {current_admin.id} bulk resolved {resolved_count} exceptions "
f"for GTIN {request.gtin} with product {request.product_id}"
)
return BulkResolveResponse(
resolved_count=resolved_count,
gtin=request.gtin,
product_id=request.product_id,
)