Initial commit - production deployment
This commit is contained in:
498
services/inventory/app/api/dashboard.py
Normal file
498
services/inventory/app/api/dashboard.py
Normal file
@@ -0,0 +1,498 @@
|
||||
# ================================================================
|
||||
# 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
|
||||
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.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
|
||||
)
|
||||
from app.utils.cache import get_cached, set_cached, make_cache_key
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('inventory')
|
||||
|
||||
router = APIRouter(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(
|
||||
route_builder.build_dashboard_route("summary"),
|
||||
response_model=InventoryDashboardSummary
|
||||
)
|
||||
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)
|
||||
):
|
||||
"""Get comprehensive inventory dashboard summary with caching (30s TTL)"""
|
||||
try:
|
||||
# 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
|
||||
summary = await dashboard_service.get_inventory_dashboard_summary(db, tenant_id, filters)
|
||||
|
||||
# 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",
|
||||
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(
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("food-safety"),
|
||||
response_model=FoodSafetyDashboard
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("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_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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("business-model"),
|
||||
response_model=BusinessModelInsights
|
||||
)
|
||||
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 =====
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("stock-status"),
|
||||
response_model=List[StockStatusSummary]
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("alerts-summary"),
|
||||
response_model=List[AlertSummary]
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("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_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 =====
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("live-metrics")
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("temperature-status")
|
||||
)
|
||||
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 =====
|
||||
|
||||
@router.get(
|
||||
route_builder.build_dashboard_route("config")
|
||||
)
|
||||
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 =====
|
||||
|
||||
@router.get(
|
||||
route_builder.build_operations_route("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_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 =====
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("health")
|
||||
)
|
||||
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"
|
||||
)
|
||||
Reference in New Issue
Block a user