Files
bakery-ia/services/sales/app/api/batch.py

161 lines
5.3 KiB
Python
Raw Normal View History

2025-12-05 20:07:01 +01:00
# services/sales/app/api/batch.py
"""
Sales Batch API - Batch operations for enterprise dashboards
Phase 2 optimization: Eliminate N+1 query patterns by fetching data for
multiple tenants in a single request.
"""
from fastapi import APIRouter, Depends, HTTPException, Body, Path
from typing import List, Dict, Any
from datetime import date
from uuid import UUID
from pydantic import BaseModel, Field
import structlog
import asyncio
from app.services.sales_service import SalesService
from shared.auth.decorators import get_current_user_dep
from shared.routing import RouteBuilder
from shared.auth.access_control import require_user_role
route_builder = RouteBuilder('sales')
router = APIRouter(tags=["sales-batch"])
logger = structlog.get_logger()
def get_sales_service():
"""Dependency injection for SalesService"""
return SalesService()
class SalesSummaryBatchRequest(BaseModel):
"""Request model for batch sales summary"""
tenant_ids: List[str] = Field(..., description="List of tenant IDs", max_length=100)
start_date: date = Field(..., description="Start date for sales period")
end_date: date = Field(..., description="End date for sales period")
class SalesSummary(BaseModel):
"""Sales summary for a single tenant"""
tenant_id: str
total_revenue: float
total_orders: int
average_order_value: float
period_start: str
period_end: str
@router.post("/api/v1/batch/sales-summary", response_model=Dict[str, SalesSummary])
async def get_sales_summary_batch(
request: SalesSummaryBatchRequest = Body(...),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
sales_service: SalesService = Depends(get_sales_service)
):
"""
Get sales summary for multiple tenants in a single request.
Optimized for enterprise dashboards to eliminate N+1 query patterns.
Fetches sales data for all tenants in parallel.
Args:
request: Batch request with tenant IDs and date range
Returns:
Dictionary mapping tenant_id -> sales summary
Example:
POST /api/v1/sales/batch/sales-summary
{
"tenant_ids": ["tenant-1", "tenant-2", "tenant-3"],
"start_date": "2025-01-01",
"end_date": "2025-01-31"
}
Response:
{
"tenant-1": {"tenant_id": "tenant-1", "total_revenue": 50000, ...},
"tenant-2": {"tenant_id": "tenant-2", "total_revenue": 45000", ...},
"tenant-3": {"tenant_id": "tenant-3", "total_revenue": 52000, ...}
}
"""
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 sales summaries",
tenant_count=len(request.tenant_ids),
start_date=str(request.start_date),
end_date=str(request.end_date)
)
async def fetch_tenant_sales(tenant_id: str) -> tuple[str, SalesSummary]:
"""Fetch sales summary for a single tenant"""
try:
tenant_uuid = UUID(tenant_id)
summary = await sales_service.get_sales_analytics(
tenant_uuid,
request.start_date,
request.end_date
)
return tenant_id, SalesSummary(
tenant_id=tenant_id,
total_revenue=float(summary.get('total_revenue', 0)),
total_orders=int(summary.get('total_orders', 0)),
average_order_value=float(summary.get('average_order_value', 0)),
period_start=str(request.start_date),
period_end=str(request.end_date)
)
except Exception as e:
logger.warning(
"Failed to fetch sales for tenant in batch",
tenant_id=tenant_id,
error=str(e)
)
return tenant_id, SalesSummary(
tenant_id=tenant_id,
total_revenue=0.0,
total_orders=0,
average_order_value=0.0,
period_start=str(request.start_date),
period_end=str(request.end_date)
)
# Fetch all tenant sales in parallel
tasks = [fetch_tenant_sales(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 sales fetch", error=str(result))
continue
tenant_id, summary = result
result_dict[tenant_id] = summary
logger.info(
"Batch sales 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 sales summary", error=str(e), exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Failed to fetch batch sales summaries: {str(e)}"
)