718 lines
26 KiB
Python
718 lines
26 KiB
Python
# ================================================================
|
|
# services/inventory/app/api/food_safety.py
|
|
# ================================================================
|
|
"""
|
|
Food Safety API endpoints for Inventory Service
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import List, 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, get_current_tenant_id_dep
|
|
from app.core.database import get_db
|
|
from app.services.food_safety_service import FoodSafetyService
|
|
from app.schemas.food_safety import (
|
|
FoodSafetyComplianceCreate,
|
|
FoodSafetyComplianceUpdate,
|
|
FoodSafetyComplianceResponse,
|
|
TemperatureLogCreate,
|
|
TemperatureLogResponse,
|
|
FoodSafetyAlertCreate,
|
|
FoodSafetyAlertUpdate,
|
|
FoodSafetyAlertResponse,
|
|
BulkTemperatureLogCreate,
|
|
FoodSafetyFilter,
|
|
TemperatureMonitoringFilter,
|
|
FoodSafetyMetrics,
|
|
TemperatureAnalytics
|
|
)
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
router = APIRouter(prefix="/food-safety", tags=["food-safety"])
|
|
|
|
|
|
# ===== Dependency Injection =====
|
|
|
|
async def get_food_safety_service() -> FoodSafetyService:
|
|
"""Get food safety service instance"""
|
|
return FoodSafetyService()
|
|
|
|
|
|
# ===== Compliance Management Endpoints =====
|
|
|
|
@router.post("/tenants/{tenant_id}/compliance", response_model=FoodSafetyComplianceResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_compliance_record(
|
|
compliance_data: FoodSafetyComplianceCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Create a new food safety compliance record"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Ensure tenant_id matches
|
|
compliance_data.tenant_id = tenant_id
|
|
|
|
compliance = await food_safety_service.create_compliance_record(
|
|
db,
|
|
compliance_data,
|
|
user_id=UUID(current_user["sub"])
|
|
)
|
|
|
|
logger.info("Compliance record created",
|
|
compliance_id=str(compliance.id),
|
|
standard=compliance.standard)
|
|
|
|
return compliance
|
|
|
|
except ValueError as e:
|
|
logger.warning("Invalid compliance data", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(e)
|
|
)
|
|
except Exception as e:
|
|
logger.error("Error creating compliance record", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to create compliance record"
|
|
)
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/compliance", response_model=List[FoodSafetyComplianceResponse])
|
|
async def get_compliance_records(
|
|
tenant_id: UUID = Path(...),
|
|
ingredient_id: Optional[UUID] = Query(None, description="Filter by ingredient ID"),
|
|
standard: Optional[str] = Query(None, description="Filter by compliance standard"),
|
|
status_filter: Optional[str] = Query(None, description="Filter by compliance status"),
|
|
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
|
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get compliance records with filtering"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Build query filters
|
|
filters = {}
|
|
if ingredient_id:
|
|
filters["ingredient_id"] = ingredient_id
|
|
if standard:
|
|
filters["standard"] = standard
|
|
if status_filter:
|
|
filters["compliance_status"] = status_filter
|
|
|
|
# Query compliance records
|
|
query = """
|
|
SELECT * FROM food_safety_compliance
|
|
WHERE tenant_id = :tenant_id AND is_active = true
|
|
"""
|
|
params = {"tenant_id": tenant_id}
|
|
|
|
if filters:
|
|
for key, value in filters.items():
|
|
query += f" AND {key} = :{key}"
|
|
params[key] = value
|
|
|
|
query += " ORDER BY created_at DESC LIMIT :limit OFFSET :skip"
|
|
params.update({"limit": limit, "skip": skip})
|
|
|
|
result = await db.execute(query, params)
|
|
records = result.fetchall()
|
|
|
|
return [
|
|
FoodSafetyComplianceResponse(**dict(record))
|
|
for record in records
|
|
]
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting compliance records", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to retrieve compliance records"
|
|
)
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/compliance/{compliance_id}", response_model=FoodSafetyComplianceResponse)
|
|
async def update_compliance_record(
|
|
compliance_data: FoodSafetyComplianceUpdate,
|
|
tenant_id: UUID = Path(...),
|
|
compliance_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Update an existing compliance record"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
compliance = await food_safety_service.update_compliance_record(
|
|
db,
|
|
compliance_id,
|
|
tenant_id,
|
|
compliance_data,
|
|
user_id=UUID(current_user["sub"])
|
|
)
|
|
|
|
if not compliance:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Compliance record not found"
|
|
)
|
|
|
|
logger.info("Compliance record updated",
|
|
compliance_id=str(compliance.id))
|
|
|
|
return compliance
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error updating compliance record",
|
|
compliance_id=str(compliance_id),
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to update compliance record"
|
|
)
|
|
|
|
|
|
# ===== Temperature Monitoring Endpoints =====
|
|
|
|
@router.post("/tenants/{tenant_id}/temperature", response_model=TemperatureLogResponse, status_code=status.HTTP_201_CREATED)
|
|
async def log_temperature(
|
|
temp_data: TemperatureLogCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Log a temperature reading"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Ensure tenant_id matches
|
|
temp_data.tenant_id = tenant_id
|
|
|
|
temp_log = await food_safety_service.log_temperature(
|
|
db,
|
|
temp_data,
|
|
user_id=UUID(current_user["sub"])
|
|
)
|
|
|
|
logger.info("Temperature logged",
|
|
location=temp_data.storage_location,
|
|
temperature=temp_data.temperature_celsius)
|
|
|
|
return temp_log
|
|
|
|
except Exception as e:
|
|
logger.error("Error logging temperature", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to log temperature"
|
|
)
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/temperature/bulk", response_model=List[TemperatureLogResponse])
|
|
async def bulk_log_temperatures(
|
|
bulk_data: BulkTemperatureLogCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Bulk log temperature readings"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Ensure tenant_id matches for all readings
|
|
for reading in bulk_data.readings:
|
|
reading.tenant_id = tenant_id
|
|
|
|
temp_logs = await food_safety_service.bulk_log_temperatures(
|
|
db,
|
|
bulk_data.readings,
|
|
user_id=UUID(current_user["sub"])
|
|
)
|
|
|
|
logger.info("Bulk temperature logging completed",
|
|
count=len(bulk_data.readings))
|
|
|
|
return temp_logs
|
|
|
|
except Exception as e:
|
|
logger.error("Error bulk logging temperatures", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to bulk log temperatures"
|
|
)
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/temperature", response_model=List[TemperatureLogResponse])
|
|
async def get_temperature_logs(
|
|
tenant_id: UUID = Path(...),
|
|
location: Optional[str] = Query(None, description="Filter by storage location"),
|
|
equipment_id: Optional[str] = Query(None, description="Filter by equipment ID"),
|
|
date_from: Optional[datetime] = Query(None, description="Start date for filtering"),
|
|
date_to: Optional[datetime] = Query(None, description="End date for filtering"),
|
|
violations_only: bool = Query(False, description="Show only temperature violations"),
|
|
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
|
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get temperature logs with filtering"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Build query
|
|
where_conditions = ["tenant_id = :tenant_id"]
|
|
params = {"tenant_id": tenant_id}
|
|
|
|
if location:
|
|
where_conditions.append("storage_location ILIKE :location")
|
|
params["location"] = f"%{location}%"
|
|
|
|
if equipment_id:
|
|
where_conditions.append("equipment_id = :equipment_id")
|
|
params["equipment_id"] = equipment_id
|
|
|
|
if date_from:
|
|
where_conditions.append("recorded_at >= :date_from")
|
|
params["date_from"] = date_from
|
|
|
|
if date_to:
|
|
where_conditions.append("recorded_at <= :date_to")
|
|
params["date_to"] = date_to
|
|
|
|
if violations_only:
|
|
where_conditions.append("is_within_range = false")
|
|
|
|
where_clause = " AND ".join(where_conditions)
|
|
|
|
query = f"""
|
|
SELECT * FROM temperature_logs
|
|
WHERE {where_clause}
|
|
ORDER BY recorded_at DESC
|
|
LIMIT :limit OFFSET :skip
|
|
"""
|
|
params.update({"limit": limit, "skip": skip})
|
|
|
|
result = await db.execute(query, params)
|
|
logs = result.fetchall()
|
|
|
|
return [
|
|
TemperatureLogResponse(**dict(log))
|
|
for log in logs
|
|
]
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting temperature logs", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to retrieve temperature logs"
|
|
)
|
|
|
|
|
|
# ===== Alert Management Endpoints =====
|
|
|
|
@router.post("/tenants/{tenant_id}/alerts", response_model=FoodSafetyAlertResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_food_safety_alert(
|
|
alert_data: FoodSafetyAlertCreate,
|
|
tenant_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
food_safety_service: FoodSafetyService = Depends(get_food_safety_service),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Create a food safety alert"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Ensure tenant_id matches
|
|
alert_data.tenant_id = tenant_id
|
|
|
|
alert = await food_safety_service.create_food_safety_alert(
|
|
db,
|
|
alert_data,
|
|
user_id=UUID(current_user["sub"])
|
|
)
|
|
|
|
logger.info("Food safety alert created",
|
|
alert_id=str(alert.id),
|
|
alert_type=alert.alert_type)
|
|
|
|
return alert
|
|
|
|
except Exception as e:
|
|
logger.error("Error creating food safety alert", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to create food safety alert"
|
|
)
|
|
|
|
|
|
@router.get("/tenants/{tenant_id}/alerts", response_model=List[FoodSafetyAlertResponse])
|
|
async def get_food_safety_alerts(
|
|
tenant_id: UUID = Path(...),
|
|
alert_type: Optional[str] = Query(None, description="Filter by alert type"),
|
|
severity: Optional[str] = Query(None, description="Filter by severity"),
|
|
status_filter: Optional[str] = Query(None, description="Filter by status"),
|
|
unresolved_only: bool = Query(True, description="Show only unresolved alerts"),
|
|
skip: int = Query(0, ge=0, description="Number of alerts to skip"),
|
|
limit: int = Query(100, ge=1, le=1000, description="Number of alerts to return"),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get food safety alerts with filtering"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Build query filters
|
|
where_conditions = ["tenant_id = :tenant_id"]
|
|
params = {"tenant_id": tenant_id}
|
|
|
|
if alert_type:
|
|
where_conditions.append("alert_type = :alert_type")
|
|
params["alert_type"] = alert_type
|
|
|
|
if severity:
|
|
where_conditions.append("severity = :severity")
|
|
params["severity"] = severity
|
|
|
|
if status_filter:
|
|
where_conditions.append("status = :status")
|
|
params["status"] = status_filter
|
|
elif unresolved_only:
|
|
where_conditions.append("status NOT IN ('resolved', 'dismissed')")
|
|
|
|
where_clause = " AND ".join(where_conditions)
|
|
|
|
query = f"""
|
|
SELECT * FROM food_safety_alerts
|
|
WHERE {where_clause}
|
|
ORDER BY created_at DESC
|
|
LIMIT :limit OFFSET :skip
|
|
"""
|
|
params.update({"limit": limit, "skip": skip})
|
|
|
|
result = await db.execute(query, params)
|
|
alerts = result.fetchall()
|
|
|
|
return [
|
|
FoodSafetyAlertResponse(**dict(alert))
|
|
for alert in alerts
|
|
]
|
|
|
|
except Exception as e:
|
|
logger.error("Error getting food safety alerts", error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to retrieve food safety alerts"
|
|
)
|
|
|
|
|
|
@router.put("/tenants/{tenant_id}/alerts/{alert_id}", response_model=FoodSafetyAlertResponse)
|
|
async def update_food_safety_alert(
|
|
alert_data: FoodSafetyAlertUpdate,
|
|
tenant_id: UUID = Path(...),
|
|
alert_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Update a food safety alert"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Get existing alert
|
|
alert_query = "SELECT * FROM food_safety_alerts WHERE id = :alert_id AND tenant_id = :tenant_id"
|
|
result = await db.execute(alert_query, {"alert_id": alert_id, "tenant_id": tenant_id})
|
|
alert_record = result.fetchone()
|
|
|
|
if not alert_record:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Food safety alert not found"
|
|
)
|
|
|
|
# Update alert fields
|
|
update_fields = alert_data.dict(exclude_unset=True)
|
|
if update_fields:
|
|
set_clauses = []
|
|
params = {"alert_id": alert_id, "tenant_id": tenant_id}
|
|
|
|
for field, value in update_fields.items():
|
|
set_clauses.append(f"{field} = :{field}")
|
|
params[field] = value
|
|
|
|
# Add updated timestamp and user
|
|
set_clauses.append("updated_at = NOW()")
|
|
set_clauses.append("updated_by = :updated_by")
|
|
params["updated_by"] = UUID(current_user["sub"])
|
|
|
|
update_query = f"""
|
|
UPDATE food_safety_alerts
|
|
SET {', '.join(set_clauses)}
|
|
WHERE id = :alert_id AND tenant_id = :tenant_id
|
|
"""
|
|
|
|
await db.execute(update_query, params)
|
|
await db.commit()
|
|
|
|
# Get updated alert
|
|
result = await db.execute(alert_query, {"alert_id": alert_id, "tenant_id": tenant_id})
|
|
updated_alert = result.fetchone()
|
|
|
|
logger.info("Food safety alert updated",
|
|
alert_id=str(alert_id))
|
|
|
|
return FoodSafetyAlertResponse(**dict(updated_alert))
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Error updating food safety alert",
|
|
alert_id=str(alert_id),
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to update food safety alert"
|
|
)
|
|
|
|
|
|
@router.post("/tenants/{tenant_id}/alerts/{alert_id}/acknowledge")
|
|
async def acknowledge_alert(
|
|
tenant_id: UUID = Path(...),
|
|
alert_id: UUID = Path(...),
|
|
notes: Optional[str] = Query(None, description="Acknowledgment notes"),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Acknowledge a food safety alert"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Update alert to acknowledged status
|
|
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["sub"]),
|
|
"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"
|
|
)
|
|
|
|
|
|
# ===== Analytics and Reporting Endpoints =====
|
|
|
|
@router.get("/tenants/{tenant_id}/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_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get food safety performance metrics"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
# Calculate compliance rate
|
|
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
|
|
|
|
# Calculate temperature compliance
|
|
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
|
|
|
|
# Get alert metrics
|
|
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)), # Same as compliance rate for now
|
|
audit_score_avg=Decimal("85.0"), # Would calculate from actual audit data
|
|
risk_score=Decimal("3.2") # Would calculate from risk assessments
|
|
)
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
# ===== Health and Status Endpoints =====
|
|
|
|
@router.get("/tenants/{tenant_id}/status")
|
|
async def get_food_safety_status(
|
|
tenant_id: UUID = Path(...),
|
|
current_tenant: str = Depends(get_current_tenant_id_dep),
|
|
current_user: dict = Depends(get_current_user_dep)
|
|
):
|
|
"""Get food safety service status"""
|
|
try:
|
|
if str(tenant_id) != current_tenant:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied to tenant data"
|
|
)
|
|
|
|
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"
|
|
) |