New alert service

This commit is contained in:
Urtzi Alfaro
2025-12-05 20:07:01 +01:00
parent 1fe3a73549
commit 667e6e0404
393 changed files with 26002 additions and 61033 deletions

View File

@@ -1,38 +1,33 @@
"""
Inventory Notification Service
Inventory Notification Service - Simplified
Emits informational notifications for inventory state changes:
- stock_received: When deliveries arrive
- stock_movement: Transfers, adjustments
- stock_updated: General stock updates
Emits minimal events using EventPublisher.
All enrichment handled by alert_processor.
These are NOTIFICATIONS (not alerts) - informational state changes that don't require user action.
"""
import logging
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from sqlalchemy.orm import Session
from uuid import UUID
import structlog
from shared.schemas.event_classification import RawEvent, EventClass, EventDomain
from shared.alerts.base_service import BaseAlertService
from shared.messaging import UnifiedEventPublisher
logger = structlog.get_logger()
logger = logging.getLogger(__name__)
class InventoryNotificationService(BaseAlertService):
class InventoryNotificationService:
"""
Service for emitting inventory notifications (informational state changes).
Service for emitting inventory notifications using EventPublisher.
"""
def __init__(self, rabbitmq_url: str = None):
super().__init__(service_name="inventory", rabbitmq_url=rabbitmq_url)
def __init__(self, event_publisher: UnifiedEventPublisher):
self.publisher = event_publisher
async def emit_stock_received_notification(
self,
db: Session,
tenant_id: str,
tenant_id: UUID,
stock_receipt_id: str,
ingredient_id: str,
ingredient_name: str,
@@ -43,61 +38,38 @@ class InventoryNotificationService(BaseAlertService):
) -> None:
"""
Emit notification when stock is received.
Args:
db: Database session
tenant_id: Tenant ID
stock_receipt_id: Stock receipt ID
ingredient_id: Ingredient ID
ingredient_name: Ingredient name
quantity_received: Quantity received
unit: Unit of measurement
supplier_name: Supplier name (optional)
delivery_id: Delivery ID (optional)
"""
try:
# Create notification event
event = RawEvent(
tenant_id=tenant_id,
event_class=EventClass.NOTIFICATION,
event_domain=EventDomain.INVENTORY,
event_type="stock_received",
title=f"Stock Received: {ingredient_name}",
message=f"Received {quantity_received} {unit} of {ingredient_name}"
+ (f" from {supplier_name}" if supplier_name else ""),
service="inventory",
event_metadata={
"stock_receipt_id": stock_receipt_id,
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
"quantity_received": quantity_received,
"unit": unit,
"supplier_name": supplier_name,
"delivery_id": delivery_id,
"received_at": datetime.now(timezone.utc).isoformat(),
},
timestamp=datetime.now(timezone.utc),
)
message = f"Received {quantity_received} {unit} of {ingredient_name}"
if supplier_name:
message += f" from {supplier_name}"
# Publish to RabbitMQ for processing
await self.publish_item(tenant_id, event.dict(), item_type="notification")
metadata = {
"stock_receipt_id": stock_receipt_id,
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
"quantity_received": float(quantity_received),
"unit": unit,
"supplier_name": supplier_name,
"delivery_id": delivery_id,
"received_at": datetime.now(timezone.utc).isoformat(),
}
logger.info(
f"Stock received notification emitted: {ingredient_name} ({quantity_received} {unit})",
extra={"tenant_id": tenant_id, "ingredient_id": ingredient_id}
)
await self.publisher.publish_notification(
event_type="inventory.stock_received",
tenant_id=tenant_id,
data=metadata
)
except Exception as e:
logger.error(
f"Failed to emit stock received notification: {e}",
extra={"tenant_id": tenant_id, "ingredient_id": ingredient_id},
exc_info=True,
)
logger.info(
"stock_received_notification_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
quantity_received=quantity_received
)
async def emit_stock_movement_notification(
self,
db: Session,
tenant_id: str,
tenant_id: UUID,
movement_id: str,
ingredient_id: str,
ingredient_name: str,
@@ -110,82 +82,54 @@ class InventoryNotificationService(BaseAlertService):
) -> None:
"""
Emit notification for stock movements (transfers, adjustments, waste).
Args:
db: Database session
tenant_id: Tenant ID
movement_id: Movement ID
ingredient_id: Ingredient ID
ingredient_name: Ingredient name
quantity: Quantity moved
unit: Unit of measurement
movement_type: Type of movement
from_location: Source location (optional)
to_location: Destination location (optional)
reason: Reason for movement (optional)
"""
try:
# Build message based on movement type
if movement_type == "transfer":
message = f"Transferred {quantity} {unit} of {ingredient_name}"
if from_location and to_location:
message += f" from {from_location} to {to_location}"
elif movement_type == "adjustment":
message = f"Adjusted {ingredient_name} by {quantity} {unit}"
if reason:
message += f" - {reason}"
elif movement_type == "waste":
message = f"Waste recorded: {quantity} {unit} of {ingredient_name}"
if reason:
message += f" - {reason}"
elif movement_type == "return":
message = f"Returned {quantity} {unit} of {ingredient_name}"
else:
message = f"Stock movement: {quantity} {unit} of {ingredient_name}"
# Build message based on movement type
if movement_type == "transfer":
message = f"Transferred {quantity} {unit} of {ingredient_name}"
if from_location and to_location:
message += f" from {from_location} to {to_location}"
elif movement_type == "adjustment":
message = f"Adjusted {ingredient_name} by {quantity} {unit}"
if reason:
message += f" - {reason}"
elif movement_type == "waste":
message = f"Waste recorded: {quantity} {unit} of {ingredient_name}"
if reason:
message += f" - {reason}"
elif movement_type == "return":
message = f"Returned {quantity} {unit} of {ingredient_name}"
else:
message = f"Stock movement: {quantity} {unit} of {ingredient_name}"
# Create notification event
event = RawEvent(
tenant_id=tenant_id,
event_class=EventClass.NOTIFICATION,
event_domain=EventDomain.INVENTORY,
event_type="stock_movement",
title=f"Stock {movement_type.title()}: {ingredient_name}",
message=message,
service="inventory",
event_metadata={
"movement_id": movement_id,
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
"quantity": quantity,
"unit": unit,
"movement_type": movement_type,
"from_location": from_location,
"to_location": to_location,
"reason": reason,
"moved_at": datetime.now(timezone.utc).isoformat(),
},
timestamp=datetime.now(timezone.utc),
)
metadata = {
"movement_id": movement_id,
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
"quantity": float(quantity),
"unit": unit,
"movement_type": movement_type,
"from_location": from_location,
"to_location": to_location,
"reason": reason,
"moved_at": datetime.now(timezone.utc).isoformat(),
}
# Publish to RabbitMQ
await self.publish_item(tenant_id, event.dict(), item_type="notification")
await self.publisher.publish_notification(
event_type="inventory.stock_movement",
tenant_id=tenant_id,
data=metadata
)
logger.info(
f"Stock movement notification emitted: {movement_type} - {ingredient_name}",
extra={"tenant_id": tenant_id, "movement_id": movement_id}
)
except Exception as e:
logger.error(
f"Failed to emit stock movement notification: {e}",
extra={"tenant_id": tenant_id, "movement_id": movement_id},
exc_info=True,
)
logger.info(
"stock_movement_notification_emitted",
tenant_id=str(tenant_id),
movement_type=movement_type,
ingredient_name=ingredient_name
)
async def emit_stock_updated_notification(
self,
db: Session,
tenant_id: str,
tenant_id: UUID,
ingredient_id: str,
ingredient_name: str,
old_quantity: float,
@@ -195,52 +139,32 @@ class InventoryNotificationService(BaseAlertService):
) -> None:
"""
Emit notification when stock is updated.
Args:
db: Database session
tenant_id: Tenant ID
ingredient_id: Ingredient ID
ingredient_name: Ingredient name
old_quantity: Previous quantity
new_quantity: New quantity
unit: Unit of measurement
update_reason: Reason for update
"""
try:
quantity_change = new_quantity - old_quantity
change_direction = "increased" if quantity_change > 0 else "decreased"
quantity_change = new_quantity - old_quantity
change_direction = "increased" if quantity_change > 0 else "decreased"
event = RawEvent(
tenant_id=tenant_id,
event_class=EventClass.NOTIFICATION,
event_domain=EventDomain.INVENTORY,
event_type="stock_updated",
title=f"Stock Updated: {ingredient_name}",
message=f"Stock {change_direction} by {abs(quantity_change)} {unit} - {update_reason}",
service="inventory",
event_metadata={
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
"old_quantity": old_quantity,
"new_quantity": new_quantity,
"quantity_change": quantity_change,
"unit": unit,
"update_reason": update_reason,
"updated_at": datetime.now(timezone.utc).isoformat(),
},
timestamp=datetime.now(timezone.utc),
)
message = f"Stock {change_direction} by {abs(quantity_change)} {unit} - {update_reason}"
await self.publish_item(tenant_id, event.dict(), item_type="notification")
metadata = {
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
"old_quantity": float(old_quantity),
"new_quantity": float(new_quantity),
"quantity_change": float(quantity_change),
"unit": unit,
"update_reason": update_reason,
"updated_at": datetime.now(timezone.utc).isoformat(),
}
logger.info(
f"Stock updated notification emitted: {ingredient_name}",
extra={"tenant_id": tenant_id, "ingredient_id": ingredient_id}
)
await self.publisher.publish_notification(
event_type="inventory.stock_updated",
tenant_id=tenant_id,
data=metadata
)
except Exception as e:
logger.error(
f"Failed to emit stock updated notification: {e}",
extra={"tenant_id": tenant_id, "ingredient_id": ingredient_id},
exc_info=True,
)
logger.info(
"stock_updated_notification_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
quantity_change=quantity_change
)