New alert system and panel de control page

This commit is contained in:
Urtzi Alfaro
2025-11-27 15:52:40 +01:00
parent 1a2f4602f3
commit e902419b6e
178 changed files with 20982 additions and 6944 deletions

View File

@@ -0,0 +1,246 @@
"""
Inventory Notification Service
Emits informational notifications for inventory state changes:
- stock_received: When deliveries arrive
- stock_movement: Transfers, adjustments
- stock_updated: General stock updates
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 shared.schemas.event_classification import RawEvent, EventClass, EventDomain
from shared.alerts.base_service import BaseAlertService
logger = logging.getLogger(__name__)
class InventoryNotificationService(BaseAlertService):
"""
Service for emitting inventory notifications (informational state changes).
"""
def __init__(self, rabbitmq_url: str = None):
super().__init__(service_name="inventory", rabbitmq_url=rabbitmq_url)
async def emit_stock_received_notification(
self,
db: Session,
tenant_id: str,
stock_receipt_id: str,
ingredient_id: str,
ingredient_name: str,
quantity_received: float,
unit: str,
supplier_name: Optional[str] = None,
delivery_id: Optional[str] = None,
) -> 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),
)
# Publish to RabbitMQ for processing
await self.publish_item(tenant_id, event.dict(), item_type="notification")
logger.info(
f"Stock received notification emitted: {ingredient_name} ({quantity_received} {unit})",
extra={"tenant_id": tenant_id, "ingredient_id": ingredient_id}
)
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,
)
async def emit_stock_movement_notification(
self,
db: Session,
tenant_id: str,
movement_id: str,
ingredient_id: str,
ingredient_name: str,
quantity: float,
unit: str,
movement_type: str, # 'transfer', 'adjustment', 'waste', 'return'
from_location: Optional[str] = None,
to_location: Optional[str] = None,
reason: Optional[str] = None,
) -> 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}"
# 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),
)
# Publish to RabbitMQ
await self.publish_item(tenant_id, event.dict(), item_type="notification")
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,
)
async def emit_stock_updated_notification(
self,
db: Session,
tenant_id: str,
ingredient_id: str,
ingredient_name: str,
old_quantity: float,
new_quantity: float,
unit: str,
update_reason: str,
) -> 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"
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),
)
await self.publish_item(tenant_id, event.dict(), item_type="notification")
logger.info(
f"Stock updated notification emitted: {ingredient_name}",
extra={"tenant_id": tenant_id, "ingredient_id": ingredient_id}
)
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,
)