Files
bakery-ia/services/orders/app/api/orders.py

406 lines
13 KiB
Python
Raw Permalink Normal View History

2025-08-21 20:28:14 +02:00
# ================================================================
# services/orders/app/api/orders.py
# ================================================================
"""
2025-10-06 15:27:01 +02:00
Orders API endpoints - ATOMIC CRUD operations only
2025-08-21 20:28:14 +02:00
"""
2025-10-06 15:27:01 +02:00
from datetime import date
2025-08-21 20:28:14 +02:00
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
2025-10-31 11:54:19 +01:00
from sqlalchemy.ext.asyncio import AsyncSession
2025-08-21 20:28:14 +02:00
import structlog
from shared.auth.decorators import get_current_user_dep
2025-10-06 15:27:01 +02:00
from shared.auth.access_control import require_user_role
from shared.routing import RouteBuilder
from shared.security import create_audit_logger, AuditSeverity, AuditAction
2025-08-21 20:28:14 +02:00
from app.core.database import get_db
from app.services.orders_service import OrdersService
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-08-21 20:28:14 +02:00
from app.schemas.order_schemas import (
OrderCreate,
OrderUpdate,
2025-10-06 15:27:01 +02:00
OrderResponse
2025-08-21 20:28:14 +02:00
)
logger = structlog.get_logger()
2025-10-29 06:58:05 +01:00
audit_logger = create_audit_logger("orders-service", AuditLog)
2025-08-21 20:28:14 +02:00
2025-10-06 15:27:01 +02:00
# Create route builder for consistent URL structure
route_builder = RouteBuilder('orders')
2025-08-21 20:28:14 +02:00
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 (
2025-08-23 16:30:45 +02:00
get_inventory_client,
get_production_client,
get_sales_client
2025-08-21 20:28:14 +02:00
)
2025-10-06 15:27:01 +02:00
2025-08-21 20:28:14 +02:00
return OrdersService(
order_repo=OrderRepository(),
customer_repo=CustomerRepository(),
order_item_repo=OrderItemRepository(),
status_history_repo=OrderStatusHistoryRepository(),
2025-08-23 16:30:45 +02:00
inventory_client=get_inventory_client(),
production_client=get_production_client(),
2025-09-19 11:44:38 +02:00
sales_client=get_sales_client()
2025-08-21 20:28:14 +02:00
)
2025-10-06 15:27:01 +02:00
# ===== Order CRUD Endpoints =====
2025-08-21 20:28:14 +02:00
2025-10-06 15:27:01 +02:00
@router.post(
2025-10-21 19:50:07 +02:00
route_builder.build_base_route("").rstrip("/"),
2025-10-06 15:27:01 +02:00
response_model=OrderResponse,
status_code=status.HTTP_201_CREATED
)
@require_user_role(['admin', 'owner', 'member'])
2025-08-21 20:28:14 +02:00
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:
2025-10-27 16:33:26 +01:00
# 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"
)
2025-08-21 20:28:14 +02:00
order = await orders_service.create_order(
db,
order_data,
2025-10-27 16:33:26 +01:00
user_id=UUID(user_id)
2025-08-21 20:28:14 +02:00
)
2025-10-27 16:33:26 +01:00
# Commit the transaction to persist changes
await db.commit()
logger.info("Order created successfully",
2025-08-21 20:28:14 +02:00
order_id=str(order.id),
order_number=order.order_number)
2025-08-21 20:28:14 +02:00
return order
2025-08-21 20:28:14 +02:00
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"
)
2025-10-06 15:27:01 +02:00
@router.get(
2025-10-21 19:50:07 +02:00
route_builder.build_base_route("").rstrip("/"),
2025-10-06 15:27:01 +02:00
response_model=List[OrderResponse]
)
2025-08-21 20:28:14 +02:00
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
)
2025-10-06 15:27:01 +02:00
2025-08-21 20:28:14 +02:00
return [OrderResponse.from_orm(order) for order in orders]
2025-10-06 15:27:01 +02:00
2025-08-21 20:28:14 +02:00
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"
)
2025-10-06 15:27:01 +02:00
@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,
2025-08-21 20:28:14 +02:00
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)
):
2025-10-06 15:27:01 +02:00
"""Update order information"""
2025-08-21 20:28:14 +02:00
try:
2025-10-06 15:27:01 +02:00
# Get existing order
order = await orders_service.order_repo.get(db, order_id, tenant_id)
2025-08-21 20:28:14 +02:00
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found"
)
2025-10-06 15:27:01 +02:00
# Update order
updated_order = await orders_service.order_repo.update(
db,
db_obj=order,
obj_in=order_data.dict(exclude_unset=True),
2025-10-29 06:58:05 +01:00
updated_by=UUID(current_user["user_id"])
2025-08-21 20:28:14 +02:00
)
2025-10-27 16:33:26 +01:00
# Commit the transaction to persist changes
await db.commit()
2025-10-06 15:27:01 +02:00
logger.info("Order updated successfully",
order_id=str(order_id))
2025-08-21 20:28:14 +02:00
2025-10-06 15:27:01 +02:00
return OrderResponse.from_orm(updated_order)
2025-08-21 20:28:14 +02:00
except HTTPException:
raise
except Exception as e:
2025-10-06 15:27:01 +02:00
logger.error("Error updating order",
order_id=str(order_id),
error=str(e))
2025-08-21 20:28:14 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-10-06 15:27:01 +02:00
detail="Failed to update order"
2025-08-21 20:28:14 +02:00
)
2025-10-06 15:27:01 +02:00
@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(
2025-08-21 20:28:14 +02:00
tenant_id: UUID = Path(...),
2025-10-06 15:27:01 +02:00
order_id: UUID = Path(...),
2025-08-21 20:28:14 +02:00
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)"""
2025-08-21 20:28:14 +02:00
try:
2025-10-06 15:27:01 +02:00
order = await orders_service.order_repo.get(db, order_id, tenant_id)
if not order:
2025-08-21 20:28:14 +02:00
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
2025-10-06 15:27:01 +02:00
detail="Order not found"
2025-08-21 20:28:14 +02:00
)
# 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
}
2025-10-06 15:27:01 +02:00
await orders_service.order_repo.delete(db, order_id, tenant_id)
2025-08-21 20:28:14 +02:00
2025-10-27 16:33:26 +01:00
# 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))
2025-10-06 15:27:01 +02:00
logger.info("Order deleted successfully",
order_id=str(order_id),
tenant_id=str(tenant_id),
user_id=current_user["user_id"])
2025-08-21 20:28:14 +02:00
2025-10-06 15:27:01 +02:00
except HTTPException:
raise
2025-08-21 20:28:14 +02:00
except Exception as e:
2025-10-06 15:27:01 +02:00
logger.error("Error deleting order",
order_id=str(order_id),
error=str(e))
2025-08-21 20:28:14 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-10-06 15:27:01 +02:00
detail="Failed to delete order"
2025-10-27 16:33:26 +01:00
)
2025-10-31 11:54:19 +01:00
# ===== Tenant Data Deletion Endpoint =====
@router.delete(
route_builder.build_base_route("tenant/{tenant_id}", include_tenant_prefix=False),
status_code=status.HTTP_200_OK
)
async def delete_tenant_data(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Delete all order-related data for a tenant
Only accessible by internal services (called during tenant deletion)
"""
logger.info("Tenant data deletion request received",
tenant_id=tenant_id,
requesting_service=current_user.get("service", "unknown"))
# Only allow internal service calls
if current_user.get("type") != "service":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This endpoint is only accessible to internal services"
)
try:
from app.services.tenant_deletion_service import OrdersTenantDeletionService
deletion_service = OrdersTenantDeletionService(db)
result = await deletion_service.safe_delete_tenant_data(tenant_id)
return {
"message": "Tenant data deletion completed in orders-service",
"summary": result.to_dict()
}
except Exception as e:
logger.error("Tenant data deletion failed",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete tenant data: {str(e)}"
)
@router.get(
route_builder.build_base_route("tenant/{tenant_id}/deletion-preview", include_tenant_prefix=False),
status_code=status.HTTP_200_OK
)
async def preview_tenant_data_deletion(
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""
Preview what data would be deleted for a tenant (dry-run)
Accessible by internal services and tenant admins
"""
# Allow internal services and admins
is_service = current_user.get("type") == "service"
is_admin = current_user.get("role") in ["owner", "admin"]
if not (is_service or is_admin):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions"
)
try:
from app.services.tenant_deletion_service import OrdersTenantDeletionService
deletion_service = OrdersTenantDeletionService(db)
preview = await deletion_service.get_tenant_data_preview(tenant_id)
return {
"tenant_id": tenant_id,
"service": "orders-service",
"data_counts": preview,
"total_items": sum(preview.values())
}
except Exception as e:
logger.error("Deletion preview failed",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get deletion preview: {str(e)}"
)