293 lines
9.5 KiB
Python
293 lines
9.5 KiB
Python
# ================================================================
|
|
# 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("orders"),
|
|
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:
|
|
# Ensure tenant_id matches
|
|
order_data.tenant_id = tenant_id
|
|
|
|
order = await orders_service.create_order(
|
|
db,
|
|
order_data,
|
|
user_id=UUID(current_user["sub"])
|
|
)
|
|
|
|
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("orders"),
|
|
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"])
|
|
)
|
|
|
|
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)
|
|
|
|
# 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"
|
|
) |