REFACTOR ALL APIs
This commit is contained in:
288
services/inventory/app/api/food_safety_operations.py
Normal file
288
services/inventory/app/api/food_safety_operations.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# 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["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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_analytics_route("food-safety-metrics"),
|
||||
response_model=FoodSafetyMetrics
|
||||
)
|
||||
@analytics_tier_required
|
||||
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"
|
||||
)
|
||||
Reference in New Issue
Block a user