210 lines
6.6 KiB
Python
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
|
|
) |