Files
bakery-ia/services/inventory/app/services/inventory_alert_service.py
2025-12-13 23:57:54 +01:00

377 lines
11 KiB
Python

"""
Inventory Alert Service - Simplified
Emits minimal events using EventPublisher.
All enrichment handled by alert_processor.
"""
import asyncio
from typing import List, Dict, Any, Optional
from uuid import UUID
from datetime import datetime
import structlog
from shared.messaging import UnifiedEventPublisher, EVENT_TYPES
logger = structlog.get_logger()
class InventoryAlertService:
"""Simplified inventory alert service using EventPublisher"""
def __init__(self, event_publisher: UnifiedEventPublisher):
self.publisher = event_publisher
async def start(self):
"""Start the inventory alert service"""
logger.info("InventoryAlertService started")
# Add any initialization logic here if needed
async def stop(self):
"""Stop the inventory alert service"""
logger.info("InventoryAlertService stopped")
# Add any cleanup logic here if needed
async def emit_critical_stock_shortage(
self,
tenant_id: UUID,
ingredient_id: UUID,
ingredient_name: str,
current_stock: float,
required_stock: float,
shortage_amount: float,
minimum_stock: float,
tomorrow_needed: Optional[float] = None,
supplier_name: Optional[str] = None,
supplier_phone: Optional[str] = None,
lead_time_days: Optional[int] = None,
hours_until_stockout: Optional[int] = None
):
"""Emit minimal critical stock shortage event"""
metadata = {
"ingredient_id": str(ingredient_id),
"ingredient_name": ingredient_name,
"current_stock": current_stock,
"required_stock": required_stock,
"shortage_amount": shortage_amount,
"minimum_stock": minimum_stock
}
# Add optional fields if present
if tomorrow_needed:
metadata["tomorrow_needed"] = tomorrow_needed
if supplier_name:
metadata["supplier_name"] = supplier_name
if supplier_phone:
metadata["supplier_contact"] = supplier_phone
if lead_time_days:
metadata["lead_time_days"] = lead_time_days
if hours_until_stockout:
metadata["hours_until"] = hours_until_stockout
await self.publisher.publish_alert(
event_type="inventory.critical_stock_shortage",
tenant_id=tenant_id,
severity="urgent",
data=metadata
)
logger.info(
"critical_stock_shortage_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
shortage_amount=shortage_amount
)
async def emit_low_stock_warning(
self,
tenant_id: UUID,
ingredient_id: UUID,
ingredient_name: str,
current_stock: float,
minimum_stock: float,
supplier_name: Optional[str] = None,
supplier_phone: Optional[str] = None
):
"""Emit low stock warning event"""
metadata = {
"ingredient_id": str(ingredient_id),
"ingredient_name": ingredient_name,
"current_stock": current_stock,
"minimum_stock": minimum_stock
}
if supplier_name:
metadata["supplier_name"] = supplier_name
if supplier_phone:
metadata["supplier_contact"] = supplier_phone
await self.publisher.publish_alert(
event_type="inventory.low_stock_warning",
tenant_id=tenant_id,
severity="high",
data=metadata
)
logger.info(
"low_stock_warning_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name
)
async def emit_temperature_breach(
self,
tenant_id: UUID,
sensor_id: str,
location: str,
temperature: float,
max_threshold: float,
duration_minutes: int
):
"""Emit temperature breach event"""
# Determine severity based on duration
if duration_minutes > 120:
severity = "urgent"
elif duration_minutes > 60:
severity = "high"
else:
severity = "medium"
metadata = {
"sensor_id": sensor_id,
"location": location,
"temperature": temperature,
"max_threshold": max_threshold,
"duration_minutes": duration_minutes
}
await self.publisher.publish_alert(
event_type="inventory.temperature_breach",
tenant_id=tenant_id,
severity=severity,
data=metadata
)
logger.info(
"temperature_breach_emitted",
tenant_id=str(tenant_id),
location=location,
temperature=temperature
)
async def emit_expired_products(
self,
tenant_id: UUID,
expired_items: List[Dict[str, Any]]
):
"""Emit expired products alert"""
metadata = {
"expired_count": len(expired_items),
"total_quantity_kg": sum(item["quantity"] for item in expired_items),
"total_value": sum(item.get("value", 0) for item in expired_items),
"expired_items": [
{
"id": str(item["id"]),
"name": item["name"],
"stock_id": str(item["stock_id"]),
"quantity": float(item["quantity"]),
"days_expired": item.get("days_expired", 0)
}
for item in expired_items
]
}
await self.publisher.publish_alert(
tenant_id=tenant_id,
event_type="inventory.expired_products",
severity="urgent",
data=metadata
)
logger.info(
"expired_products_emitted",
tenant_id=str(tenant_id),
expired_count=len(expired_items)
)
async def emit_urgent_expiry(
self,
tenant_id: UUID,
ingredient_id: UUID,
ingredient_name: str,
stock_id: UUID,
days_to_expiry: int,
quantity: float
):
"""Emit urgent expiry alert (1-2 days)"""
metadata = {
"ingredient_id": str(ingredient_id),
"ingredient_name": ingredient_name,
"stock_id": str(stock_id),
"days_to_expiry": days_to_expiry,
"days_until_expiry": days_to_expiry, # Alias for urgency analyzer
"quantity": quantity
}
await self.publisher.publish_alert(
tenant_id=tenant_id,
event_type="inventory.urgent_expiry",
severity="high",
data=metadata
)
logger.info(
"urgent_expiry_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
days_to_expiry=days_to_expiry
)
async def emit_overstock_warning(
self,
tenant_id: UUID,
ingredient_id: UUID,
ingredient_name: str,
current_stock: float,
maximum_stock: float,
waste_risk_kg: float = 0
):
"""Emit overstock warning"""
metadata = {
"ingredient_id": str(ingredient_id),
"ingredient_name": ingredient_name,
"current_stock": current_stock,
"maximum_stock": maximum_stock,
"waste_risk_kg": waste_risk_kg
}
await self.publisher.publish_alert(
tenant_id=tenant_id,
event_type="inventory.overstock_warning",
severity="medium",
data=metadata
)
logger.info(
"overstock_warning_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name
)
async def emit_expired_batches_processed(
self,
tenant_id: UUID,
total_batches: int,
total_quantity: float,
affected_ingredients: List[Dict[str, Any]]
):
"""Emit alert for automatically processed expired batches"""
metadata = {
"total_batches_processed": total_batches,
"total_quantity_wasted": total_quantity,
"processing_date": datetime.utcnow().isoformat(),
"affected_ingredients": affected_ingredients,
"automation_source": "daily_expired_batch_check"
}
await self.publisher.publish_alert(
tenant_id=tenant_id,
event_type="inventory.expired_batches_auto_processed",
severity="medium",
data=metadata
)
logger.info(
"expired_batches_processed_emitted",
tenant_id=str(tenant_id),
total_batches=total_batches,
total_quantity=total_quantity
)
# Recommendation methods
async def emit_inventory_optimization(
self,
tenant_id: UUID,
ingredient_id: UUID,
ingredient_name: str,
recommendation_type: str,
current_max: Optional[float] = None,
suggested_max: Optional[float] = None,
current_min: Optional[float] = None,
suggested_min: Optional[float] = None,
avg_daily_usage: Optional[float] = None
):
"""Emit inventory optimization recommendation"""
metadata = {
"ingredient_id": str(ingredient_id),
"ingredient_name": ingredient_name,
"recommendation_type": recommendation_type
}
if current_max:
metadata["current_max"] = current_max
if suggested_max:
metadata["suggested_max"] = suggested_max
if current_min:
metadata["current_min"] = current_min
if suggested_min:
metadata["suggested_min"] = suggested_min
if avg_daily_usage:
metadata["avg_daily_usage"] = avg_daily_usage
await self.publisher.publish_recommendation(
event_type="inventory.inventory_optimization",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"inventory_optimization_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
recommendation_type=recommendation_type
)
async def emit_waste_reduction_recommendation(
self,
tenant_id: UUID,
ingredient_id: UUID,
ingredient_name: str,
total_waste_30d: float,
waste_incidents: int,
waste_reason: str,
estimated_reduction_percent: float
):
"""Emit waste reduction recommendation"""
metadata = {
"ingredient_id": str(ingredient_id),
"ingredient_name": ingredient_name,
"total_waste_30d": total_waste_30d,
"waste_incidents": waste_incidents,
"waste_reason": waste_reason,
"estimated_reduction_percent": estimated_reduction_percent
}
await self.publisher.publish_recommendation(
event_type="inventory.waste_reduction",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"waste_reduction_recommendation_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
total_waste=total_waste_30d
)