# app/modules/orders/routes/api/store.py """ Store order management endpoints. Store Context: Uses token_store_id from JWT token (authenticated store API pattern). The get_current_store_api dependency guarantees token_store_id is present. This router aggregates both order routes and exception routes. """ 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_store_api, require_module_access from app.core.database import get_db from app.modules.enums import FrontendType from app.modules.orders.schemas import ( OrderDetailResponse, OrderListResponse, OrderResponse, OrderUpdate, ) from app.modules.orders.services.order_inventory_service import order_inventory_service from app.modules.orders.services.order_service import order_service from app.modules.tenancy.schemas.auth import UserContext # Base router for orders _orders_router = APIRouter( prefix="/orders", dependencies=[Depends(require_module_access("orders", FrontendType.STORE))], ) # Aggregate router that includes both orders and exceptions router = APIRouter() logger = logging.getLogger(__name__) @_orders_router.get("", response_model=OrderListResponse) def get_store_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_store_api), db: Session = Depends(get_db), ): """ Get all orders for store. Supports filtering by: - status: Order status (pending, processing, shipped, delivered, cancelled) - customer_id: Filter orders from specific customer Store is determined from JWT token (store_id claim). Requires Authorization header (API endpoint). """ orders, total = order_service.get_store_orders( db=db, store_id=current_user.token_store_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, ) @_orders_router.get("/{order_id}", response_model=OrderDetailResponse) def get_order_details( order_id: int, current_user: UserContext = Depends(get_current_store_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, store_id=current_user.token_store_id, order_id=order_id ) return OrderDetailResponse.model_validate(order) @_orders_router.put("/{order_id}/status", response_model=OrderResponse) def update_order_status( order_id: int, order_update: OrderUpdate, current_user: UserContext = Depends(get_current_store_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, store_id=current_user.token_store_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] @_orders_router.get("/{order_id}/shipment-status", response_model=ShipmentStatusResponse) def get_shipment_status( order_id: int, current_user: UserContext = Depends(get_current_store_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, store_id=current_user.token_store_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"]], ) @_orders_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_store_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, store_id=current_user.token_store_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_store_id, order_id) if order.is_fully_shipped and order.status != "shipped": order_service.update_order_status( db=db, store_id=current_user.token_store_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, store_id=current_user.token_store_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) # ============================================================================ # Aggregate routers # ============================================================================ # Import sub-routers from app.modules.orders.routes.api.store_customer_orders import ( store_customer_orders_router, ) from app.modules.orders.routes.api.store_exceptions import store_exceptions_router from app.modules.orders.routes.api.store_invoices import store_invoices_router # Include all sub-routers into the aggregate router router.include_router(_orders_router, tags=["store-orders"]) router.include_router(store_exceptions_router, tags=["store-order-exceptions"]) router.include_router(store_invoices_router, tags=["store-invoices"]) router.include_router(store_customer_orders_router, tags=["store-customer-orders"])