Files
bakery-ia/services/inventory/app/api/food_safety_operations.py
2025-10-29 06:58:05 +01:00

288 lines
10 KiB
Python

# services/inventory/app/api/food_safety_operations.py
"""
Food Safety Operations API - Business operations for food safety management
"""
from datetime import datetime
from typing import Optional
from decimal import Decimal
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 shared.auth.access_control import require_user_role, analytics_tier_required
from shared.routing import RouteBuilder
from app.core.database import get_db
from app.services.food_safety_service import FoodSafetyService
from app.schemas.food_safety import FoodSafetyMetrics
logger = structlog.get_logger()
route_builder = RouteBuilder('inventory')
router = APIRouter(tags=["food-safety-operations"])
async def get_food_safety_service() -> FoodSafetyService:
"""Get food safety service instance"""
return FoodSafetyService()
@router.post(
route_builder.build_nested_resource_route("food-safety/alerts", "alert_id", "acknowledge"),
response_model=dict
)
@require_user_role(['admin', 'owner', 'member'])
async def acknowledge_alert(
tenant_id: UUID = Path(...),
alert_id: UUID = Path(...),
notes: Optional[str] = Query(None, description="Acknowledgment notes"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Acknowledge a food safety alert"""
try:
update_query = """
UPDATE food_safety_alerts
SET status = 'acknowledged',
acknowledged_at = NOW(),
acknowledged_by = :user_id,
investigation_notes = COALESCE(investigation_notes, '') || :notes,
updated_at = NOW(),
updated_by = :user_id
WHERE id = :alert_id AND tenant_id = :tenant_id
"""
result = await db.execute(update_query, {
"alert_id": alert_id,
"tenant_id": tenant_id,
"user_id": UUID(current_user["user_id"]),
"notes": f"\nAcknowledged: {notes}" if notes else "\nAcknowledged"
})
if result.rowcount == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Food safety alert not found"
)
await db.commit()
logger.info("Food safety alert acknowledged",
alert_id=str(alert_id))
return {"message": "Alert acknowledged successfully"}
except HTTPException:
raise
except Exception as e:
logger.error("Error acknowledging alert",
alert_id=str(alert_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to acknowledge alert"
)
@router.get(
route_builder.build_analytics_route("food-safety-metrics"),
response_model=FoodSafetyMetrics
)
async def get_food_safety_metrics(
tenant_id: UUID = Path(...),
days_back: 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)
):
"""Get food safety performance metrics"""
try:
compliance_query = """
SELECT
COUNT(*) as total,
COUNT(CASE WHEN compliance_status = 'compliant' THEN 1 END) as compliant
FROM food_safety_compliance
WHERE tenant_id = :tenant_id AND is_active = true
"""
result = await db.execute(compliance_query, {"tenant_id": tenant_id})
compliance_stats = result.fetchone()
compliance_rate = 0.0
if compliance_stats.total > 0:
compliance_rate = (compliance_stats.compliant / compliance_stats.total) * 100
temp_query = """
SELECT
COUNT(*) as total_readings,
COUNT(CASE WHEN is_within_range THEN 1 END) as compliant_readings
FROM temperature_logs
WHERE tenant_id = :tenant_id
AND recorded_at > NOW() - INTERVAL '%s days'
""" % days_back
result = await db.execute(temp_query, {"tenant_id": tenant_id})
temp_stats = result.fetchone()
temp_compliance_rate = 0.0
if temp_stats.total_readings > 0:
temp_compliance_rate = (temp_stats.compliant_readings / temp_stats.total_readings) * 100
alert_query = """
SELECT
COUNT(*) as total_alerts,
COUNT(CASE WHEN is_recurring THEN 1 END) as recurring_alerts,
COUNT(CASE WHEN regulatory_action_required THEN 1 END) as regulatory_violations,
AVG(CASE WHEN response_time_minutes IS NOT NULL THEN response_time_minutes END) as avg_response_time,
AVG(CASE WHEN resolution_time_minutes IS NOT NULL THEN resolution_time_minutes END) as avg_resolution_time
FROM food_safety_alerts
WHERE tenant_id = :tenant_id
AND created_at > NOW() - INTERVAL '%s days'
""" % days_back
result = await db.execute(alert_query, {"tenant_id": tenant_id})
alert_stats = result.fetchone()
return FoodSafetyMetrics(
compliance_rate=Decimal(str(compliance_rate)),
temperature_compliance_rate=Decimal(str(temp_compliance_rate)),
alert_response_time_avg=Decimal(str(alert_stats.avg_response_time or 0)),
alert_resolution_time_avg=Decimal(str(alert_stats.avg_resolution_time or 0)),
recurring_issues_count=alert_stats.recurring_alerts or 0,
regulatory_violations=alert_stats.regulatory_violations or 0,
certification_coverage=Decimal(str(compliance_rate)),
audit_score_avg=Decimal("85.0"),
risk_score=Decimal("3.2")
)
except Exception as e:
logger.error("Error getting food safety metrics", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve food safety metrics"
)
@router.get(
route_builder.build_operations_route("food-safety/status")
)
async def get_food_safety_status(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
):
"""Get food safety service status"""
try:
return {
"service": "food-safety",
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"tenant_id": str(tenant_id),
"features": {
"compliance_tracking": "enabled",
"temperature_monitoring": "enabled",
"automated_alerts": "enabled",
"regulatory_reporting": "enabled"
}
}
except Exception as e:
logger.error("Error getting food safety status", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get food safety status"
)
@router.get(
route_builder.build_operations_route("food-safety/temperature/violations")
)
async def get_temperature_violations(
tenant_id: UUID = Path(...),
days_back: int = Query(7, ge=1, le=90, description="Days to analyze"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get temperature violations summary"""
try:
query = """
SELECT
COUNT(*) as total_violations,
COUNT(DISTINCT storage_location) as affected_locations,
COUNT(DISTINCT equipment_id) as affected_equipment,
AVG(ABS(temperature_celsius - (min_temp_celsius + max_temp_celsius)/2)) as avg_deviation
FROM temperature_logs
WHERE tenant_id = :tenant_id
AND is_within_range = false
AND recorded_at > NOW() - INTERVAL '%s days'
""" % days_back
result = await db.execute(query, {"tenant_id": tenant_id})
stats = result.fetchone()
return {
"period_days": days_back,
"total_violations": stats.total_violations or 0,
"affected_locations": stats.affected_locations or 0,
"affected_equipment": stats.affected_equipment or 0,
"average_deviation_celsius": float(stats.avg_deviation or 0)
}
except Exception as e:
logger.error("Error getting temperature violations", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get temperature violations"
)
@router.get(
route_builder.build_operations_route("food-safety/compliance/summary")
)
async def get_compliance_summary(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get compliance summary by standard"""
try:
query = """
SELECT
standard,
COUNT(*) as total,
COUNT(CASE WHEN compliance_status = 'compliant' THEN 1 END) as compliant,
COUNT(CASE WHEN compliance_status = 'non_compliant' THEN 1 END) as non_compliant,
COUNT(CASE WHEN compliance_status = 'pending' THEN 1 END) as pending
FROM food_safety_compliance
WHERE tenant_id = :tenant_id AND is_active = true
GROUP BY standard
ORDER BY standard
"""
result = await db.execute(query, {"tenant_id": tenant_id})
records = result.fetchall()
summary = []
for record in records:
compliance_rate = (record.compliant / record.total * 100) if record.total > 0 else 0
summary.append({
"standard": record.standard,
"total_items": record.total,
"compliant": record.compliant,
"non_compliant": record.non_compliant,
"pending": record.pending,
"compliance_rate": round(compliance_rate, 2)
})
return {
"tenant_id": str(tenant_id),
"standards": summary,
"total_standards": len(summary)
}
except Exception as e:
logger.error("Error getting compliance summary", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get compliance summary"
)