# 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" )