New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View 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)}"
)

View File

@@ -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