# ================================================================ # services/orders/app/api/orders.py # ================================================================ """ Orders API endpoints - ATOMIC CRUD operations only """ from datetime import date from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Path, Query, status import structlog from shared.auth.decorators import get_current_user_dep from shared.auth.access_control import require_user_role from shared.routing import RouteBuilder from shared.security import create_audit_logger, AuditSeverity, AuditAction from app.core.database import get_db from app.services.orders_service import OrdersService from app.schemas.order_schemas import ( OrderCreate, OrderUpdate, OrderResponse ) logger = structlog.get_logger() audit_logger = create_audit_logger("orders-service") # Create route builder for consistent URL structure route_builder = RouteBuilder('orders') router = APIRouter() # ===== Dependency Injection ===== async def get_orders_service(db = Depends(get_db)) -> OrdersService: """Get orders service with dependencies""" from app.repositories.order_repository import ( OrderRepository, CustomerRepository, OrderItemRepository, OrderStatusHistoryRepository ) from shared.clients import ( get_inventory_client, get_production_client, get_sales_client ) return OrdersService( order_repo=OrderRepository(), customer_repo=CustomerRepository(), order_item_repo=OrderItemRepository(), status_history_repo=OrderStatusHistoryRepository(), inventory_client=get_inventory_client(), production_client=get_production_client(), sales_client=get_sales_client() ) # ===== Order CRUD Endpoints ===== @router.post( route_builder.build_base_route("").rstrip("/"), response_model=OrderResponse, status_code=status.HTTP_201_CREATED ) @require_user_role(['admin', 'owner', 'member']) async def create_order( order_data: OrderCreate, tenant_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), orders_service: OrdersService = Depends(get_orders_service), db = Depends(get_db) ): """Create a new customer order""" try: # Extract user ID safely user_id = current_user.get("user_id") if not user_id: logger.error("User ID not found in current_user context", current_user_keys=list(current_user.keys())) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="User authentication error" ) order = await orders_service.create_order( db, order_data, user_id=UUID(user_id) ) # Commit the transaction to persist changes await db.commit() logger.info("Order created successfully", order_id=str(order.id), order_number=order.order_number) return order except ValueError as e: logger.warning("Invalid order data", error=str(e)) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) ) except Exception as e: logger.error("Error creating order", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create order" ) @router.get( route_builder.build_base_route("").rstrip("/"), response_model=List[OrderResponse] ) async def get_orders( tenant_id: UUID = Path(...), status_filter: Optional[str] = Query(None, description="Filter by order status"), start_date: Optional[date] = Query(None, description="Start date for date range filter"), end_date: Optional[date] = Query(None, description="End date for date range filter"), skip: int = Query(0, ge=0, description="Number of orders to skip"), limit: int = Query(100, ge=1, le=1000, description="Number of orders to return"), current_user: dict = Depends(get_current_user_dep), orders_service: OrdersService = Depends(get_orders_service), db = Depends(get_db) ): """Get orders with filtering and pagination""" try: # Determine which repository method to use based on filters if status_filter: orders = await orders_service.order_repo.get_orders_by_status( db, tenant_id, status_filter, skip, limit ) elif start_date and end_date: orders = await orders_service.order_repo.get_orders_by_date_range( db, tenant_id, start_date, end_date, skip, limit ) else: orders = await orders_service.order_repo.get_multi( db, tenant_id, skip, limit, order_by="order_date", order_desc=True ) return [OrderResponse.from_orm(order) for order in orders] except Exception as e: logger.error("Error getting orders", error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve orders" ) @router.get( route_builder.build_base_route("{order_id}"), response_model=OrderResponse ) async def get_order( tenant_id: UUID = Path(...), order_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), orders_service: OrdersService = Depends(get_orders_service), db = Depends(get_db) ): """Get order details with items""" try: order = await orders_service.get_order_with_items(db, order_id, tenant_id) if not order: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Order not found" ) return order except HTTPException: raise except Exception as e: logger.error("Error getting order", order_id=str(order_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve order" ) @router.put( route_builder.build_base_route("{order_id}"), response_model=OrderResponse ) @require_user_role(['admin', 'owner', 'member']) async def update_order( order_data: OrderUpdate, tenant_id: UUID = Path(...), order_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), orders_service: OrdersService = Depends(get_orders_service), db = Depends(get_db) ): """Update order information""" try: # Get existing order order = await orders_service.order_repo.get(db, order_id, tenant_id) if not order: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Order not found" ) # Update order updated_order = await orders_service.order_repo.update( db, db_obj=order, obj_in=order_data.dict(exclude_unset=True), updated_by=UUID(current_user["sub"]) ) # Commit the transaction to persist changes await db.commit() logger.info("Order updated successfully", order_id=str(order_id)) return OrderResponse.from_orm(updated_order) except HTTPException: raise except Exception as e: logger.error("Error updating order", order_id=str(order_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update order" ) @router.delete( route_builder.build_base_route("{order_id}"), status_code=status.HTTP_204_NO_CONTENT ) @require_user_role(['admin', 'owner']) async def delete_order( tenant_id: UUID = Path(...), order_id: UUID = Path(...), current_user: dict = Depends(get_current_user_dep), orders_service: OrdersService = Depends(get_orders_service), db = Depends(get_db) ): """Delete an order (Admin+ only, soft delete)""" try: order = await orders_service.order_repo.get(db, order_id, tenant_id) if not order: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Order not found" ) # Capture order data before deletion order_data = { "order_number": order.order_number, "customer_id": str(order.customer_id) if order.customer_id else None, "order_status": order.order_status, "total_amount": float(order.total_amount) if order.total_amount else 0.0, "order_date": order.order_date.isoformat() if order.order_date else None } await orders_service.order_repo.delete(db, order_id, tenant_id) # Commit the transaction to persist deletion await db.commit() # Log audit event for order deletion try: await audit_logger.log_deletion( db_session=db, tenant_id=str(tenant_id), user_id=current_user["user_id"], resource_type="order", resource_id=str(order_id), resource_data=order_data, description=f"Admin {current_user.get('email', 'unknown')} deleted order {order_data['order_number']}", endpoint=f"/orders/{order_id}", method="DELETE" ) except Exception as audit_error: logger.warning("Failed to log audit event", error=str(audit_error)) logger.info("Order deleted successfully", order_id=str(order_id), tenant_id=str(tenant_id), user_id=current_user["user_id"]) except HTTPException: raise except Exception as e: logger.error("Error deleting order", order_id=str(order_id), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete order" )