Files
bakery-ia/services/inventory/app/api/dashboard.py

498 lines
17 KiB
Python
Raw Permalink Normal View History

2025-08-21 20:28:14 +02:00
# ================================================================
# 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
2025-10-06 15:27:01 +02:00
from shared.auth.access_control import require_user_role, analytics_tier_required
from shared.routing import RouteBuilder
2025-08-21 20:28:14 +02:00
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
)
2025-12-05 20:07:01 +01:00
from app.utils.cache import get_cached, set_cached, make_cache_key
2025-08-21 20:28:14 +02:00
logger = structlog.get_logger()
2025-10-06 15:27:01 +02:00
# Create route builder for consistent URL structure
route_builder = RouteBuilder('inventory')
2025-09-09 22:27:52 +02:00
router = APIRouter(tags=["dashboard"])
2025-08-21 20:28:14 +02:00
# ===== 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 =====
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("summary"),
response_model=InventoryDashboardSummary
)
2025-08-21 20:28:14 +02:00
async def get_inventory_dashboard_summary(
tenant_id: UUID = Path(...),
filters: Optional[DashboardFilter] = None,
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
2025-12-05 20:07:01 +01:00
"""Get comprehensive inventory dashboard summary with caching (30s TTL)"""
2025-08-21 20:28:14 +02:00
try:
2025-12-05 20:07:01 +01:00
# PHASE 2: Check cache first (only if no filters applied)
cache_key = None
if filters is None:
cache_key = make_cache_key("inventory_dashboard", str(tenant_id))
cached_result = await get_cached(cache_key)
if cached_result is not None:
logger.debug("Cache hit for inventory dashboard", cache_key=cache_key, tenant_id=str(tenant_id))
return InventoryDashboardSummary(**cached_result)
# Cache miss or filters applied - fetch from database
2025-08-21 20:28:14 +02:00
summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters)
2025-12-05 20:07:01 +01:00
# PHASE 2: Cache the result (30s TTL for inventory levels)
if cache_key:
await set_cached(cache_key, summary.model_dump(), ttl=30)
logger.debug("Cached inventory dashboard", cache_key=cache_key, ttl=30, tenant_id=str(tenant_id))
logger.info("Dashboard summary retrieved",
2025-08-21 20:28:14 +02:00
tenant_id=str(tenant_id),
total_ingredients=summary.total_ingredients)
2025-12-05 20:07:01 +01:00
2025-08-21 20:28:14 +02:00
return summary
2025-12-05 20:07:01 +01:00
2025-08-21 20:28:14 +02:00
except Exception as e:
2025-12-05 20:07:01 +01:00
logger.error("Error getting dashboard summary",
tenant_id=str(tenant_id),
2025-08-21 20:28:14 +02:00
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve dashboard summary"
)
2025-12-05 20:07:01 +01:00
@router.get(
route_builder.build_dashboard_route("overview")
)
async def get_inventory_dashboard_overview(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""
Get lightweight inventory dashboard overview for health checks.
This endpoint is optimized for frequent polling by the orchestrator service
for dashboard health-status checks. It returns only essential metrics needed
to determine inventory health status.
"""
try:
overview = await dashboard_service.get_inventory_overview(db, tenant_id)
logger.info("Inventory dashboard overview retrieved",
tenant_id=str(tenant_id),
out_of_stock_count=overview.get('out_of_stock_count', 0))
return overview
except Exception as e:
logger.error("Error getting inventory dashboard overview",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve inventory dashboard overview"
)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("food-safety"),
response_model=FoodSafetyDashboard
)
2025-08-21 20:28:14 +02:00
async def get_food_safety_dashboard(
tenant_id: UUID = Path(...),
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:
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"
)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("analytics"),
response_model=InventoryAnalytics
)
2025-10-07 07:15:07 +02:00
2025-08-21 20:28:14 +02:00
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_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get advanced inventory analytics"""
try:
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"
)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("business-model"),
response_model=BusinessModelInsights
)
2025-08-21 20:28:14 +02:00
async def get_business_model_insights(
tenant_id: UUID = Path(...),
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:
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 =====
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("stock-status"),
response_model=List[StockStatusSummary]
)
2025-08-21 20:28:14 +02:00
async def get_stock_status_by_category(
tenant_id: UUID = Path(...),
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:
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"
)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("alerts-summary"),
response_model=List[AlertSummary]
)
2025-08-21 20:28:14 +02:00
async def get_alerts_summary(
tenant_id: UUID = Path(...),
filters: Optional[AlertsFilter] = None,
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:
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"
)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("recent-activity"),
response_model=List[RecentActivity]
)
2025-08-21 20:28:14 +02:00
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_user: dict = Depends(get_current_user_dep),
dashboard_service: DashboardService = Depends(get_dashboard_service),
db: AsyncSession = Depends(get_db)
):
"""Get recent inventory activity"""
try:
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 =====
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("live-metrics")
)
2025-08-21 20:28:14 +02:00
async def get_live_metrics(
tenant_id: UUID = Path(...),
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:
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"
)
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("temperature-status")
)
2025-08-21 20:28:14 +02:00
async def get_temperature_monitoring_status(
tenant_id: UUID = Path(...),
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:
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 =====
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_dashboard_route("config")
)
2025-08-21 20:28:14 +02:00
async def get_dashboard_config(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
):
"""Get dashboard configuration and settings"""
try:
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 =====
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_operations_route("export/summary")
)
2025-08-21 20:28:14 +02:00
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_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 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 =====
2025-10-06 15:27:01 +02:00
@router.get(
route_builder.build_base_route("health")
)
2025-08-21 20:28:14 +02:00
async def get_dashboard_health(
tenant_id: UUID = Path(...),
current_user: dict = Depends(get_current_user_dep)
):
"""Get dashboard service health status"""
try:
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"
)