246 lines
7.6 KiB
Python
246 lines
7.6 KiB
Python
"""
|
|
Forecasting Recommendation Service - Simplified
|
|
|
|
Emits minimal events using EventPublisher.
|
|
All enrichment handled by alert_processor.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional, Dict, Any, List
|
|
from uuid import UUID
|
|
import structlog
|
|
|
|
from shared.messaging import UnifiedEventPublisher
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class ForecastingRecommendationService:
|
|
"""
|
|
Service for emitting forecasting recommendations using EventPublisher.
|
|
"""
|
|
|
|
def __init__(self, event_publisher: UnifiedEventPublisher):
|
|
self.publisher = event_publisher
|
|
|
|
async def emit_demand_surge_recommendation(
|
|
self,
|
|
tenant_id: UUID,
|
|
product_sku: str,
|
|
product_name: str,
|
|
predicted_demand: float,
|
|
normal_demand: float,
|
|
surge_percentage: float,
|
|
surge_date: str,
|
|
confidence_score: float,
|
|
reasoning: str,
|
|
) -> None:
|
|
"""
|
|
Emit RECOMMENDATION for predicted demand surge.
|
|
"""
|
|
metadata = {
|
|
"product_sku": product_sku,
|
|
"product_name": product_name,
|
|
"predicted_demand": float(predicted_demand),
|
|
"normal_demand": float(normal_demand),
|
|
"surge_percentage": float(surge_percentage),
|
|
"surge_date": surge_date,
|
|
"confidence_score": float(confidence_score),
|
|
"reasoning": reasoning,
|
|
"estimated_impact": {
|
|
"additional_revenue_eur": predicted_demand * 5, # Rough estimate
|
|
"stockout_risk": "high" if surge_percentage > 50 else "medium",
|
|
},
|
|
}
|
|
|
|
await self.publisher.publish_recommendation(
|
|
event_type="demand.demand_surge_predicted",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"demand_surge_recommendation_emitted",
|
|
tenant_id=str(tenant_id),
|
|
product_name=product_name,
|
|
surge_percentage=surge_percentage
|
|
)
|
|
|
|
async def emit_weather_impact_recommendation(
|
|
self,
|
|
tenant_id: UUID,
|
|
weather_event: str, # 'rain', 'snow', 'heatwave', etc.
|
|
forecast_date: str,
|
|
affected_products: List[Dict[str, Any]],
|
|
impact_description: str,
|
|
confidence_score: float,
|
|
) -> None:
|
|
"""
|
|
Emit RECOMMENDATION for weather impact on demand.
|
|
"""
|
|
metadata = {
|
|
"weather_event": weather_event,
|
|
"forecast_date": forecast_date,
|
|
"affected_products": affected_products,
|
|
"impact_description": impact_description,
|
|
"confidence_score": float(confidence_score),
|
|
}
|
|
|
|
await self.publisher.publish_recommendation(
|
|
event_type="demand.weather_impact_forecast",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"weather_impact_recommendation_emitted",
|
|
tenant_id=str(tenant_id),
|
|
weather_event=weather_event
|
|
)
|
|
|
|
async def emit_holiday_preparation_recommendation(
|
|
self,
|
|
tenant_id: UUID,
|
|
holiday_name: str,
|
|
holiday_date: str,
|
|
days_until_holiday: int,
|
|
recommended_products: List[Dict[str, Any]],
|
|
preparation_tips: List[str],
|
|
) -> None:
|
|
"""
|
|
Emit RECOMMENDATION for holiday preparation.
|
|
"""
|
|
metadata = {
|
|
"holiday_name": holiday_name,
|
|
"holiday_date": holiday_date,
|
|
"days_until_holiday": days_until_holiday,
|
|
"recommended_products": recommended_products,
|
|
"preparation_tips": preparation_tips,
|
|
"confidence_score": 0.9, # High confidence for known holidays
|
|
}
|
|
|
|
await self.publisher.publish_recommendation(
|
|
event_type="demand.holiday_preparation",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"holiday_preparation_recommendation_emitted",
|
|
tenant_id=str(tenant_id),
|
|
holiday=holiday_name
|
|
)
|
|
|
|
async def emit_seasonal_trend_recommendation(
|
|
self,
|
|
tenant_id: UUID,
|
|
season: str, # 'spring', 'summer', 'fall', 'winter'
|
|
trend_type: str, # 'increasing', 'decreasing', 'stable'
|
|
affected_categories: List[str],
|
|
trend_description: str,
|
|
suggested_actions: List[str],
|
|
) -> None:
|
|
"""
|
|
Emit RECOMMENDATION for seasonal trend insight.
|
|
"""
|
|
metadata = {
|
|
"season": season,
|
|
"trend_type": trend_type,
|
|
"affected_categories": affected_categories,
|
|
"trend_description": trend_description,
|
|
"suggested_actions": suggested_actions,
|
|
"confidence_score": 0.85,
|
|
}
|
|
|
|
await self.publisher.publish_recommendation(
|
|
event_type="demand.seasonal_trend_insight",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"seasonal_trend_recommendation_emitted",
|
|
tenant_id=str(tenant_id),
|
|
season=season
|
|
)
|
|
|
|
async def emit_inventory_optimization_recommendation(
|
|
self,
|
|
tenant_id: UUID,
|
|
ingredient_id: str,
|
|
ingredient_name: str,
|
|
current_stock: float,
|
|
optimal_stock: float,
|
|
unit: str,
|
|
reason: str,
|
|
estimated_savings_eur: Optional[float] = None,
|
|
) -> None:
|
|
"""
|
|
Emit RECOMMENDATION for inventory optimization.
|
|
"""
|
|
difference = abs(current_stock - optimal_stock)
|
|
action = "reduce" if current_stock > optimal_stock else "increase"
|
|
|
|
estimated_impact = {}
|
|
if estimated_savings_eur:
|
|
estimated_impact["financial_savings_eur"] = estimated_savings_eur
|
|
|
|
metadata = {
|
|
"ingredient_id": ingredient_id,
|
|
"ingredient_name": ingredient_name,
|
|
"current_stock": float(current_stock),
|
|
"optimal_stock": float(optimal_stock),
|
|
"difference": float(difference),
|
|
"action": action,
|
|
"unit": unit,
|
|
"reason": reason,
|
|
"estimated_impact": estimated_impact if estimated_impact else None,
|
|
"confidence_score": 0.75,
|
|
}
|
|
|
|
await self.publisher.publish_recommendation(
|
|
event_type="inventory.inventory_optimization_opportunity",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"inventory_optimization_recommendation_emitted",
|
|
tenant_id=str(tenant_id),
|
|
ingredient_name=ingredient_name
|
|
)
|
|
|
|
async def emit_cost_reduction_recommendation(
|
|
self,
|
|
tenant_id: UUID,
|
|
opportunity_type: str, # 'supplier_switch', 'bulk_purchase', 'seasonal_buying'
|
|
title: str,
|
|
description: str,
|
|
estimated_savings_eur: float,
|
|
suggested_actions: List[str],
|
|
details: Dict[str, Any],
|
|
) -> None:
|
|
"""
|
|
Emit RECOMMENDATION for cost reduction opportunity.
|
|
"""
|
|
metadata = {
|
|
"opportunity_type": opportunity_type,
|
|
"title": title,
|
|
"description": description,
|
|
"estimated_savings_eur": float(estimated_savings_eur),
|
|
"suggested_actions": suggested_actions,
|
|
"details": details,
|
|
"confidence_score": 0.8,
|
|
}
|
|
|
|
await self.publisher.publish_recommendation(
|
|
event_type="supply_chain.cost_reduction_suggestion",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"cost_reduction_recommendation_emitted",
|
|
tenant_id=str(tenant_id),
|
|
opportunity_type=opportunity_type
|
|
) |