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