2025-10-23 07:44:54 +02:00
|
|
|
# ================================================================
|
|
|
|
|
# services/inventory/app/api/sustainability.py
|
|
|
|
|
# ================================================================
|
|
|
|
|
"""
|
2025-12-15 13:39:33 +01:00
|
|
|
Inventory Sustainability API - Microservices Architecture
|
|
|
|
|
Provides inventory-specific sustainability metrics (waste tracking, expiry alerts)
|
|
|
|
|
Following microservices principles: each service owns its domain data
|
2025-10-23 07:44:54 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
from typing import Optional
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
import structlog
|
|
|
|
|
|
|
|
|
|
from shared.auth.decorators import get_current_user_dep
|
|
|
|
|
from app.core.database import get_db
|
2025-12-15 13:39:33 +01:00
|
|
|
from app.repositories.stock_movement_repository import StockMovementRepository
|
|
|
|
|
from app.repositories.stock_repository import StockRepository
|
2025-10-23 07:44:54 +02:00
|
|
|
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
|
|
|
|
router = APIRouter(tags=["sustainability"])
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# ===== INVENTORY SUSTAINABILITY ENDPOINTS =====
|
2025-10-23 07:44:54 +02:00
|
|
|
|
|
|
|
|
@router.get(
|
2025-12-15 13:39:33 +01:00
|
|
|
"/api/v1/tenants/{tenant_id}/inventory/sustainability/waste-metrics",
|
|
|
|
|
summary="Get Inventory Waste Metrics",
|
|
|
|
|
description="Get inventory-specific waste metrics from stock movements and expired items"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
2025-12-15 13:39:33 +01:00
|
|
|
async def get_inventory_waste_metrics(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
start_date: Optional[datetime] = Query(None, description="Start date for metrics (default: 30 days ago)"),
|
|
|
|
|
end_date: Optional[datetime] = Query(None, description="End date for metrics (default: now)"),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
db: AsyncSession = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""
|
2025-12-15 13:39:33 +01:00
|
|
|
Get inventory waste metrics including:
|
|
|
|
|
- Waste from stock movements (expired, damaged, contaminated, spillage)
|
|
|
|
|
- Total waste quantity and cost
|
|
|
|
|
- Breakdown by waste reason
|
|
|
|
|
- Number of waste incidents
|
|
|
|
|
|
|
|
|
|
**Domain**: Inventory Service owns this data
|
|
|
|
|
**Use case**: Frontend aggregates with production service waste metrics
|
2025-10-23 07:44:54 +02:00
|
|
|
"""
|
|
|
|
|
try:
|
2025-12-15 13:39:33 +01:00
|
|
|
# Default to last 30 days
|
|
|
|
|
if not end_date:
|
|
|
|
|
end_date = datetime.now()
|
|
|
|
|
if not start_date:
|
|
|
|
|
start_date = end_date - timedelta(days=30)
|
|
|
|
|
|
|
|
|
|
# Get inventory waste from stock movements
|
|
|
|
|
stock_movement_repo = StockMovementRepository(db)
|
|
|
|
|
|
|
|
|
|
# Get waste movements using explicit date range
|
|
|
|
|
waste_movements = await stock_movement_repo.get_waste_movements(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
start_date=start_date,
|
2025-12-15 13:39:33 +01:00
|
|
|
end_date=end_date,
|
|
|
|
|
limit=1000
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# Calculate period days
|
|
|
|
|
days_back = (end_date - start_date).days
|
|
|
|
|
|
|
|
|
|
# Calculate totals
|
|
|
|
|
total_waste_kg = 0.0
|
|
|
|
|
total_waste_cost_eur = 0.0
|
|
|
|
|
waste_by_reason = {
|
|
|
|
|
'expired': 0.0,
|
|
|
|
|
'damaged': 0.0,
|
|
|
|
|
'contaminated': 0.0,
|
|
|
|
|
'spillage': 0.0,
|
|
|
|
|
'other': 0.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for movement in (waste_movements or []):
|
|
|
|
|
quantity = float(movement.quantity) if movement.quantity else 0.0
|
|
|
|
|
total_waste_kg += quantity
|
|
|
|
|
|
|
|
|
|
# Add to cost if available
|
|
|
|
|
if movement.total_cost:
|
|
|
|
|
total_waste_cost_eur += float(movement.total_cost)
|
|
|
|
|
|
|
|
|
|
# Categorize by reason
|
|
|
|
|
reason = movement.reason_code or 'other'
|
|
|
|
|
if reason in waste_by_reason:
|
|
|
|
|
waste_by_reason[reason] += quantity
|
|
|
|
|
else:
|
|
|
|
|
waste_by_reason['other'] += quantity
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
'inventory_waste_kg': round(total_waste_kg, 2),
|
|
|
|
|
'waste_cost_eur': round(total_waste_cost_eur, 2),
|
|
|
|
|
'waste_by_reason': {
|
|
|
|
|
key: round(val, 2) for key, val in waste_by_reason.items()
|
|
|
|
|
},
|
|
|
|
|
'waste_movements_count': len(waste_movements) if waste_movements else 0,
|
|
|
|
|
'period': {
|
|
|
|
|
'start_date': start_date.isoformat(),
|
|
|
|
|
'end_date': end_date.isoformat(),
|
|
|
|
|
'days': days_back
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 07:44:54 +02:00
|
|
|
logger.info(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Inventory waste metrics retrieved",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
2025-12-15 13:39:33 +01:00
|
|
|
waste_kg=result['inventory_waste_kg'],
|
|
|
|
|
movements=result['waste_movements_count']
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
return result
|
2025-10-23 07:44:54 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Error getting inventory waste metrics",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
error=str(e)
|
|
|
|
|
)
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2025-12-15 13:39:33 +01:00
|
|
|
detail=f"Failed to retrieve inventory waste metrics: {str(e)}"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
2025-12-15 13:39:33 +01:00
|
|
|
"/api/v1/tenants/{tenant_id}/inventory/sustainability/expiry-alerts",
|
|
|
|
|
summary="Get Expiry Alerts",
|
|
|
|
|
description="Get items at risk of expiring soon (waste prevention opportunities)"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
2025-12-15 13:39:33 +01:00
|
|
|
async def get_expiry_alerts(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
2025-12-15 13:39:33 +01:00
|
|
|
days_ahead: int = Query(7, ge=1, le=30, description="Days ahead to check for expiry"),
|
2025-10-23 07:44:54 +02:00
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
db: AsyncSession = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""
|
2025-12-15 13:39:33 +01:00
|
|
|
Get items at risk of expiring within the specified time window.
|
2025-10-23 07:44:54 +02:00
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
**Purpose**: Waste prevention and FIFO compliance
|
|
|
|
|
**Returns**:
|
|
|
|
|
- Items expiring soon
|
|
|
|
|
- Potential waste value
|
|
|
|
|
- Recommended actions
|
2025-10-23 07:44:54 +02:00
|
|
|
"""
|
|
|
|
|
try:
|
2025-12-15 13:39:33 +01:00
|
|
|
stock_repo = StockRepository(db)
|
2025-10-23 07:44:54 +02:00
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# Get stock items expiring soon
|
|
|
|
|
expiring_soon = await stock_repo.get_expiring_stock(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=tenant_id,
|
2025-12-15 13:39:33 +01:00
|
|
|
days_ahead=days_ahead
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
at_risk_items = []
|
|
|
|
|
total_at_risk_kg = 0.0
|
|
|
|
|
total_at_risk_value_eur = 0.0
|
|
|
|
|
|
|
|
|
|
for stock in (expiring_soon or []):
|
|
|
|
|
quantity = float(stock.quantity) if stock.quantity else 0.0
|
|
|
|
|
unit_cost = float(stock.unit_cost) if stock.unit_cost else 0.0
|
|
|
|
|
total_value = quantity * unit_cost
|
|
|
|
|
|
|
|
|
|
total_at_risk_kg += quantity
|
|
|
|
|
total_at_risk_value_eur += total_value
|
|
|
|
|
|
|
|
|
|
at_risk_items.append({
|
|
|
|
|
'stock_id': str(stock.id),
|
|
|
|
|
'ingredient_id': str(stock.ingredient_id),
|
|
|
|
|
'ingredient_name': stock.ingredient.name if stock.ingredient else 'Unknown',
|
|
|
|
|
'quantity': round(quantity, 2),
|
|
|
|
|
'unit': stock.unit,
|
|
|
|
|
'expiry_date': stock.expiry_date.isoformat() if stock.expiry_date else None,
|
|
|
|
|
'days_until_expiry': (stock.expiry_date - datetime.now()).days if stock.expiry_date else None,
|
|
|
|
|
'value_eur': round(total_value, 2),
|
|
|
|
|
'location': stock.location or 'unspecified'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
'at_risk_items': at_risk_items,
|
|
|
|
|
'total_items': len(at_risk_items),
|
|
|
|
|
'total_at_risk_kg': round(total_at_risk_kg, 2),
|
|
|
|
|
'total_at_risk_value_eur': round(total_at_risk_value_eur, 2),
|
|
|
|
|
'alert_window_days': days_ahead,
|
|
|
|
|
'checked_at': datetime.now().isoformat()
|
2025-10-23 07:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Expiry alerts retrieved",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
2025-12-15 13:39:33 +01:00
|
|
|
at_risk_items=result['total_items'],
|
|
|
|
|
at_risk_value=result['total_at_risk_value_eur']
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
return result
|
2025-10-23 07:44:54 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Error getting expiry alerts",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
error=str(e)
|
|
|
|
|
)
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2025-12-15 13:39:33 +01:00
|
|
|
detail=f"Failed to retrieve expiry alerts: {str(e)}"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
@router.get(
|
|
|
|
|
"/api/v1/tenants/{tenant_id}/inventory/sustainability/waste-events",
|
|
|
|
|
summary="Get Waste Event Log",
|
|
|
|
|
description="Get detailed waste event history with reasons, costs, and timestamps"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
2025-12-15 13:39:33 +01:00
|
|
|
async def get_waste_events(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
2025-12-15 13:39:33 +01:00
|
|
|
limit: int = Query(50, ge=1, le=500, description="Maximum number of events to return"),
|
|
|
|
|
offset: int = Query(0, ge=0, description="Number of events to skip"),
|
|
|
|
|
start_date: Optional[datetime] = Query(None, description="Start date filter"),
|
|
|
|
|
end_date: Optional[datetime] = Query(None, description="End date filter"),
|
|
|
|
|
reason_code: Optional[str] = Query(None, description="Filter by reason code (expired, damaged, etc.)"),
|
2025-10-23 07:44:54 +02:00
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
db: AsyncSession = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""
|
2025-12-15 13:39:33 +01:00
|
|
|
Get detailed waste event log for trend analysis and auditing.
|
|
|
|
|
|
|
|
|
|
**Use cases**:
|
|
|
|
|
- Root cause analysis
|
|
|
|
|
- Waste trend identification
|
|
|
|
|
- Compliance auditing
|
|
|
|
|
- Process improvement
|
2025-10-23 07:44:54 +02:00
|
|
|
"""
|
|
|
|
|
try:
|
2025-12-15 13:39:33 +01:00
|
|
|
stock_movement_repo = StockMovementRepository(db)
|
2025-10-23 07:44:54 +02:00
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# Default to last 90 days if no date range
|
|
|
|
|
if not end_date:
|
|
|
|
|
end_date = datetime.now()
|
|
|
|
|
if not start_date:
|
|
|
|
|
start_date = end_date - timedelta(days=90)
|
2025-10-23 07:44:54 +02:00
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
days_back = (end_date - start_date).days
|
2025-10-23 07:44:54 +02:00
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# Get waste movements
|
|
|
|
|
waste_movements = await stock_movement_repo.get_waste_movements(
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
days_back=days_back,
|
|
|
|
|
limit=limit + offset # Get extra for offset handling
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# Filter by reason if specified
|
|
|
|
|
if reason_code and waste_movements:
|
|
|
|
|
waste_movements = [
|
|
|
|
|
m for m in waste_movements
|
|
|
|
|
if m.reason_code == reason_code
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Apply pagination
|
|
|
|
|
total_count = len(waste_movements) if waste_movements else 0
|
|
|
|
|
paginated_movements = (waste_movements or [])[offset:offset + limit]
|
|
|
|
|
|
|
|
|
|
# Format events
|
|
|
|
|
events = []
|
|
|
|
|
for movement in paginated_movements:
|
|
|
|
|
events.append({
|
|
|
|
|
'event_id': str(movement.id),
|
|
|
|
|
'ingredient_id': str(movement.ingredient_id),
|
|
|
|
|
'ingredient_name': movement.ingredient.name if movement.ingredient else 'Unknown',
|
|
|
|
|
'quantity': float(movement.quantity) if movement.quantity else 0.0,
|
|
|
|
|
'unit': movement.unit,
|
|
|
|
|
'reason_code': movement.reason_code,
|
|
|
|
|
'total_cost_eur': float(movement.total_cost) if movement.total_cost else 0.0,
|
|
|
|
|
'movement_date': movement.movement_date.isoformat() if movement.movement_date else None,
|
|
|
|
|
'notes': movement.notes or '',
|
|
|
|
|
'created_by': movement.created_by
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
'events': events,
|
|
|
|
|
'total_count': total_count,
|
|
|
|
|
'returned_count': len(events),
|
|
|
|
|
'offset': offset,
|
|
|
|
|
'limit': limit,
|
|
|
|
|
'period': {
|
|
|
|
|
'start_date': start_date.isoformat(),
|
|
|
|
|
'end_date': end_date.isoformat()
|
|
|
|
|
},
|
|
|
|
|
'filter': {
|
|
|
|
|
'reason_code': reason_code
|
|
|
|
|
}
|
2025-10-23 07:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Waste events retrieved",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
2025-12-15 13:39:33 +01:00
|
|
|
total_events=total_count,
|
|
|
|
|
returned=len(events)
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
return result
|
2025-10-23 07:44:54 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Error getting waste events",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
error=str(e)
|
|
|
|
|
)
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2025-12-15 13:39:33 +01:00
|
|
|
detail=f"Failed to retrieve waste events: {str(e)}"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
2025-12-15 13:39:33 +01:00
|
|
|
"/api/v1/tenants/{tenant_id}/inventory/sustainability/summary",
|
|
|
|
|
summary="Get Inventory Sustainability Summary",
|
|
|
|
|
description="Get condensed inventory sustainability data for dashboard widgets"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
2025-12-15 13:39:33 +01:00
|
|
|
async def get_inventory_sustainability_summary(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
|
|
|
days: int = Query(30, ge=1, le=365, description="Number of days to analyze"),
|
|
|
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
|
|
|
db: AsyncSession = Depends(get_db)
|
|
|
|
|
):
|
|
|
|
|
"""
|
2025-12-15 13:39:33 +01:00
|
|
|
Get summary of inventory sustainability metrics optimized for widgets.
|
|
|
|
|
|
|
|
|
|
**Returns**: Condensed version of waste metrics and expiry alerts
|
|
|
|
|
**Use case**: Dashboard widgets, quick overview cards
|
2025-10-23 07:44:54 +02:00
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
end_date = datetime.now()
|
|
|
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
# Get waste metrics
|
|
|
|
|
stock_movement_repo = StockMovementRepository(db)
|
|
|
|
|
waste_movements = await stock_movement_repo.get_waste_movements(
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=tenant_id,
|
2025-12-15 13:39:33 +01:00
|
|
|
days_back=days,
|
|
|
|
|
limit=1000
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
total_waste_kg = sum(
|
|
|
|
|
float(m.quantity) for m in (waste_movements or [])
|
|
|
|
|
if m.quantity
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
total_waste_cost = sum(
|
|
|
|
|
float(m.total_cost) for m in (waste_movements or [])
|
|
|
|
|
if m.total_cost
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Get expiry alerts
|
|
|
|
|
stock_repo = StockRepository(db)
|
|
|
|
|
expiring_soon = await stock_repo.get_expiring_stock(
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
days_ahead=7
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
at_risk_count = len(expiring_soon) if expiring_soon else 0
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
'inventory_waste_kg': round(total_waste_kg, 2),
|
|
|
|
|
'waste_cost_eur': round(total_waste_cost, 2),
|
|
|
|
|
'waste_incidents': len(waste_movements) if waste_movements else 0,
|
|
|
|
|
'items_at_risk_expiry': at_risk_count,
|
|
|
|
|
'period_days': days,
|
|
|
|
|
'period': {
|
|
|
|
|
'start_date': start_date.isoformat(),
|
|
|
|
|
'end_date': end_date.isoformat()
|
|
|
|
|
}
|
2025-10-23 07:44:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Inventory sustainability summary retrieved",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
2025-12-15 13:39:33 +01:00
|
|
|
waste_kg=result['inventory_waste_kg']
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-15 13:39:33 +01:00
|
|
|
return result
|
2025-10-23 07:44:54 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(
|
2025-12-15 13:39:33 +01:00
|
|
|
"Error getting inventory sustainability summary",
|
2025-10-23 07:44:54 +02:00
|
|
|
tenant_id=str(tenant_id),
|
|
|
|
|
error=str(e)
|
|
|
|
|
)
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
2025-12-15 13:39:33 +01:00
|
|
|
detail=f"Failed to retrieve inventory sustainability summary: {str(e)}"
|
2025-10-23 07:44:54 +02:00
|
|
|
)
|