New alert system and panel de control page
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user