Spaces:
Configuration error
Configuration error
File size: 6,893 Bytes
b143975 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | """
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"])
@router.post("/stats", response_model=KPIStatsResponse)
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")
@router.post("/stats/individual/{kpi_id}")
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}
@router.post("/rebuild", response_model=RebuildResponse)
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")
@router.post("/list")
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")
|