377 lines
11 KiB
Python
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
|
|
)
|