Add POI feature and imporve the overall backend implementation

This commit is contained in:
Urtzi Alfaro
2025-11-12 15:34:10 +01:00
parent e8096cd979
commit 5783c7ed05
173 changed files with 16862 additions and 9078 deletions

View File

@@ -880,6 +880,9 @@ class EnhancedForecastingService:
request: ForecastRequest
) -> Dict[str, Any]:
"""Prepare features with comprehensive fallbacks"""
# Check for school holidays using external service
is_holiday = await self._check_holiday(tenant_id, request.forecast_date)
features = {
"date": request.forecast_date.isoformat(),
"day_of_week": request.forecast_date.weekday(),
@@ -889,7 +892,7 @@ class EnhancedForecastingService:
"quarter": (request.forecast_date.month - 1) // 3 + 1,
"week_of_year": request.forecast_date.isocalendar().week,
"season": self._get_season(request.forecast_date.month),
"is_holiday": self._is_spanish_holiday(request.forecast_date),
"is_holiday": is_holiday,
# CRITICAL FIX: Add tenant_id and inventory_product_id for historical feature enrichment
"tenant_id": tenant_id,
"inventory_product_id": request.inventory_product_id,
@@ -977,6 +980,9 @@ class EnhancedForecastingService:
weather_map: Dict[str, Any]
) -> Dict[str, Any]:
"""Prepare features with comprehensive fallbacks using a pre-fetched weather map"""
# Check for holidays using external service
is_holiday = await self._check_holiday(tenant_id, request.forecast_date)
features = {
"date": request.forecast_date.isoformat(),
"day_of_week": request.forecast_date.weekday(),
@@ -986,7 +992,7 @@ class EnhancedForecastingService:
"quarter": (request.forecast_date.month - 1) // 3 + 1,
"week_of_year": request.forecast_date.isocalendar().week,
"season": self._get_season(request.forecast_date.month),
"is_holiday": self._is_spanish_holiday(request.forecast_date),
"is_holiday": is_holiday,
# CRITICAL FIX: Add tenant_id and inventory_product_id for historical feature enrichment
"tenant_id": tenant_id,
"inventory_product_id": request.inventory_product_id,
@@ -1041,11 +1047,44 @@ class EnhancedForecastingService:
else:
return 4 # Autumn
def _is_spanish_holiday(self, date_obj: date) -> bool:
"""Check if a date is a major Spanish holiday"""
async def _check_holiday(self, tenant_id: str, date_obj: date) -> bool:
"""
Check if a date is a holiday using external service calendar
Falls back to Spanish national holidays if:
- Tenant has no calendar configured
- External service is unavailable
"""
try:
# Get tenant's calendar ID
calendar_id = await self.data_client.get_tenant_calendar(tenant_id)
if calendar_id:
# Check school holiday via external service
is_school_holiday = await self.data_client.check_school_holiday(
calendar_id=calendar_id,
check_date=date_obj.isoformat(),
tenant_id=tenant_id
)
return is_school_holiday
else:
# Fallback to Spanish national holidays
return self._is_spanish_national_holiday(date_obj)
except Exception as e:
logger.warning(
"Holiday check failed, falling back to national holidays",
error=str(e),
tenant_id=tenant_id,
date=date_obj.isoformat()
)
return self._is_spanish_national_holiday(date_obj)
def _is_spanish_national_holiday(self, date_obj: date) -> bool:
"""Check if a date is a major Spanish national holiday (fallback)"""
month_day = (date_obj.month, date_obj.day)
spanish_holidays = [
(1, 1), (1, 6), (5, 1), (8, 15), (10, 12),
(1, 1), (1, 6), (5, 1), (8, 15), (10, 12),
(11, 1), (12, 6), (12, 8), (12, 25)
]
return month_day in spanish_holidays

View File

@@ -0,0 +1,106 @@
"""
POI Feature Service for Forecasting
Fetches POI features for use in demand forecasting predictions.
Ensures feature consistency between training and prediction.
"""
import httpx
from typing import Dict, Any, Optional
import structlog
logger = structlog.get_logger()
class POIFeatureService:
"""
POI feature service for forecasting.
Fetches POI context from External service to ensure
prediction uses the same features as training.
"""
def __init__(self, external_service_url: str = "http://external-service:8000"):
"""
Initialize POI feature service.
Args:
external_service_url: Base URL for external service
"""
self.external_service_url = external_service_url.rstrip("/")
self.poi_context_endpoint = f"{self.external_service_url}/poi-context"
async def get_poi_features(
self,
tenant_id: str
) -> Dict[str, Any]:
"""
Get POI features for tenant.
Args:
tenant_id: Tenant UUID
Returns:
Dictionary with POI features or empty dict if not available
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{self.poi_context_endpoint}/{tenant_id}"
)
if response.status_code == 404:
logger.warning(
"No POI context found for tenant",
tenant_id=tenant_id
)
return {}
response.raise_for_status()
data = response.json()
poi_context = data.get("poi_context", {})
ml_features = poi_context.get("ml_features", {})
logger.info(
"POI features retrieved for forecasting",
tenant_id=tenant_id,
feature_count=len(ml_features)
)
return ml_features
except httpx.HTTPError as e:
logger.error(
"Failed to fetch POI features for forecasting",
tenant_id=tenant_id,
error=str(e)
)
return {}
except Exception as e:
logger.error(
"Unexpected error fetching POI features",
tenant_id=tenant_id,
error=str(e),
exc_info=True
)
return {}
async def check_poi_service_health(self) -> bool:
"""
Check if POI service is accessible.
Returns:
True if service is healthy, False otherwise
"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
f"{self.poi_context_endpoint}/health"
)
return response.status_code == 200
except Exception as e:
logger.error(
"POI service health check failed",
error=str(e)
)
return False