244 lines
7.7 KiB
Python
244 lines
7.7 KiB
Python
|
|
# services/inventory/app/services/messaging.py
|
||
|
|
"""
|
||
|
|
Messaging service for inventory events
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Dict, Any, Optional
|
||
|
|
from uuid import UUID
|
||
|
|
import structlog
|
||
|
|
|
||
|
|
from shared.messaging.rabbitmq import MessagePublisher
|
||
|
|
from shared.messaging.events import (
|
||
|
|
EVENT_TYPES,
|
||
|
|
InventoryEvent,
|
||
|
|
StockAlertEvent,
|
||
|
|
StockMovementEvent
|
||
|
|
)
|
||
|
|
|
||
|
|
logger = structlog.get_logger()
|
||
|
|
|
||
|
|
|
||
|
|
class InventoryMessagingService:
|
||
|
|
"""Service for publishing inventory-related events"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.publisher = MessagePublisher()
|
||
|
|
|
||
|
|
async def publish_ingredient_created(
|
||
|
|
self,
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
ingredient_data: Dict[str, Any]
|
||
|
|
):
|
||
|
|
"""Publish ingredient creation event"""
|
||
|
|
try:
|
||
|
|
event = InventoryEvent(
|
||
|
|
event_type=EVENT_TYPES.INVENTORY.INGREDIENT_CREATED,
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
ingredient_id=str(ingredient_id),
|
||
|
|
data=ingredient_data
|
||
|
|
)
|
||
|
|
|
||
|
|
await self.publisher.publish_event(
|
||
|
|
routing_key="inventory.ingredient.created",
|
||
|
|
event=event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published ingredient created event",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Failed to publish ingredient created event",
|
||
|
|
error=str(e),
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id
|
||
|
|
)
|
||
|
|
|
||
|
|
async def publish_stock_added(
|
||
|
|
self,
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
stock_id: UUID,
|
||
|
|
quantity: float,
|
||
|
|
batch_number: Optional[str] = None
|
||
|
|
):
|
||
|
|
"""Publish stock addition event"""
|
||
|
|
try:
|
||
|
|
movement_event = StockMovementEvent(
|
||
|
|
event_type=EVENT_TYPES.INVENTORY.STOCK_ADDED,
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
ingredient_id=str(ingredient_id),
|
||
|
|
stock_id=str(stock_id),
|
||
|
|
quantity=quantity,
|
||
|
|
movement_type="purchase",
|
||
|
|
data={
|
||
|
|
"batch_number": batch_number,
|
||
|
|
"movement_type": "purchase"
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
await self.publisher.publish_event(
|
||
|
|
routing_key="inventory.stock.added",
|
||
|
|
event=movement_event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published stock added event",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id,
|
||
|
|
quantity=quantity
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Failed to publish stock added event",
|
||
|
|
error=str(e),
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id
|
||
|
|
)
|
||
|
|
|
||
|
|
async def publish_stock_consumed(
|
||
|
|
self,
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
consumed_items: list,
|
||
|
|
total_quantity: float,
|
||
|
|
reference_number: Optional[str] = None
|
||
|
|
):
|
||
|
|
"""Publish stock consumption event"""
|
||
|
|
try:
|
||
|
|
for item in consumed_items:
|
||
|
|
movement_event = StockMovementEvent(
|
||
|
|
event_type=EVENT_TYPES.INVENTORY.STOCK_CONSUMED,
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
ingredient_id=str(ingredient_id),
|
||
|
|
stock_id=item['stock_id'],
|
||
|
|
quantity=item['quantity_consumed'],
|
||
|
|
movement_type="production_use",
|
||
|
|
data={
|
||
|
|
"batch_number": item.get('batch_number'),
|
||
|
|
"reference_number": reference_number,
|
||
|
|
"movement_type": "production_use"
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
await self.publisher.publish_event(
|
||
|
|
routing_key="inventory.stock.consumed",
|
||
|
|
event=movement_event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published stock consumed events",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id,
|
||
|
|
total_quantity=total_quantity,
|
||
|
|
items_count=len(consumed_items)
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Failed to publish stock consumed event",
|
||
|
|
error=str(e),
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id
|
||
|
|
)
|
||
|
|
|
||
|
|
async def publish_low_stock_alert(
|
||
|
|
self,
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
ingredient_name: str,
|
||
|
|
current_stock: float,
|
||
|
|
threshold: float,
|
||
|
|
needs_reorder: bool = False
|
||
|
|
):
|
||
|
|
"""Publish low stock alert event"""
|
||
|
|
try:
|
||
|
|
alert_event = StockAlertEvent(
|
||
|
|
event_type=EVENT_TYPES.INVENTORY.LOW_STOCK_ALERT,
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
ingredient_id=str(ingredient_id),
|
||
|
|
alert_type="low_stock" if not needs_reorder else "reorder_needed",
|
||
|
|
severity="medium" if not needs_reorder else "high",
|
||
|
|
data={
|
||
|
|
"ingredient_name": ingredient_name,
|
||
|
|
"current_stock": current_stock,
|
||
|
|
"threshold": threshold,
|
||
|
|
"needs_reorder": needs_reorder
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
await self.publisher.publish_event(
|
||
|
|
routing_key="inventory.alerts.low_stock",
|
||
|
|
event=alert_event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published low stock alert",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id,
|
||
|
|
current_stock=current_stock
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Failed to publish low stock alert",
|
||
|
|
error=str(e),
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id
|
||
|
|
)
|
||
|
|
|
||
|
|
async def publish_expiration_alert(
|
||
|
|
self,
|
||
|
|
tenant_id: UUID,
|
||
|
|
ingredient_id: UUID,
|
||
|
|
stock_id: UUID,
|
||
|
|
ingredient_name: str,
|
||
|
|
batch_number: Optional[str],
|
||
|
|
expiration_date: str,
|
||
|
|
days_to_expiry: int,
|
||
|
|
quantity: float
|
||
|
|
):
|
||
|
|
"""Publish expiration alert event"""
|
||
|
|
try:
|
||
|
|
severity = "critical" if days_to_expiry <= 1 else "high"
|
||
|
|
|
||
|
|
alert_event = StockAlertEvent(
|
||
|
|
event_type=EVENT_TYPES.INVENTORY.EXPIRATION_ALERT,
|
||
|
|
tenant_id=str(tenant_id),
|
||
|
|
ingredient_id=str(ingredient_id),
|
||
|
|
alert_type="expiring_soon",
|
||
|
|
severity=severity,
|
||
|
|
data={
|
||
|
|
"stock_id": str(stock_id),
|
||
|
|
"ingredient_name": ingredient_name,
|
||
|
|
"batch_number": batch_number,
|
||
|
|
"expiration_date": expiration_date,
|
||
|
|
"days_to_expiry": days_to_expiry,
|
||
|
|
"quantity": quantity
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
await self.publisher.publish_event(
|
||
|
|
routing_key="inventory.alerts.expiration",
|
||
|
|
event=alert_event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published expiration alert",
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id,
|
||
|
|
days_to_expiry=days_to_expiry
|
||
|
|
)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Failed to publish expiration alert",
|
||
|
|
error=str(e),
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
ingredient_id=ingredient_id
|
||
|
|
)
|