Files
orion/app/api/v1/vendor/orders.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

270 lines
7.9 KiB
Python

# app/api/v1/vendor/orders.py
"""
Vendor order management endpoints.
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The get_current_vendor_api dependency guarantees token_vendor_id is present.
"""
import logging
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api
from app.core.database import get_db
from app.services.order_inventory_service import order_inventory_service
from app.services.order_service import order_service
from models.schema.auth import UserContext
from app.modules.orders.schemas import (
OrderDetailResponse,
OrderListResponse,
OrderResponse,
OrderUpdate,
)
router = APIRouter(prefix="/orders")
logger = logging.getLogger(__name__)
@router.get("", response_model=OrderListResponse)
def get_vendor_orders(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
status: str | None = Query(None, description="Filter by order status"),
customer_id: int | None = Query(None, description="Filter by customer"),
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get all orders for vendor.
Supports filtering by:
- status: Order status (pending, processing, shipped, delivered, cancelled)
- customer_id: Filter orders from specific customer
Vendor is determined from JWT token (vendor_id claim).
Requires Authorization header (API endpoint).
"""
orders, total = order_service.get_vendor_orders(
db=db,
vendor_id=current_user.token_vendor_id,
skip=skip,
limit=limit,
status=status,
customer_id=customer_id,
)
return OrderListResponse(
orders=[OrderResponse.model_validate(o) for o in orders],
total=total,
skip=skip,
limit=limit,
)
@router.get("/{order_id}", response_model=OrderDetailResponse)
def get_order_details(
order_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get detailed order information including items and addresses.
Requires Authorization header (API endpoint).
"""
order = order_service.get_order(
db=db, vendor_id=current_user.token_vendor_id, order_id=order_id
)
return OrderDetailResponse.model_validate(order)
@router.put("/{order_id}/status", response_model=OrderResponse)
def update_order_status(
order_id: int,
order_update: OrderUpdate,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Update order status and tracking information.
Valid statuses:
- pending: Order placed, awaiting processing
- processing: Order being prepared
- shipped: Order shipped to customer
- delivered: Order delivered
- cancelled: Order cancelled
- refunded: Order refunded
Requires Authorization header (API endpoint).
"""
order = order_service.update_order_status(
db=db,
vendor_id=current_user.token_vendor_id,
order_id=order_id,
order_update=order_update,
)
db.commit()
logger.info(
f"Order {order.order_number} status updated to {order.status} "
f"by user {current_user.username}"
)
return OrderResponse.model_validate(order)
# ============================================================================
# Partial Shipment Endpoints
# ============================================================================
class ShipItemRequest(BaseModel):
"""Request to ship specific quantity of an order item."""
quantity: int | None = Field(
None, ge=1, description="Quantity to ship (default: remaining quantity)"
)
class ShipItemResponse(BaseModel):
"""Response from shipping an item."""
order_id: int
item_id: int
fulfilled_quantity: int
shipped_quantity: int | None = None
remaining_quantity: int | None = None
is_fully_shipped: bool | None = None
message: str | None = None
class ShipmentStatusItemResponse(BaseModel):
"""Item-level shipment status."""
item_id: int
product_id: int
product_name: str
quantity: int
shipped_quantity: int
remaining_quantity: int
is_fully_shipped: bool
is_partially_shipped: bool
class ShipmentStatusResponse(BaseModel):
"""Order shipment status response."""
order_id: int
order_number: str
order_status: str
is_fully_shipped: bool
is_partially_shipped: bool
shipped_item_count: int
total_item_count: int
total_shipped_units: int
total_ordered_units: int
items: list[ShipmentStatusItemResponse]
@router.get("/{order_id}/shipment-status", response_model=ShipmentStatusResponse)
def get_shipment_status(
order_id: int,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Get detailed shipment status for an order.
Returns item-level shipment status showing what has been shipped
and what remains. Useful for partial shipment tracking.
Requires Authorization header (API endpoint).
"""
result = order_inventory_service.get_shipment_status(
db=db,
vendor_id=current_user.token_vendor_id,
order_id=order_id,
)
return ShipmentStatusResponse(
order_id=result["order_id"],
order_number=result["order_number"],
order_status=result["order_status"],
is_fully_shipped=result["is_fully_shipped"],
is_partially_shipped=result["is_partially_shipped"],
shipped_item_count=result["shipped_item_count"],
total_item_count=result["total_item_count"],
total_shipped_units=result["total_shipped_units"],
total_ordered_units=result["total_ordered_units"],
items=[ShipmentStatusItemResponse(**item) for item in result["items"]],
)
@router.post("/{order_id}/items/{item_id}/ship", response_model=ShipItemResponse)
def ship_order_item(
order_id: int,
item_id: int,
request: ShipItemRequest | None = None,
current_user: UserContext = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
):
"""
Ship a specific order item (supports partial shipment).
Fulfills inventory and updates the item's shipped quantity.
If quantity is not specified, ships the remaining quantity.
Example use cases:
- Ship all of an item: POST /orders/{id}/items/{item_id}/ship
- Ship partial: POST /orders/{id}/items/{item_id}/ship with {"quantity": 2}
Requires Authorization header (API endpoint).
"""
quantity = request.quantity if request else None
result = order_inventory_service.fulfill_item(
db=db,
vendor_id=current_user.token_vendor_id,
order_id=order_id,
item_id=item_id,
quantity=quantity,
skip_missing=True,
)
# Update order status based on shipment state
order = order_service.get_order(db, current_user.token_vendor_id, order_id)
if order.is_fully_shipped and order.status != "shipped":
order_service.update_order_status(
db=db,
vendor_id=current_user.token_vendor_id,
order_id=order_id,
order_update=OrderUpdate(status="shipped"),
)
logger.info(f"Order {order.order_number} fully shipped")
elif order.is_partially_shipped and order.status not in (
"partially_shipped",
"shipped",
):
order_service.update_order_status(
db=db,
vendor_id=current_user.token_vendor_id,
order_id=order_id,
order_update=OrderUpdate(status="partially_shipped"),
)
logger.info(f"Order {order.order_number} partially shipped")
db.commit()
logger.info(
f"Shipped item {item_id} of order {order_id}: "
f"{result.get('fulfilled_quantity', 0)} units"
)
return ShipItemResponse(**result)