Files
bakery-ia/services/forecasting/app/services/forecasting_recommendation_service.py
2025-12-05 20:07:01 +01:00

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
)