New alert service
This commit is contained in:
@@ -436,7 +436,33 @@ class DashboardService:
|
||||
except Exception as e:
|
||||
logger.error("Failed to get live metrics", error=str(e))
|
||||
raise
|
||||
|
||||
|
||||
async def get_inventory_overview(
|
||||
self,
|
||||
db,
|
||||
tenant_id: UUID
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get lightweight inventory overview for orchestrator health checks.
|
||||
Returns minimal data needed by dashboard health-status endpoint.
|
||||
This is a fast endpoint optimized for frequent polling.
|
||||
"""
|
||||
try:
|
||||
# Get only the essential metric needed by orchestrator
|
||||
inventory_summary = await self.inventory_service.get_inventory_summary(tenant_id)
|
||||
|
||||
return {
|
||||
"out_of_stock_count": inventory_summary.out_of_stock_items,
|
||||
"tenant_id": str(tenant_id),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get inventory overview",
|
||||
tenant_id=str(tenant_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def export_dashboard_data(
|
||||
self,
|
||||
db,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
)
|
||||
@@ -22,6 +22,7 @@ from app.schemas.inventory import (
|
||||
from app.core.database import get_db_transaction
|
||||
from shared.database.exceptions import DatabaseError
|
||||
from shared.utils.batch_generator import BatchNumberGenerator, create_fallback_batch_number
|
||||
from app.utils.cache import delete_cached, make_cache_key
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -843,6 +844,11 @@ class InventoryService:
|
||||
ingredient_dict['category'] = ingredient.ingredient_category.value if ingredient.ingredient_category else None
|
||||
response.ingredient = IngredientResponse(**ingredient_dict)
|
||||
|
||||
# PHASE 2: Invalidate inventory dashboard cache
|
||||
cache_key = make_cache_key("inventory_dashboard", str(tenant_id))
|
||||
await delete_cached(cache_key)
|
||||
logger.debug("Invalidated inventory dashboard cache", cache_key=cache_key, tenant_id=str(tenant_id))
|
||||
|
||||
logger.info("Stock entry updated successfully", stock_id=stock_id, tenant_id=tenant_id)
|
||||
return response
|
||||
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
# 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
|
||||
)
|
||||
Reference in New Issue
Block a user