New alert service
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,33 @@
|
||||
"""
|
||||
Production Notification Service
|
||||
Production Notification Service - Simplified
|
||||
|
||||
Emits informational notifications for production state changes:
|
||||
- batch_state_changed: When batch transitions between states
|
||||
- batch_completed: When batch production completes
|
||||
- batch_started: When batch production begins
|
||||
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 ProductionNotificationService(BaseAlertService):
|
||||
class ProductionNotificationService:
|
||||
"""
|
||||
Service for emitting production notifications (informational state changes).
|
||||
Service for emitting production notifications using EventPublisher.
|
||||
"""
|
||||
|
||||
def __init__(self, rabbitmq_url: str = None):
|
||||
super().__init__(service_name="production", rabbitmq_url=rabbitmq_url)
|
||||
def __init__(self, event_publisher: UnifiedEventPublisher):
|
||||
self.publisher = event_publisher
|
||||
|
||||
async def emit_batch_state_changed_notification(
|
||||
self,
|
||||
db: Session,
|
||||
tenant_id: str,
|
||||
tenant_id: UUID,
|
||||
batch_id: str,
|
||||
product_sku: str,
|
||||
product_name: str,
|
||||
@@ -44,76 +39,50 @@ class ProductionNotificationService(BaseAlertService):
|
||||
) -> None:
|
||||
"""
|
||||
Emit notification when a production batch changes state.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
tenant_id: Tenant ID
|
||||
batch_id: Production batch ID
|
||||
product_sku: Product SKU
|
||||
product_name: Product name
|
||||
old_status: Previous status (PENDING, IN_PROGRESS, COMPLETED, etc.)
|
||||
new_status: New status
|
||||
quantity: Batch quantity
|
||||
unit: Unit of measurement
|
||||
assigned_to: Assigned worker/station (optional)
|
||||
"""
|
||||
try:
|
||||
# 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}",
|
||||
}
|
||||
# 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}"
|
||||
)
|
||||
message = transition_messages.get(
|
||||
(old_status, new_status),
|
||||
f"{product_name} status changed from {old_status} to {new_status}"
|
||||
)
|
||||
|
||||
# Create notification event
|
||||
event = RawEvent(
|
||||
tenant_id=tenant_id,
|
||||
event_class=EventClass.NOTIFICATION,
|
||||
event_domain=EventDomain.PRODUCTION,
|
||||
event_type="batch_state_changed",
|
||||
title=f"Batch Status: {new_status}",
|
||||
message=f"{message} ({quantity} {unit})",
|
||||
service="production",
|
||||
event_metadata={
|
||||
"batch_id": batch_id,
|
||||
"product_sku": product_sku,
|
||||
"product_name": product_name,
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
"quantity": quantity,
|
||||
"unit": unit,
|
||||
"assigned_to": assigned_to,
|
||||
"state_changed_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
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(),
|
||||
}
|
||||
|
||||
# Publish to RabbitMQ for processing
|
||||
await self.publish_item(tenant_id, event.dict(), item_type="notification")
|
||||
await self.publisher.publish_notification(
|
||||
event_type="production.batch_state_changed",
|
||||
tenant_id=tenant_id,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Batch state change notification emitted: {batch_id} ({old_status} → {new_status})",
|
||||
extra={"tenant_id": tenant_id, "batch_id": batch_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to emit batch state change notification: {e}",
|
||||
extra={"tenant_id": tenant_id, "batch_id": batch_id},
|
||||
exc_info=True,
|
||||
)
|
||||
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,
|
||||
db: Session,
|
||||
tenant_id: str,
|
||||
tenant_id: UUID,
|
||||
batch_id: str,
|
||||
product_sku: str,
|
||||
product_name: str,
|
||||
@@ -124,64 +93,42 @@ class ProductionNotificationService(BaseAlertService):
|
||||
) -> None:
|
||||
"""
|
||||
Emit notification when a production batch is completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
tenant_id: Tenant ID
|
||||
batch_id: Production batch ID
|
||||
product_sku: Product SKU
|
||||
product_name: Product name
|
||||
quantity_produced: Quantity produced
|
||||
unit: Unit of measurement
|
||||
production_duration_minutes: Total production time (optional)
|
||||
quality_score: Quality score (0-100, optional)
|
||||
"""
|
||||
try:
|
||||
message = f"Produced {quantity_produced} {unit} of {product_name}"
|
||||
if production_duration_minutes:
|
||||
message += f" in {production_duration_minutes} minutes"
|
||||
if quality_score:
|
||||
message += f" (Quality: {quality_score:.1f}%)"
|
||||
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)
|
||||
|
||||
event = RawEvent(
|
||||
tenant_id=tenant_id,
|
||||
event_class=EventClass.NOTIFICATION,
|
||||
event_domain=EventDomain.PRODUCTION,
|
||||
event_type="batch_completed",
|
||||
title=f"Batch Completed: {product_name}",
|
||||
message=message,
|
||||
service="production",
|
||||
event_metadata={
|
||||
"batch_id": batch_id,
|
||||
"product_sku": product_sku,
|
||||
"product_name": product_name,
|
||||
"quantity_produced": quantity_produced,
|
||||
"unit": unit,
|
||||
"production_duration_minutes": production_duration_minutes,
|
||||
"quality_score": quality_score,
|
||||
"completed_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
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.publish_item(tenant_id, event.dict(), item_type="notification")
|
||||
await self.publisher.publish_notification(
|
||||
event_type="production.batch_completed",
|
||||
tenant_id=tenant_id,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Batch completed notification emitted: {batch_id} ({quantity_produced} {unit})",
|
||||
extra={"tenant_id": tenant_id, "batch_id": batch_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to emit batch completed notification: {e}",
|
||||
extra={"tenant_id": tenant_id, "batch_id": batch_id},
|
||||
exc_info=True,
|
||||
)
|
||||
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,
|
||||
db: Session,
|
||||
tenant_id: str,
|
||||
tenant_id: UUID,
|
||||
batch_id: str,
|
||||
product_sku: str,
|
||||
product_name: str,
|
||||
@@ -192,64 +139,41 @@ class ProductionNotificationService(BaseAlertService):
|
||||
) -> None:
|
||||
"""
|
||||
Emit notification when a production batch is started.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
tenant_id: Tenant ID
|
||||
batch_id: Production batch ID
|
||||
product_sku: Product SKU
|
||||
product_name: Product name
|
||||
quantity_planned: Planned quantity
|
||||
unit: Unit of measurement
|
||||
estimated_duration_minutes: Estimated duration (optional)
|
||||
assigned_to: Assigned worker/station (optional)
|
||||
"""
|
||||
try:
|
||||
message = f"Started production of {quantity_planned} {unit} of {product_name}"
|
||||
if estimated_duration_minutes:
|
||||
message += f" (Est. {estimated_duration_minutes} min)"
|
||||
if assigned_to:
|
||||
message += f" - Assigned to {assigned_to}"
|
||||
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)
|
||||
|
||||
event = RawEvent(
|
||||
tenant_id=tenant_id,
|
||||
event_class=EventClass.NOTIFICATION,
|
||||
event_domain=EventDomain.PRODUCTION,
|
||||
event_type="batch_started",
|
||||
title=f"Batch Started: {product_name}",
|
||||
message=message,
|
||||
service="production",
|
||||
event_metadata={
|
||||
"batch_id": batch_id,
|
||||
"product_sku": product_sku,
|
||||
"product_name": product_name,
|
||||
"quantity_planned": quantity_planned,
|
||||
"unit": unit,
|
||||
"estimated_duration_minutes": estimated_duration_minutes,
|
||||
"assigned_to": assigned_to,
|
||||
"started_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
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.publish_item(tenant_id, event.dict(), item_type="notification")
|
||||
await self.publisher.publish_notification(
|
||||
event_type="production.batch_started",
|
||||
tenant_id=tenant_id,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Batch started notification emitted: {batch_id}",
|
||||
extra={"tenant_id": tenant_id, "batch_id": batch_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to emit batch started notification: {e}",
|
||||
extra={"tenant_id": tenant_id, "batch_id": batch_id},
|
||||
exc_info=True,
|
||||
)
|
||||
logger.info(
|
||||
"batch_started_notification_emitted",
|
||||
tenant_id=str(tenant_id),
|
||||
batch_id=batch_id
|
||||
)
|
||||
|
||||
async def emit_equipment_status_notification(
|
||||
self,
|
||||
db: Session,
|
||||
tenant_id: str,
|
||||
tenant_id: UUID,
|
||||
equipment_id: str,
|
||||
equipment_name: str,
|
||||
old_status: str,
|
||||
@@ -258,50 +182,29 @@ class ProductionNotificationService(BaseAlertService):
|
||||
) -> None:
|
||||
"""
|
||||
Emit notification when equipment status changes.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
tenant_id: Tenant ID
|
||||
equipment_id: Equipment ID
|
||||
equipment_name: Equipment name
|
||||
old_status: Previous status
|
||||
new_status: New status
|
||||
reason: Reason for status change (optional)
|
||||
"""
|
||||
try:
|
||||
message = f"{equipment_name} status: {old_status} → {new_status}"
|
||||
if reason:
|
||||
message += f" - {reason}"
|
||||
message = f"{equipment_name} status: {old_status} → {new_status}"
|
||||
if reason:
|
||||
message += f" - {reason}"
|
||||
|
||||
event = RawEvent(
|
||||
tenant_id=tenant_id,
|
||||
event_class=EventClass.NOTIFICATION,
|
||||
event_domain=EventDomain.PRODUCTION,
|
||||
event_type="equipment_status_changed",
|
||||
title=f"Equipment Status: {equipment_name}",
|
||||
message=message,
|
||||
service="production",
|
||||
event_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(),
|
||||
},
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
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.publish_item(tenant_id, event.dict(), item_type="notification")
|
||||
await self.publisher.publish_notification(
|
||||
event_type="production.equipment_status_changed",
|
||||
tenant_id=tenant_id,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Equipment status notification emitted: {equipment_name}",
|
||||
extra={"tenant_id": tenant_id, "equipment_id": equipment_id}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to emit equipment status notification: {e}",
|
||||
extra={"tenant_id": tenant_id, "equipment_id": equipment_id},
|
||||
exc_info=True,
|
||||
)
|
||||
logger.info(
|
||||
"equipment_status_notification_emitted",
|
||||
tenant_id=str(tenant_id),
|
||||
equipment_id=equipment_id,
|
||||
new_status=new_status
|
||||
)
|
||||
@@ -24,6 +24,7 @@ from app.schemas.production import (
|
||||
ProductionScheduleCreate, ProductionScheduleUpdate, ProductionScheduleResponse,
|
||||
DailyProductionRequirements, ProductionDashboardSummary, ProductionMetrics
|
||||
)
|
||||
from app.utils.cache import delete_cached, make_cache_key
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -324,12 +325,17 @@ class ProductionService:
|
||||
await self._update_inventory_on_completion(
|
||||
tenant_id, batch, status_update.actual_quantity
|
||||
)
|
||||
|
||||
logger.info("Updated batch status",
|
||||
batch_id=str(batch_id),
|
||||
|
||||
# PHASE 2: Invalidate production dashboard cache
|
||||
cache_key = make_cache_key("production_dashboard", str(tenant_id))
|
||||
await delete_cached(cache_key)
|
||||
logger.debug("Invalidated production dashboard cache", cache_key=cache_key, tenant_id=str(tenant_id))
|
||||
|
||||
logger.info("Updated batch status",
|
||||
batch_id=str(batch_id),
|
||||
new_status=status_update.status.value,
|
||||
tenant_id=str(tenant_id))
|
||||
|
||||
|
||||
return batch
|
||||
|
||||
except Exception as e:
|
||||
@@ -658,7 +664,26 @@ class ProductionService:
|
||||
logger.info("Started production batch",
|
||||
batch_id=str(batch_id), tenant_id=str(tenant_id))
|
||||
|
||||
return batch
|
||||
# Acknowledge production delay alerts (non-blocking)
|
||||
try:
|
||||
from shared.clients.alert_processor_client import get_alert_processor_client
|
||||
alert_client = get_alert_processor_client(self.config, "production")
|
||||
await alert_client.acknowledge_alerts_by_metadata(
|
||||
tenant_id=tenant_id,
|
||||
alert_type="production_delay",
|
||||
metadata_filter={"batch_id": str(batch_id)}
|
||||
)
|
||||
await alert_client.acknowledge_alerts_by_metadata(
|
||||
tenant_id=tenant_id,
|
||||
alert_type="batch_at_risk",
|
||||
metadata_filter={"batch_id": str(batch_id)}
|
||||
)
|
||||
logger.debug("Acknowledged production delay alerts", batch_id=str(batch_id))
|
||||
except Exception as e:
|
||||
# Log but don't fail the batch start
|
||||
logger.warning("Failed to acknowledge production alerts", batch_id=str(batch_id), error=str(e))
|
||||
|
||||
return batch
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error starting production batch",
|
||||
|
||||
Reference in New Issue
Block a user