Add more services
This commit is contained in:
494
services/inventory/app/api/dashboard.py
Normal file
494
services/inventory/app/api/dashboard.py
Normal file
@@ -0,0 +1,494 @@
|
||||
# ================================================================
|
||||
# services/inventory/app/api/dashboard.py
|
||||
# ================================================================
|
||||
"""
|
||||
Dashboard 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.inventory_service import InventoryService
|
||||
from app.services.food_safety_service import FoodSafetyService
|
||||
from app.services.dashboard_service import DashboardService
|
||||
from app.schemas.dashboard import (
|
||||
InventoryDashboardSummary,
|
||||
FoodSafetyDashboard,
|
||||
BusinessModelInsights,
|
||||
InventoryAnalytics,
|
||||
DashboardFilter,
|
||||
AlertsFilter,
|
||||
StockStatusSummary,
|
||||
AlertSummary,
|
||||
RecentActivity
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
# ===== Dependency Injection =====
|
||||
|
||||
async def get_dashboard_service(db: AsyncSession = Depends(get_db)) -> DashboardService:
|
||||
"""Get dashboard service with dependencies"""
|
||||
return DashboardService(
|
||||
inventory_service=InventoryService(),
|
||||
food_safety_service=FoodSafetyService()
|
||||
)
|
||||
|
||||
|
||||
# ===== Main Dashboard Endpoints =====
|
||||
|
||||
@router.get("/tenants/{tenant_id}/summary", response_model=InventoryDashboardSummary)
|
||||
async def get_inventory_dashboard_summary(
|
||||
tenant_id: UUID = Path(...),
|
||||
filters: Optional[DashboardFilter] = None,
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get comprehensive inventory dashboard summary"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters)
|
||||
|
||||
logger.info("Dashboard summary retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
total_ingredients=summary.total_ingredients)
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting dashboard summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve dashboard summary"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/food-safety", response_model=FoodSafetyDashboard)
|
||||
async def get_food_safety_dashboard(
|
||||
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(lambda: FoodSafetyService()),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get food safety dashboard data"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
dashboard = await food_safety_service.get_food_safety_dashboard(db, tenant_id)
|
||||
|
||||
logger.info("Food safety dashboard retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
compliance_percentage=dashboard.compliance_percentage)
|
||||
|
||||
return dashboard
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting food safety dashboard",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve food safety dashboard"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/analytics", response_model=InventoryAnalytics)
|
||||
async def get_inventory_analytics(
|
||||
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),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get advanced inventory analytics"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
analytics = await dashboard_service.get_inventory_analytics(db, tenant_id, days_back)
|
||||
|
||||
logger.info("Inventory analytics retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
days_analyzed=days_back)
|
||||
|
||||
return analytics
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting inventory analytics",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve inventory analytics"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/business-model", response_model=BusinessModelInsights)
|
||||
async def get_business_model_insights(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get business model insights based on inventory patterns"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
insights = await dashboard_service.get_business_model_insights(db, tenant_id)
|
||||
|
||||
logger.info("Business model insights retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
detected_model=insights.detected_model)
|
||||
|
||||
return insights
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting business model insights",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve business model insights"
|
||||
)
|
||||
|
||||
|
||||
# ===== Detailed Dashboard Data Endpoints =====
|
||||
|
||||
@router.get("/tenants/{tenant_id}/stock-status", response_model=List[StockStatusSummary])
|
||||
async def get_stock_status_by_category(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get stock status breakdown by category"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
stock_status = await dashboard_service.get_stock_status_by_category(db, tenant_id)
|
||||
|
||||
return stock_status
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting stock status by category",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve stock status by category"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/alerts-summary", response_model=List[AlertSummary])
|
||||
async def get_alerts_summary(
|
||||
tenant_id: UUID = Path(...),
|
||||
filters: Optional[AlertsFilter] = None,
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get alerts summary by type and severity"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
alerts_summary = await dashboard_service.get_alerts_summary(db, tenant_id, filters)
|
||||
|
||||
return alerts_summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting alerts summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve alerts summary"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/recent-activity", response_model=List[RecentActivity])
|
||||
async def get_recent_activity(
|
||||
tenant_id: UUID = Path(...),
|
||||
limit: int = Query(20, ge=1, le=100, description="Number of activities to return"),
|
||||
activity_types: Optional[List[str]] = Query(None, description="Filter by activity types"),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get recent inventory activity"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
activities = await dashboard_service.get_recent_activity(
|
||||
db, tenant_id, limit, activity_types
|
||||
)
|
||||
|
||||
return activities
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting recent activity",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve recent activity"
|
||||
)
|
||||
|
||||
|
||||
# ===== Real-time Data Endpoints =====
|
||||
|
||||
@router.get("/tenants/{tenant_id}/live-metrics")
|
||||
async def get_live_metrics(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get real-time inventory metrics"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
metrics = await dashboard_service.get_live_metrics(db, tenant_id)
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"metrics": metrics,
|
||||
"cache_ttl": 60 # Seconds
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting live metrics",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve live metrics"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/temperature-status")
|
||||
async def get_temperature_monitoring_status(
|
||||
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(lambda: FoodSafetyService()),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get current temperature monitoring status"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
temp_status = await food_safety_service.get_temperature_monitoring_status(db, tenant_id)
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"temperature_monitoring": temp_status
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting temperature status",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve temperature monitoring status"
|
||||
)
|
||||
|
||||
|
||||
# ===== Dashboard Configuration Endpoints =====
|
||||
|
||||
@router.get("/tenants/{tenant_id}/config")
|
||||
async def get_dashboard_config(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep)
|
||||
):
|
||||
"""Get dashboard configuration and settings"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
config = {
|
||||
"refresh_intervals": {
|
||||
"dashboard_cache_ttl": settings.DASHBOARD_CACHE_TTL,
|
||||
"alerts_refresh_interval": settings.ALERTS_REFRESH_INTERVAL,
|
||||
"temperature_log_interval": settings.TEMPERATURE_LOG_INTERVAL
|
||||
},
|
||||
"features": {
|
||||
"food_safety_enabled": settings.FOOD_SAFETY_ENABLED,
|
||||
"temperature_monitoring_enabled": settings.TEMPERATURE_MONITORING_ENABLED,
|
||||
"business_model_detection": settings.ENABLE_BUSINESS_MODEL_DETECTION
|
||||
},
|
||||
"thresholds": {
|
||||
"low_stock_default": settings.DEFAULT_LOW_STOCK_THRESHOLD,
|
||||
"reorder_point_default": settings.DEFAULT_REORDER_POINT,
|
||||
"expiration_warning_days": settings.EXPIRATION_WARNING_DAYS,
|
||||
"critical_expiration_hours": settings.CRITICAL_EXPIRATION_HOURS
|
||||
},
|
||||
"business_model_thresholds": {
|
||||
"central_bakery_ingredients": settings.CENTRAL_BAKERY_THRESHOLD_INGREDIENTS,
|
||||
"individual_bakery_ingredients": settings.INDIVIDUAL_BAKERY_THRESHOLD_INGREDIENTS
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting dashboard config",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve dashboard configuration"
|
||||
)
|
||||
|
||||
|
||||
# ===== Export and Reporting Endpoints =====
|
||||
|
||||
@router.get("/tenants/{tenant_id}/export/summary")
|
||||
async def export_dashboard_summary(
|
||||
tenant_id: UUID = Path(...),
|
||||
format: str = Query("json", description="Export format: json, csv, excel"),
|
||||
date_from: Optional[datetime] = Query(None, description="Start date for data export"),
|
||||
date_to: Optional[datetime] = Query(None, description="End date for data export"),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Export dashboard summary data"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
if format.lower() not in ["json", "csv", "excel"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unsupported export format. Use: json, csv, excel"
|
||||
)
|
||||
|
||||
export_data = await dashboard_service.export_dashboard_data(
|
||||
db, tenant_id, format, date_from, date_to
|
||||
)
|
||||
|
||||
logger.info("Dashboard data exported",
|
||||
tenant_id=str(tenant_id),
|
||||
format=format)
|
||||
|
||||
return export_data
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error exporting dashboard data",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to export dashboard data"
|
||||
)
|
||||
|
||||
|
||||
# ===== Health and Status Endpoints =====
|
||||
|
||||
@router.get("/tenants/{tenant_id}/health")
|
||||
async def get_dashboard_health(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_tenant: str = Depends(get_current_tenant_id_dep),
|
||||
current_user: dict = Depends(get_current_user_dep)
|
||||
):
|
||||
"""Get dashboard service health status"""
|
||||
try:
|
||||
if str(tenant_id) != current_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied to tenant data"
|
||||
)
|
||||
|
||||
return {
|
||||
"service": "inventory-dashboard",
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"tenant_id": str(tenant_id),
|
||||
"features": {
|
||||
"food_safety": "enabled",
|
||||
"temperature_monitoring": "enabled",
|
||||
"business_model_detection": "enabled",
|
||||
"real_time_alerts": "enabled"
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting dashboard health",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get dashboard health status"
|
||||
)
|
||||
718
services/inventory/app/api/food_safety.py
Normal file
718
services/inventory/app/api/food_safety.py
Normal file
@@ -0,0 +1,718 @@
|
||||
# ================================================================
|
||||
# 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"
|
||||
)
|
||||
Reference in New Issue
Block a user