New alert service
This commit is contained in:
149
services/inventory/app/api/batch.py
Normal file
149
services/inventory/app/api/batch.py
Normal file
@@ -0,0 +1,149 @@
|
||||
# services/inventory/app/api/batch.py
|
||||
"""
|
||||
Inventory Batch API - Batch operations for enterprise dashboards
|
||||
|
||||
Phase 2 optimization: Eliminate N+1 query patterns by fetching inventory data
|
||||
for multiple tenants in a single request.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from typing import List, Dict, Any
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import structlog
|
||||
import asyncio
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.dashboard_service import DashboardService
|
||||
from app.services.inventory_service import InventoryService
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
|
||||
router = APIRouter(tags=["inventory-batch"])
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class InventorySummaryBatchRequest(BaseModel):
|
||||
"""Request model for batch inventory summary"""
|
||||
tenant_ids: List[str] = Field(..., description="List of tenant IDs", max_length=100)
|
||||
|
||||
|
||||
class InventorySummary(BaseModel):
|
||||
"""Inventory summary for a single tenant"""
|
||||
tenant_id: str
|
||||
total_value: float
|
||||
out_of_stock_count: int
|
||||
low_stock_count: int
|
||||
adequate_stock_count: int
|
||||
total_ingredients: int
|
||||
|
||||
|
||||
@router.post("/batch/inventory-summary", response_model=Dict[str, InventorySummary])
|
||||
async def get_inventory_summary_batch(
|
||||
request: InventorySummaryBatchRequest = Body(...),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get inventory summary for multiple tenants in a single request.
|
||||
|
||||
Optimized for enterprise dashboards to eliminate N+1 query patterns.
|
||||
Fetches inventory data for all tenants in parallel.
|
||||
|
||||
Args:
|
||||
request: Batch request with tenant IDs
|
||||
|
||||
Returns:
|
||||
Dictionary mapping tenant_id -> inventory summary
|
||||
|
||||
Example:
|
||||
POST /api/v1/inventory/batch/inventory-summary
|
||||
{
|
||||
"tenant_ids": ["tenant-1", "tenant-2", "tenant-3"]
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"tenant-1": {"tenant_id": "tenant-1", "total_value": 15000, ...},
|
||||
"tenant-2": {"tenant_id": "tenant-2", "total_value": 12000, ...},
|
||||
"tenant-3": {"tenant_id": "tenant-3", "total_value": 18000, ...}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if len(request.tenant_ids) > 100:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Maximum 100 tenant IDs allowed per batch request"
|
||||
)
|
||||
|
||||
if not request.tenant_ids:
|
||||
return {}
|
||||
|
||||
logger.info(
|
||||
"Batch fetching inventory summaries",
|
||||
tenant_count=len(request.tenant_ids)
|
||||
)
|
||||
|
||||
async def fetch_tenant_inventory(tenant_id: str) -> tuple[str, InventorySummary]:
|
||||
"""Fetch inventory summary for a single tenant"""
|
||||
try:
|
||||
tenant_uuid = UUID(tenant_id)
|
||||
dashboard_service = DashboardService(
|
||||
inventory_service=InventoryService(),
|
||||
food_safety_service=None
|
||||
)
|
||||
|
||||
overview = await dashboard_service.get_inventory_overview(db, tenant_uuid)
|
||||
|
||||
return tenant_id, InventorySummary(
|
||||
tenant_id=tenant_id,
|
||||
total_value=float(overview.get('total_value', 0)),
|
||||
out_of_stock_count=int(overview.get('out_of_stock_count', 0)),
|
||||
low_stock_count=int(overview.get('low_stock_count', 0)),
|
||||
adequate_stock_count=int(overview.get('adequate_stock_count', 0)),
|
||||
total_ingredients=int(overview.get('total_ingredients', 0))
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to fetch inventory for tenant in batch",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
return tenant_id, InventorySummary(
|
||||
tenant_id=tenant_id,
|
||||
total_value=0.0,
|
||||
out_of_stock_count=0,
|
||||
low_stock_count=0,
|
||||
adequate_stock_count=0,
|
||||
total_ingredients=0
|
||||
)
|
||||
|
||||
# Fetch all tenant inventory data in parallel
|
||||
tasks = [fetch_tenant_inventory(tid) for tid in request.tenant_ids]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Build result dictionary
|
||||
result_dict = {}
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
logger.error("Exception in batch inventory fetch", error=str(result))
|
||||
continue
|
||||
tenant_id, summary = result
|
||||
result_dict[tenant_id] = summary
|
||||
|
||||
logger.info(
|
||||
"Batch inventory summaries retrieved",
|
||||
requested_count=len(request.tenant_ids),
|
||||
successful_count=len(result_dict)
|
||||
)
|
||||
|
||||
return result_dict
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error in batch inventory summary", error=str(e), exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to fetch batch inventory summaries: {str(e)}"
|
||||
)
|
||||
@@ -30,6 +30,7 @@ from app.schemas.dashboard import (
|
||||
AlertSummary,
|
||||
RecentActivity
|
||||
)
|
||||
from app.utils.cache import get_cached, set_cached, make_cache_key
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -62,19 +63,34 @@ async def get_inventory_dashboard_summary(
|
||||
dashboard_service: DashboardService = Depends(get_dashboard_service),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get comprehensive inventory dashboard summary"""
|
||||
"""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)
|
||||
|
||||
logger.info("Dashboard summary retrieved",
|
||||
|
||||
# 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),
|
||||
logger.error("Error getting dashboard summary",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -82,6 +98,41 @@ async def get_inventory_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
|
||||
|
||||
Reference in New Issue
Block a user