diff --git a/frontend/src/api/hooks/useForecast.ts b/frontend/src/api/hooks/useForecast.ts index 3696f832..ca82c5b8 100644 --- a/frontend/src/api/hooks/useForecast.ts +++ b/frontend/src/api/hooks/useForecast.ts @@ -120,15 +120,31 @@ export const useForecast = () => { } }, []); - const getForecastAlerts = useCallback(async (tenantId: string): Promise => { + const getForecastAlerts = useCallback(async (tenantId: string): Promise => { try { setIsLoading(true); setError(null); const response = await forecastingService.getForecastAlerts(tenantId); - setAlerts(response.data); - return response.data; + // Handle different response formats + if (response && response.alerts) { + // New format: { alerts: [...], total_returned: N, ... } + setAlerts(response.alerts); + return response; + } else if (response && response.data) { + // Old format: { data: [...] } + setAlerts(response.data); + return { alerts: response.data }; + } else if (Array.isArray(response)) { + // Direct array format + setAlerts(response); + return { alerts: response }; + } else { + // Unknown format - return empty + setAlerts([]); + return { alerts: [] }; + } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to get forecast alerts'; setError(message); diff --git a/frontend/src/hooks/useRealAlerts.ts b/frontend/src/hooks/useRealAlerts.ts index b5dd06bf..09cbda1d 100644 --- a/frontend/src/hooks/useRealAlerts.ts +++ b/frontend/src/hooks/useRealAlerts.ts @@ -79,7 +79,10 @@ export const useRealAlerts = () => { try { // Get forecast alerts from backend - const forecastAlerts = await getForecastAlerts(tenantId); + const response = await getForecastAlerts(tenantId); + + // Extract alerts array from paginated response + const forecastAlerts = response.alerts || []; // Filter only active alerts const activeAlerts = forecastAlerts.filter(alert => alert.is_active); diff --git a/gateway/app/routes/tenant.py b/gateway/app/routes/tenant.py index 4ef07848..fda49ac4 100644 --- a/gateway/app/routes/tenant.py +++ b/gateway/app/routes/tenant.py @@ -121,7 +121,7 @@ async def proxy_tenant_models(request: Request, tenant_id: str = Path(...), path async def proxy_tenant_forecasts(request: Request, tenant_id: str = Path(...), path: str = ""): """Proxy tenant forecast requests to forecasting service""" target_path = f"/api/v1/tenants/{tenant_id}/forecasts/{path}".rstrip("/") - return await _proxy_to_forecasting_service(request, target_path) + return await _proxy_to_forecasting_service(request, target_path, tenant_id=tenant_id) @router.api_route("/{tenant_id}/predictions/{path:path}", methods=["GET", "POST", "OPTIONS"]) async def proxy_tenant_predictions(request: Request, tenant_id: str = Path(...), path: str = ""): @@ -149,7 +149,7 @@ async def proxy_tenant_inventory(request: Request, tenant_id: str = Path(...), p # The inventory service expects /api/v1/tenants/{tenant_id}/inventory/{path} # Keep the full path structure for inventory service target_path = f"/api/v1/tenants/{tenant_id}/inventory/{path}".rstrip("/") - return await _proxy_to_inventory_service(request, target_path) + return await _proxy_to_inventory_service(request, target_path, tenant_id=tenant_id) @router.api_route("/{tenant_id}/ingredients{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) async def proxy_tenant_ingredients(request: Request, tenant_id: str = Path(...), path: str = ""): @@ -157,7 +157,7 @@ async def proxy_tenant_ingredients(request: Request, tenant_id: str = Path(...), # The inventory service ingredient endpoints are now tenant-scoped: /api/v1/tenants/{tenant_id}/ingredients/{path} # Keep the full tenant path structure target_path = f"/api/v1/tenants/{tenant_id}/ingredients{path}".rstrip("/") - return await _proxy_to_inventory_service(request, target_path) + return await _proxy_to_inventory_service(request, target_path, tenant_id=tenant_id) # ================================================================ # PROXY HELPER FUNCTIONS @@ -179,19 +179,19 @@ async def _proxy_to_training_service(request: Request, target_path: str): """Proxy request to training service""" return await _proxy_request(request, target_path, settings.TRAINING_SERVICE_URL) -async def _proxy_to_forecasting_service(request: Request, target_path: str): +async def _proxy_to_forecasting_service(request: Request, target_path: str, tenant_id: str = None): """Proxy request to forecasting service""" - return await _proxy_request(request, target_path, settings.FORECASTING_SERVICE_URL) + return await _proxy_request(request, target_path, settings.FORECASTING_SERVICE_URL, tenant_id=tenant_id) async def _proxy_to_notification_service(request: Request, target_path: str): """Proxy request to notification service""" return await _proxy_request(request, target_path, settings.NOTIFICATION_SERVICE_URL) -async def _proxy_to_inventory_service(request: Request, target_path: str): +async def _proxy_to_inventory_service(request: Request, target_path: str, tenant_id: str = None): """Proxy request to inventory service""" - return await _proxy_request(request, target_path, settings.INVENTORY_SERVICE_URL) + return await _proxy_request(request, target_path, settings.INVENTORY_SERVICE_URL, tenant_id=tenant_id) -async def _proxy_request(request: Request, target_path: str, service_url: str): +async def _proxy_request(request: Request, target_path: str, service_url: str, tenant_id: str = None): """Generic proxy function with enhanced error handling""" # Handle OPTIONS requests directly for CORS @@ -214,6 +214,10 @@ async def _proxy_request(request: Request, target_path: str, service_url: str): headers = dict(request.headers) headers.pop("host", None) + # Add tenant ID header if provided + if tenant_id: + headers["X-Tenant-ID"] = tenant_id + # Get request body if present body = None if request.method in ["POST", "PUT", "PATCH"]: diff --git a/services/forecasting/app/api/forecasts.py b/services/forecasting/app/api/forecasts.py index f8508e2c..bee93c6a 100644 --- a/services/forecasting/app/api/forecasts.py +++ b/services/forecasting/app/api/forecasts.py @@ -242,6 +242,70 @@ async def get_enhanced_tenant_forecasts( ) +@router.get("/tenants/{tenant_id}/forecasts/alerts") +@track_execution_time("enhanced_get_alerts_duration_seconds", "forecasting-service") +async def get_enhanced_forecast_alerts( + tenant_id: str = Path(..., description="Tenant ID"), + active_only: bool = Query(True, description="Return only active alerts"), + skip: int = Query(0, description="Number of records to skip"), + limit: int = Query(50, description="Number of records to return"), + request_obj: Request = None, + current_tenant: str = Depends(get_current_tenant_id_dep), + enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) +): + """Get forecast alerts using enhanced repository pattern""" + metrics = get_metrics_collector(request_obj) + + try: + # Enhanced tenant validation + if tenant_id != current_tenant: + if metrics: + metrics.increment_counter("enhanced_get_alerts_access_denied_total") + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied to tenant resources" + ) + + # Record metrics + if metrics: + metrics.increment_counter("enhanced_get_alerts_total") + + # Get alerts using enhanced service + alerts = await enhanced_forecasting_service.get_tenant_alerts( + tenant_id=tenant_id, + active_only=active_only, + skip=skip, + limit=limit + ) + + if metrics: + metrics.increment_counter("enhanced_get_alerts_success_total") + + return { + "tenant_id": tenant_id, + "alerts": alerts, + "total_returned": len(alerts), + "active_only": active_only, + "pagination": { + "skip": skip, + "limit": limit + }, + "enhanced_features": True, + "repository_integration": True + } + + except Exception as e: + if metrics: + metrics.increment_counter("enhanced_get_alerts_errors_total") + logger.error("Failed to get enhanced forecast alerts", + tenant_id=tenant_id, + error=str(e)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to get forecast alerts" + ) + + @router.get("/tenants/{tenant_id}/forecasts/{forecast_id}") @track_execution_time("enhanced_get_forecast_duration_seconds", "forecasting-service") async def get_enhanced_forecast_by_id( @@ -363,70 +427,6 @@ async def delete_enhanced_forecast( ) -@router.get("/tenants/{tenant_id}/forecasts/alerts") -@track_execution_time("enhanced_get_alerts_duration_seconds", "forecasting-service") -async def get_enhanced_forecast_alerts( - tenant_id: str = Path(..., description="Tenant ID"), - active_only: bool = Query(True, description="Return only active alerts"), - skip: int = Query(0, description="Number of records to skip"), - limit: int = Query(50, description="Number of records to return"), - request_obj: Request = None, - current_tenant: str = Depends(get_current_tenant_id_dep), - enhanced_forecasting_service: EnhancedForecastingService = Depends(get_enhanced_forecasting_service) -): - """Get forecast alerts using enhanced repository pattern""" - metrics = get_metrics_collector(request_obj) - - try: - # Enhanced tenant validation - if tenant_id != current_tenant: - if metrics: - metrics.increment_counter("enhanced_get_alerts_access_denied_total") - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Access denied to tenant resources" - ) - - # Record metrics - if metrics: - metrics.increment_counter("enhanced_get_alerts_total") - - # Get alerts using enhanced service - alerts = await enhanced_forecasting_service.get_tenant_alerts( - tenant_id=tenant_id, - active_only=active_only, - skip=skip, - limit=limit - ) - - if metrics: - metrics.increment_counter("enhanced_get_alerts_success_total") - - return { - "tenant_id": tenant_id, - "alerts": alerts, - "total_returned": len(alerts), - "active_only": active_only, - "pagination": { - "skip": skip, - "limit": limit - }, - "enhanced_features": True, - "repository_integration": True - } - - except Exception as e: - if metrics: - metrics.increment_counter("enhanced_get_alerts_errors_total") - logger.error("Failed to get enhanced forecast alerts", - tenant_id=tenant_id, - error=str(e)) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to get forecast alerts" - ) - - @router.get("/tenants/{tenant_id}/forecasts/statistics") @track_execution_time("enhanced_forecast_statistics_duration_seconds", "forecasting-service") async def get_enhanced_forecast_statistics( diff --git a/services/forecasting/app/services/forecasting_service.py b/services/forecasting/app/services/forecasting_service.py index 034c566b..12c5cfab 100644 --- a/services/forecasting/app/services/forecasting_service.py +++ b/services/forecasting/app/services/forecasting_service.py @@ -83,10 +83,68 @@ class EnhancedForecastingService: skip: int = 0, limit: int = 100) -> List[Dict]: """Get tenant forecasts with filtering""" try: - # Implementation would use repository pattern to fetch forecasts - return [] + # Get session and initialize repositories + async with self.database_manager.get_background_session() as session: + repos = await self._init_repositories(session) + + # Build filters + filters = {"tenant_id": tenant_id} + if inventory_product_id: + filters["inventory_product_id"] = inventory_product_id + + # If date range specified, use specialized method + if start_date and end_date: + forecasts = await repos['forecast'].get_forecasts_by_date_range( + tenant_id=tenant_id, + start_date=start_date, + end_date=end_date, + inventory_product_id=inventory_product_id + ) + else: + # Use general get_multi with tenant filter + forecasts = await repos['forecast'].get_multi( + filters=filters, + skip=skip, + limit=limit, + order_by="forecast_date", + order_desc=True + ) + + # Convert to dict format + forecast_list = [] + for forecast in forecasts: + forecast_dict = { + "id": str(forecast.id), + "tenant_id": str(forecast.tenant_id), + "inventory_product_id": forecast.inventory_product_id, + "location": forecast.location, + "forecast_date": forecast.forecast_date.isoformat(), + "predicted_demand": float(forecast.predicted_demand), + "confidence_lower": float(forecast.confidence_lower), + "confidence_upper": float(forecast.confidence_upper), + "confidence_level": float(forecast.confidence_level), + "model_id": forecast.model_id, + "model_version": forecast.model_version, + "algorithm": forecast.algorithm, + "business_type": forecast.business_type, + "is_holiday": forecast.is_holiday, + "is_weekend": forecast.is_weekend, + "processing_time_ms": forecast.processing_time_ms, + "created_at": forecast.created_at.isoformat() if forecast.created_at else None + } + forecast_list.append(forecast_dict) + + logger.info("Retrieved tenant forecasts", + tenant_id=tenant_id, + count=len(forecast_list), + filters=filters) + + return forecast_list + except Exception as e: - logger.error("Failed to get tenant forecasts", error=str(e)) + logger.error("Failed to get tenant forecasts", + tenant_id=tenant_id, + error=str(e)) raise async def get_forecast_by_id(self, forecast_id: str) -> Optional[Dict]: @@ -199,7 +257,7 @@ class EnhancedForecastingService: date=request.forecast_date.isoformat()) # Get session and initialize repositories - async with self.database_manager.get_session() as session: + async with self.database_manager.get_background_session() as session: repos = await self._init_repositories(session) # Step 1: Check cache first