Spaces:
Configuration error
Configuration error
| """ | |
| KPI Cache router. | |
| POST /kpi-cache/stats — bulk KPI fetch | |
| POST /kpi-cache/stats/individual/{kpi_id} — single KPI (404 on unknown, 403 on no widget access) | |
| POST /kpi-cache/rebuild — force recompute | |
| POST /kpi-cache/list — list cache snapshots (projection supported) | |
| Auth rules (mirrors SCM-ms widget_router.py): | |
| - merchant_id is ALWAYS sourced from JWT — never from request body | |
| - Bulk stats / rebuild / list: require permissions.dashboard.view | |
| - Individual KPI: additionally checks scm_access_roles.widget_access[] for that widget_id | |
| """ | |
| from datetime import datetime, timezone | |
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from app.core.logging import get_logger | |
| from app.dependencies.auth import get_current_user, TokenUser | |
| from app.dependencies.kpi_permissions import require_dashboard_view, check_widget_access | |
| from app.kpi_handlers.registry import KPI_HANDLER_REGISTRY | |
| from app.kpi_cache.schemas.schema import ( | |
| KPIStatsRequest, | |
| KPIStatsResponse, | |
| KPIIndividualRequest, | |
| KPIRebuildRequest, | |
| RebuildResponse, | |
| KPICacheListRequest, | |
| ) | |
| from app.kpi_cache.services.service import KPICacheService | |
| logger = get_logger(__name__) | |
| router = APIRouter(prefix="/kpi-cache", tags=["KPI Cache"]) | |
| async def get_kpi_stats( | |
| payload: KPIStatsRequest, | |
| current_user: TokenUser = Depends(require_dashboard_view), | |
| ): | |
| """ | |
| Return KPI widget values for the authenticated merchant. | |
| merchant_id is taken from JWT — not from the request body. | |
| Requires: permissions.dashboard.view | |
| Partial failures return {"error": ..., "value": null} per KPI — never HTTP 500. | |
| """ | |
| branch_id = payload.branch_id or "all" | |
| try: | |
| kpis = await KPICacheService.get_kpi_stats( | |
| merchant_id=current_user.merchant_id, | |
| period_window=payload.period_window, | |
| branch_id=branch_id, | |
| kpi_ids=payload.kpi_ids, | |
| use_cache=payload.use_cache, | |
| ) | |
| logger.info( | |
| "KPI stats retrieved", | |
| extra={ | |
| "event": "kpi_stats_retrieved", | |
| "user_id": current_user.user_id, | |
| "merchant_id": current_user.merchant_id, | |
| "period_window": payload.period_window, | |
| "kpi_count": len(kpis), | |
| }, | |
| ) | |
| return KPIStatsResponse( | |
| success=True, | |
| merchant_id=current_user.merchant_id, | |
| period_window=payload.period_window, | |
| branch_id=branch_id, | |
| kpis=kpis, | |
| generated_at=datetime.now(timezone.utc).isoformat(), | |
| ) | |
| except Exception as exc: | |
| logger.error("get_kpi_stats failed", | |
| extra={"event": "kpi_stats_error", "error": str(exc)}, exc_info=True) | |
| raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Failed to fetch KPI stats") | |
| async def get_individual_kpi( | |
| kpi_id: str, | |
| payload: KPIIndividualRequest, | |
| current_user: TokenUser = Depends(require_dashboard_view), | |
| ): | |
| """ | |
| Refresh a single KPI widget. | |
| Requires: permissions.dashboard.view + widget_id in scm_access_roles.widget_access[] | |
| Returns HTTP 404 with available_kpis list when kpi_id is unknown. | |
| use_cache=false bypasses cache read but writes fresh result back to Redis. | |
| """ | |
| # 404 on unknown widget_id (exact match, not fuzzy) | |
| if kpi_id not in KPI_HANDLER_REGISTRY: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail={ | |
| "error": "KPI not found", | |
| "available_kpis": list(KPI_HANDLER_REGISTRY.keys()), | |
| }, | |
| ) | |
| # Widget-level access control | |
| await check_widget_access(kpi_id, current_user) | |
| branch_id = payload.branch_id or "all" | |
| result = await KPICacheService.get_individual_kpi( | |
| widget_id=kpi_id, | |
| merchant_id=current_user.merchant_id, | |
| period_window=payload.period_window, | |
| branch_id=branch_id, | |
| use_cache=payload.use_cache, | |
| ) | |
| logger.info( | |
| "Individual KPI retrieved", | |
| extra={ | |
| "event": "kpi_individual_retrieved", | |
| "kpi_id": kpi_id, | |
| "user_id": current_user.user_id, | |
| "merchant_id": current_user.merchant_id, | |
| }, | |
| ) | |
| return {"success": True, "merchant_id": current_user.merchant_id, "kpi": result} | |
| async def rebuild_kpi_cache( | |
| payload: KPIRebuildRequest, | |
| current_user: TokenUser = Depends(require_dashboard_view), | |
| ): | |
| """ | |
| Force recompute KPI cache for the authenticated merchant. | |
| Requires: permissions.dashboard.view | |
| """ | |
| try: | |
| result = await KPICacheService.rebuild( | |
| merchant_id=current_user.merchant_id, | |
| period_window=payload.period_window, | |
| branch_id=payload.branch_id, | |
| kpi_ids=payload.kpi_ids, | |
| ) | |
| return RebuildResponse( | |
| success=True, | |
| message="KPI cache rebuilt successfully", | |
| merchant_id=current_user.merchant_id, | |
| rebuilt_count=result["rebuilt"], | |
| failed_count=result["failed"], | |
| details=result.get("errors") or None, | |
| ) | |
| except Exception as exc: | |
| logger.error("rebuild_kpi_cache failed", | |
| extra={"event": "kpi_rebuild_error", "error": str(exc)}, exc_info=True) | |
| raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Failed to rebuild KPI cache") | |
| async def list_kpi_cache( | |
| payload: KPICacheListRequest, | |
| current_user: TokenUser = Depends(require_dashboard_view), | |
| ): | |
| """ | |
| List KPI cache snapshots with optional projection_list. | |
| merchant_id filter is always enforced from JWT — cannot be overridden. | |
| Requires: permissions.dashboard.view | |
| """ | |
| filters = payload.filters.model_dump() if payload.filters else {} | |
| # Always scope to the authenticated merchant — ignore any merchant_id in filters | |
| filters["merchant_id"] = current_user.merchant_id | |
| try: | |
| docs = await KPICacheService.list_cache( | |
| filters=filters, | |
| skip=payload.skip, | |
| limit=payload.limit, | |
| projection_list=payload.projection_list, | |
| ) | |
| return {"success": True, "data": docs, "count": len(docs)} | |
| except Exception as exc: | |
| logger.error("list_kpi_cache failed", | |
| extra={"event": "kpi_list_error", "error": str(exc)}, exc_info=True) | |
| raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="Failed to list KPI cache") | |