Files
bakery-ia/services/production/app/services/production_notification_service.py
2025-12-05 20:07:01 +01:00

210 lines
6.6 KiB
Python

"""
Production Notification Service - Simplified
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.
"""
from datetime import datetime, timezone
from typing import Optional, Dict, Any
from uuid import UUID
import structlog
from shared.messaging import UnifiedEventPublisher
logger = structlog.get_logger()
class ProductionNotificationService:
"""
Service for emitting production notifications using EventPublisher.
"""
def __init__(self, event_publisher: UnifiedEventPublisher):
self.publisher = event_publisher
async def emit_batch_state_changed_notification(
self,
tenant_id: UUID,
batch_id: str,
product_sku: str,
product_name: str,
old_status: str,
new_status: str,
quantity: float,
unit: str,
assigned_to: Optional[str] = None,
) -> None:
"""
Emit notification when a production batch changes state.
"""
# Build message based on state transition
transition_messages = {
("PENDING", "IN_PROGRESS"): f"Production started for {product_name}",
("IN_PROGRESS", "COMPLETED"): f"Production completed for {product_name}",
("IN_PROGRESS", "PAUSED"): f"Production paused for {product_name}",
("PAUSED", "IN_PROGRESS"): f"Production resumed for {product_name}",
("IN_PROGRESS", "FAILED"): f"Production failed for {product_name}",
}
message = transition_messages.get(
(old_status, new_status),
f"{product_name} status changed from {old_status} to {new_status}"
)
metadata = {
"batch_id": batch_id,
"product_sku": product_sku,
"product_name": product_name,
"old_status": old_status,
"new_status": new_status,
"quantity": float(quantity),
"unit": unit,
"assigned_to": assigned_to,
"state_changed_at": datetime.now(timezone.utc).isoformat(),
}
await self.publisher.publish_notification(
event_type="production.batch_state_changed",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"batch_state_changed_notification_emitted",
tenant_id=str(tenant_id),
batch_id=batch_id,
old_status=old_status,
new_status=new_status
)
async def emit_batch_completed_notification(
self,
tenant_id: UUID,
batch_id: str,
product_sku: str,
product_name: str,
quantity_produced: float,
unit: str,
production_duration_minutes: Optional[int] = None,
quality_score: Optional[float] = None,
) -> None:
"""
Emit notification when a production batch is completed.
"""
message_parts = [f"Produced {quantity_produced} {unit} of {product_name}"]
if production_duration_minutes:
message_parts.append(f"in {production_duration_minutes} minutes")
if quality_score:
message_parts.append(f"(Quality: {quality_score:.1f}%)")
message = " ".join(message_parts)
metadata = {
"batch_id": batch_id,
"product_sku": product_sku,
"product_name": product_name,
"quantity_produced": float(quantity_produced),
"unit": unit,
"production_duration_minutes": production_duration_minutes,
"quality_score": quality_score,
"completed_at": datetime.now(timezone.utc).isoformat(),
}
await self.publisher.publish_notification(
event_type="production.batch_completed",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"batch_completed_notification_emitted",
tenant_id=str(tenant_id),
batch_id=batch_id,
quantity_produced=quantity_produced
)
async def emit_batch_started_notification(
self,
tenant_id: UUID,
batch_id: str,
product_sku: str,
product_name: str,
quantity_planned: float,
unit: str,
estimated_duration_minutes: Optional[int] = None,
assigned_to: Optional[str] = None,
) -> None:
"""
Emit notification when a production batch is started.
"""
message_parts = [f"Started production of {quantity_planned} {unit} of {product_name}"]
if estimated_duration_minutes:
message_parts.append(f"(Est. {estimated_duration_minutes} min)")
if assigned_to:
message_parts.append(f"- Assigned to {assigned_to}")
message = " ".join(message_parts)
metadata = {
"batch_id": batch_id,
"product_sku": product_sku,
"product_name": product_name,
"quantity_planned": float(quantity_planned),
"unit": unit,
"estimated_duration_minutes": estimated_duration_minutes,
"assigned_to": assigned_to,
"started_at": datetime.now(timezone.utc).isoformat(),
}
await self.publisher.publish_notification(
event_type="production.batch_started",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"batch_started_notification_emitted",
tenant_id=str(tenant_id),
batch_id=batch_id
)
async def emit_equipment_status_notification(
self,
tenant_id: UUID,
equipment_id: str,
equipment_name: str,
old_status: str,
new_status: str,
reason: Optional[str] = None,
) -> None:
"""
Emit notification when equipment status changes.
"""
message = f"{equipment_name} status: {old_status}{new_status}"
if reason:
message += f" - {reason}"
metadata = {
"equipment_id": equipment_id,
"equipment_name": equipment_name,
"old_status": old_status,
"new_status": new_status,
"reason": reason,
"status_changed_at": datetime.now(timezone.utc).isoformat(),
}
await self.publisher.publish_notification(
event_type="production.equipment_status_changed",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"equipment_status_notification_emitted",
tenant_id=str(tenant_id),
equipment_id=equipment_id,
new_status=new_status
)