Files
bakery-ia/services/procurement/app/api/purchase_orders.py

603 lines
19 KiB
Python
Raw Normal View History

2025-10-30 21:08:07 +01:00
# ================================================================
# services/procurement/app/api/purchase_orders.py
# ================================================================
"""
Purchase Orders API - Endpoints for purchase order management
"""
import uuid
from typing import List, Optional
2025-11-05 13:34:56 +01:00
from fastapi import APIRouter, Depends, HTTPException, Path, Query
2025-10-30 21:08:07 +01:00
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.config import settings
from app.services.purchase_order_service import PurchaseOrderService
from app.services.overdue_po_detector import OverduePODetector
2025-10-30 21:08:07 +01:00
from app.schemas.purchase_order_schemas import (
PurchaseOrderCreate,
PurchaseOrderUpdate,
PurchaseOrderResponse,
2025-10-31 18:57:58 +01:00
PurchaseOrderWithSupplierResponse,
2025-10-30 21:08:07 +01:00
PurchaseOrderApproval,
DeliveryCreate,
DeliveryResponse,
SupplierInvoiceCreate,
SupplierInvoiceResponse,
)
2025-11-05 13:34:56 +01:00
from shared.routing import RouteBuilder
from shared.auth.decorators import get_current_user_dep
Fix critical bugs and standardize service integrations Critical Fixes: - Orchestrator: Add missing OrchestrationStatus import (fixes HTTP 500 during demo clone) - Procurement: Migrate from custom cache utils to shared Redis utils - Suppliers: Use proper Settings for Redis configuration with TLS/auth - Recipes/Suppliers clients: Fix endpoint paths (remove duplicate path segments) - Procurement client: Use suppliers service directly for supplier details Details: 1. services/orchestrator/app/api/internal_demo.py: - Added OrchestrationStatus import to fix cloning error - This was causing HTTP 500 errors during demo session cloning 2. services/procurement/app/api/purchase_orders.py + service: - Replaced app.utils.cache with shared.redis_utils - Standardizes caching across all services - Removed custom cache utilities (deleted app/utils/cache.py) 3. services/suppliers/app/consumers/alert_event_consumer.py: - Use Settings().REDIS_URL instead of os.getenv - Ensures proper Redis connection with TLS and authentication 4. shared/clients/recipes_client.py: - Fixed endpoint paths: recipes/recipes/{id} → recipes/{id} - Applied to all recipe methods (by_id, by_products, instructions, yield) 5. shared/clients/suppliers_client.py: - Fixed endpoint path: suppliers/suppliers/{id} → suppliers/{id} 6. shared/clients/procurement_client.py: - get_supplier_by_id now uses SuppliersServiceClient directly - Removes incorrect call to procurement service for supplier details Impact: - Demo session cloning now works without orchestrator errors ✅ - Consistent Redis usage across all services - Correct service boundaries (suppliers data from suppliers service) - Clean client endpoint paths 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 11:33:22 +01:00
from shared.redis_utils import get_value, set_with_ttl
2025-10-30 21:08:07 +01:00
import structlog
logger = structlog.get_logger()
2025-11-05 13:34:56 +01:00
# Create route builder for consistent URL structure
route_builder = RouteBuilder('procurement')
router = APIRouter(tags=["purchase-orders"])
2025-10-30 21:08:07 +01:00
def get_po_service(db: AsyncSession = Depends(get_db)) -> PurchaseOrderService:
"""Dependency to get purchase order service"""
return PurchaseOrderService(db, settings)
# ================================================================
# PURCHASE ORDER CRUD
# ================================================================
2025-11-05 13:34:56 +01:00
@router.post(
route_builder.build_base_route("purchase-orders"),
response_model=PurchaseOrderResponse,
status_code=201
)
2025-10-30 21:08:07 +01:00
async def create_purchase_order(
po_data: PurchaseOrderCreate,
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Create a new purchase order with items
Creates a PO with automatic approval rules evaluation.
Links to procurement plan if procurement_plan_id is provided.
Args:
tenant_id: Tenant UUID
po_data: Purchase order creation data
Returns:
PurchaseOrderResponse with created PO details
"""
try:
logger.info("Create PO endpoint called", tenant_id=tenant_id)
po = await service.create_purchase_order(
tenant_id=uuid.UUID(tenant_id),
po_data=po_data
)
return PurchaseOrderResponse.model_validate(po)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating purchase order", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=str(e))
2025-11-05 13:34:56 +01:00
@router.get(
route_builder.build_resource_detail_route("purchase-orders", "po_id"),
response_model=PurchaseOrderWithSupplierResponse
)
2025-10-30 21:08:07 +01:00
async def get_purchase_order(
po_id: str,
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""Get purchase order by ID with items"""
try:
po = await service.get_purchase_order(
tenant_id=uuid.UUID(tenant_id),
po_id=uuid.UUID(po_id)
)
if not po:
raise HTTPException(status_code=404, detail="Purchase order not found")
2025-10-31 18:57:58 +01:00
return PurchaseOrderWithSupplierResponse.model_validate(po)
2025-10-30 21:08:07 +01:00
except HTTPException:
raise
except Exception as e:
logger.error("Error getting purchase order", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
2025-11-05 13:34:56 +01:00
@router.get(
route_builder.build_base_route("purchase-orders"),
response_model=List[PurchaseOrderResponse]
)
2025-10-30 21:08:07 +01:00
async def list_purchase_orders(
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
skip: int = Query(default=0, ge=0),
limit: int = Query(default=50, ge=1, le=100),
supplier_id: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
2025-12-05 20:07:01 +01:00
enrich_supplier: bool = Query(default=True, description="Include supplier details (slower)"),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
2025-12-05 20:07:01 +01:00
List purchase orders with filters and caching (30s TTL)
2025-10-30 21:08:07 +01:00
Args:
tenant_id: Tenant UUID
skip: Number of records to skip (pagination)
limit: Maximum number of records to return
supplier_id: Filter by supplier ID (optional)
status: Filter by status (optional)
2025-12-05 20:07:01 +01:00
enrich_supplier: Whether to enrich with supplier data (default: True)
2025-10-30 21:08:07 +01:00
Returns:
List of purchase orders
"""
try:
2025-12-05 20:07:01 +01:00
# PERFORMANCE OPTIMIZATION: Cache even with status filter for dashboard queries
# Only skip cache for supplier_id filter and pagination (skip > 0)
cache_key = None
if skip == 0 and supplier_id is None:
Fix critical bugs and standardize service integrations Critical Fixes: - Orchestrator: Add missing OrchestrationStatus import (fixes HTTP 500 during demo clone) - Procurement: Migrate from custom cache utils to shared Redis utils - Suppliers: Use proper Settings for Redis configuration with TLS/auth - Recipes/Suppliers clients: Fix endpoint paths (remove duplicate path segments) - Procurement client: Use suppliers service directly for supplier details Details: 1. services/orchestrator/app/api/internal_demo.py: - Added OrchestrationStatus import to fix cloning error - This was causing HTTP 500 errors during demo session cloning 2. services/procurement/app/api/purchase_orders.py + service: - Replaced app.utils.cache with shared.redis_utils - Standardizes caching across all services - Removed custom cache utilities (deleted app/utils/cache.py) 3. services/suppliers/app/consumers/alert_event_consumer.py: - Use Settings().REDIS_URL instead of os.getenv - Ensures proper Redis connection with TLS and authentication 4. shared/clients/recipes_client.py: - Fixed endpoint paths: recipes/recipes/{id} → recipes/{id} - Applied to all recipe methods (by_id, by_products, instructions, yield) 5. shared/clients/suppliers_client.py: - Fixed endpoint path: suppliers/suppliers/{id} → suppliers/{id} 6. shared/clients/procurement_client.py: - get_supplier_by_id now uses SuppliersServiceClient directly - Removes incorrect call to procurement service for supplier details Impact: - Demo session cloning now works without orchestrator errors ✅ - Consistent Redis usage across all services - Correct service boundaries (suppliers data from suppliers service) - Clean client endpoint paths 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 11:33:22 +01:00
cache_key = f"purchase_orders:{tenant_id}:limit:{limit}:status:{status}:enrich:{enrich_supplier}"
try:
cached_result = await get_value(cache_key)
if cached_result is not None:
logger.debug("Cache hit for purchase orders", cache_key=cache_key, tenant_id=tenant_id, status=status)
return [PurchaseOrderResponse(**po) for po in cached_result]
except Exception as e:
logger.warning("Cache read failed, continuing without cache", cache_key=cache_key, error=str(e))
2025-12-05 20:07:01 +01:00
# Cache miss - fetch from database
2025-10-30 21:08:07 +01:00
pos = await service.list_purchase_orders(
tenant_id=uuid.UUID(tenant_id),
skip=skip,
limit=limit,
supplier_id=uuid.UUID(supplier_id) if supplier_id else None,
2025-12-05 20:07:01 +01:00
status=status,
enrich_supplier=enrich_supplier
2025-10-30 21:08:07 +01:00
)
2025-12-05 20:07:01 +01:00
result = [PurchaseOrderResponse.model_validate(po) for po in pos]
# PERFORMANCE OPTIMIZATION: Cache the result (20s TTL for purchase orders)
if cache_key:
Fix critical bugs and standardize service integrations Critical Fixes: - Orchestrator: Add missing OrchestrationStatus import (fixes HTTP 500 during demo clone) - Procurement: Migrate from custom cache utils to shared Redis utils - Suppliers: Use proper Settings for Redis configuration with TLS/auth - Recipes/Suppliers clients: Fix endpoint paths (remove duplicate path segments) - Procurement client: Use suppliers service directly for supplier details Details: 1. services/orchestrator/app/api/internal_demo.py: - Added OrchestrationStatus import to fix cloning error - This was causing HTTP 500 errors during demo session cloning 2. services/procurement/app/api/purchase_orders.py + service: - Replaced app.utils.cache with shared.redis_utils - Standardizes caching across all services - Removed custom cache utilities (deleted app/utils/cache.py) 3. services/suppliers/app/consumers/alert_event_consumer.py: - Use Settings().REDIS_URL instead of os.getenv - Ensures proper Redis connection with TLS and authentication 4. shared/clients/recipes_client.py: - Fixed endpoint paths: recipes/recipes/{id} → recipes/{id} - Applied to all recipe methods (by_id, by_products, instructions, yield) 5. shared/clients/suppliers_client.py: - Fixed endpoint path: suppliers/suppliers/{id} → suppliers/{id} 6. shared/clients/procurement_client.py: - get_supplier_by_id now uses SuppliersServiceClient directly - Removes incorrect call to procurement service for supplier details Impact: - Demo session cloning now works without orchestrator errors ✅ - Consistent Redis usage across all services - Correct service boundaries (suppliers data from suppliers service) - Clean client endpoint paths 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 11:33:22 +01:00
try:
import json
await set_with_ttl(cache_key, json.dumps([po.model_dump() for po in result]), ttl=20)
logger.debug("Cached purchase orders", cache_key=cache_key, ttl=20, tenant_id=tenant_id, status=status)
except Exception as e:
logger.warning("Cache write failed, continuing without caching", cache_key=cache_key, error=str(e))
2025-12-05 20:07:01 +01:00
return result
2025-10-30 21:08:07 +01:00
except Exception as e:
logger.error("Error listing purchase orders", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=str(e))
2025-11-05 13:34:56 +01:00
@router.patch(
route_builder.build_resource_detail_route("purchase-orders", "po_id"),
response_model=PurchaseOrderResponse
)
2025-10-30 21:08:07 +01:00
async def update_purchase_order(
po_id: str,
po_data: PurchaseOrderUpdate,
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Update purchase order information
Only draft or pending_approval orders can be modified.
Financial field changes trigger automatic total recalculation.
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
po_data: Update data
Returns:
Updated purchase order
"""
try:
po = await service.update_purchase_order(
tenant_id=uuid.UUID(tenant_id),
po_id=uuid.UUID(po_id),
po_data=po_data
)
if not po:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.model_validate(po)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error("Error updating purchase order", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
2025-11-05 13:34:56 +01:00
@router.patch(
route_builder.build_resource_action_route("purchase-orders", "po_id", "status")
)
2025-10-30 21:08:07 +01:00
async def update_order_status(
po_id: str,
status: str = Query(..., description="New status"),
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
notes: Optional[str] = Query(default=None),
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Update purchase order status
Validates status transitions to prevent invalid state changes.
Valid transitions:
- draft -> pending_approval, approved, cancelled
- pending_approval -> approved, rejected, cancelled
- approved -> sent_to_supplier, cancelled
- sent_to_supplier -> confirmed, cancelled
- confirmed -> in_production, cancelled
- in_production -> shipped, cancelled
- shipped -> delivered, cancelled
- delivered -> completed
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
status: New status
notes: Optional status change notes
Returns:
Updated purchase order
"""
try:
po = await service.update_order_status(
tenant_id=uuid.UUID(tenant_id),
po_id=uuid.UUID(po_id),
status=status,
notes=notes
)
if not po:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.model_validate(po)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error("Error updating PO status", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
# ================================================================
# APPROVAL WORKFLOW
# ================================================================
2025-11-05 13:34:56 +01:00
@router.post(
route_builder.build_resource_action_route("purchase-orders", "po_id", "approve"),
response_model=PurchaseOrderResponse
)
2025-10-30 21:08:07 +01:00
async def approve_purchase_order(
po_id: str,
approval_data: PurchaseOrderApproval,
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Approve or reject a purchase order
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
approval_data: Approval or rejection data
Returns:
Updated purchase order
"""
try:
if approval_data.action == "approve":
po = await service.approve_purchase_order(
tenant_id=uuid.UUID(tenant_id),
po_id=uuid.UUID(po_id),
approved_by=approval_data.approved_by,
approval_notes=approval_data.notes
)
elif approval_data.action == "reject":
po = await service.reject_purchase_order(
tenant_id=uuid.UUID(tenant_id),
po_id=uuid.UUID(po_id),
rejected_by=approval_data.approved_by,
rejection_reason=approval_data.notes or "No reason provided"
)
else:
raise ValueError("Invalid action. Must be 'approve' or 'reject'")
if not po:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.model_validate(po)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error("Error in PO approval workflow", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
2025-11-05 13:34:56 +01:00
@router.post(
route_builder.build_resource_action_route("purchase-orders", "po_id", "cancel"),
response_model=PurchaseOrderResponse
)
2025-10-30 21:08:07 +01:00
async def cancel_purchase_order(
po_id: str,
reason: str = Query(..., description="Cancellation reason"),
cancelled_by: Optional[str] = Query(default=None),
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Cancel a purchase order
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
reason: Cancellation reason
cancelled_by: User ID performing cancellation
Returns:
Cancelled purchase order
"""
try:
po = await service.cancel_purchase_order(
tenant_id=uuid.UUID(tenant_id),
po_id=uuid.UUID(po_id),
cancelled_by=uuid.UUID(cancelled_by) if cancelled_by else None,
cancellation_reason=reason
)
if not po:
raise HTTPException(status_code=404, detail="Purchase order not found")
return PurchaseOrderResponse.model_validate(po)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error("Error cancelling purchase order", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
# ================================================================
# DELIVERY MANAGEMENT
# ================================================================
2025-11-05 13:34:56 +01:00
@router.post(
route_builder.build_nested_resource_route("purchase-orders", "po_id", "deliveries"),
response_model=DeliveryResponse,
status_code=201
)
2025-10-30 21:08:07 +01:00
async def create_delivery(
po_id: str,
delivery_data: DeliveryCreate,
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Create a delivery record for a purchase order
Tracks delivery scheduling, items, quality inspection, and receipt.
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
delivery_data: Delivery creation data
Returns:
DeliveryResponse with created delivery details
"""
try:
# Validate PO ID matches
if str(delivery_data.purchase_order_id) != po_id:
raise ValueError("Purchase order ID mismatch")
delivery = await service.create_delivery(
tenant_id=uuid.UUID(tenant_id),
delivery_data=delivery_data,
created_by=uuid.UUID(current_user.get("user_id"))
2025-10-30 21:08:07 +01:00
)
return DeliveryResponse.model_validate(delivery)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating delivery", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
2025-11-05 13:34:56 +01:00
@router.patch(
route_builder.build_nested_resource_route("purchase-orders", "po_id", "deliveries") + "/{delivery_id}/status"
)
2025-10-30 21:08:07 +01:00
async def update_delivery_status(
2025-11-05 13:34:56 +01:00
po_id: str,
2025-10-30 21:08:07 +01:00
delivery_id: str,
status: str = Query(..., description="New delivery status"),
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Update delivery status
Valid statuses: scheduled, in_transit, delivered, completed, cancelled
Args:
tenant_id: Tenant UUID
delivery_id: Delivery UUID
status: New status
Returns:
Updated delivery
"""
try:
delivery = await service.update_delivery_status(
tenant_id=uuid.UUID(tenant_id),
delivery_id=uuid.UUID(delivery_id),
status=status,
updated_by=uuid.UUID(current_user.get("user_id"))
2025-10-30 21:08:07 +01:00
)
if not delivery:
raise HTTPException(status_code=404, detail="Delivery not found")
return DeliveryResponse.model_validate(delivery)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
logger.error("Error updating delivery status", error=str(e), delivery_id=delivery_id)
raise HTTPException(status_code=500, detail=str(e))
# ================================================================
# INVOICE MANAGEMENT
# ================================================================
2025-11-05 13:34:56 +01:00
@router.post(
route_builder.build_nested_resource_route("purchase-orders", "po_id", "invoices"),
response_model=SupplierInvoiceResponse,
status_code=201
)
2025-10-30 21:08:07 +01:00
async def create_invoice(
po_id: str,
invoice_data: SupplierInvoiceCreate,
2025-11-05 13:34:56 +01:00
tenant_id: str = Path(..., description="Tenant ID"),
current_user: dict = Depends(get_current_user_dep),
2025-10-30 21:08:07 +01:00
service: PurchaseOrderService = Depends(get_po_service)
):
"""
Create a supplier invoice for a purchase order
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
invoice_data: Invoice creation data
Returns:
SupplierInvoiceResponse with created invoice details
"""
try:
# Validate PO ID matches
if str(invoice_data.purchase_order_id) != po_id:
raise ValueError("Purchase order ID mismatch")
invoice = await service.create_invoice(
tenant_id=uuid.UUID(tenant_id),
invoice_data=invoice_data,
created_by=uuid.UUID(current_user.get("user_id"))
2025-10-30 21:08:07 +01:00
)
return SupplierInvoiceResponse.model_validate(invoice)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error("Error creating invoice", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))
# ================================================================
# OVERDUE PO DETECTION
# ================================================================
@router.get(
route_builder.build_base_route("purchase-orders/overdue"),
response_model=List[dict]
)
async def get_overdue_purchase_orders(
tenant_id: str = Path(..., description="Tenant ID"),
limit: int = Query(10, ge=1, le=100, description="Max results")
):
"""
Get overdue purchase orders for dashboard display.
Returns POs that are past their estimated delivery date
but not yet marked as delivered.
Args:
tenant_id: Tenant UUID
limit: Maximum number of results (default: 10)
Returns:
List of overdue PO summaries with severity and days overdue
"""
try:
detector = OverduePODetector()
overdue_pos = await detector.get_overdue_pos_for_dashboard(
tenant_id=uuid.UUID(tenant_id),
limit=limit
)
return overdue_pos
except Exception as e:
logger.error("Error getting overdue POs", error=str(e), tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=str(e))
@router.get(
route_builder.build_resource_action_route("purchase-orders", "po_id", "overdue-status"),
response_model=dict
)
async def check_po_overdue_status(
po_id: str,
tenant_id: str = Path(..., description="Tenant ID")
):
"""
Check if a specific PO is overdue.
Args:
tenant_id: Tenant UUID
po_id: Purchase order UUID
Returns:
Overdue status info or null if not overdue
"""
try:
detector = OverduePODetector()
overdue_info = await detector.check_single_po_overdue(
po_id=uuid.UUID(po_id),
tenant_id=uuid.UUID(tenant_id)
)
if overdue_info:
return overdue_info
else:
return {"overdue": False}
except Exception as e:
logger.error("Error checking PO overdue status", error=str(e), po_id=po_id)
raise HTTPException(status_code=500, detail=str(e))