New alert service
This commit is contained in:
@@ -1,33 +1,25 @@
|
||||
# services/orders/app/services/procurement_notification_service.py
|
||||
"""
|
||||
Procurement Notification Service - Send alerts and notifications for procurement events
|
||||
Procurement Notification Service - Send alerts and notifications for procurement events using EventPublisher
|
||||
Handles PO approval notifications, reminders, escalations, and summaries
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone
|
||||
import structlog
|
||||
|
||||
from shared.config.base import BaseServiceSettings
|
||||
from shared.alerts.base_service import BaseAlertService
|
||||
from shared.messaging import UnifiedEventPublisher
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class ProcurementNotificationService(BaseAlertService):
|
||||
"""Service for sending procurement-related notifications and alerts"""
|
||||
class ProcurementNotificationService:
|
||||
"""Service for sending procurement-related notifications and alerts using EventPublisher"""
|
||||
|
||||
def __init__(self, config: BaseServiceSettings):
|
||||
super().__init__(config)
|
||||
|
||||
def setup_scheduled_checks(self):
|
||||
"""Procurement service doesn't use scheduled checks - alerts are event-driven"""
|
||||
pass
|
||||
|
||||
async def register_db_listeners(self, conn):
|
||||
"""Procurement service doesn't use database triggers - alerts are event-driven"""
|
||||
pass
|
||||
def __init__(self, event_publisher: UnifiedEventPublisher):
|
||||
self.publisher = event_publisher
|
||||
|
||||
async def send_pos_pending_approval_alert(
|
||||
self,
|
||||
@@ -51,34 +43,33 @@ class ProcurementNotificationService(BaseAlertService):
|
||||
if critical_count > 0 or total_amount > 5000:
|
||||
severity = "high"
|
||||
elif total_amount > 10000:
|
||||
severity = "critical"
|
||||
severity = "urgent"
|
||||
|
||||
alert_data = {
|
||||
"type": "procurement_pos_pending_approval",
|
||||
"severity": severity,
|
||||
"title": f"{len(pos_data)} Pedidos Pendientes de Aprobación",
|
||||
"message": f"Se han creado {len(pos_data)} pedidos de compra que requieren tu aprobación. Total: €{total_amount:.2f}",
|
||||
"metadata": {
|
||||
"tenant_id": str(tenant_id),
|
||||
"pos_count": len(pos_data),
|
||||
"total_amount": total_amount,
|
||||
"critical_count": critical_count,
|
||||
"pos": [
|
||||
{
|
||||
"po_id": po.get("po_id"),
|
||||
"po_number": po.get("po_number"),
|
||||
"supplier_id": po.get("supplier_id"),
|
||||
"total_amount": po.get("total_amount"),
|
||||
"auto_approved": po.get("auto_approved", False)
|
||||
}
|
||||
for po in pos_data
|
||||
],
|
||||
"action_required": True,
|
||||
"action_url": "/app/comprar"
|
||||
}
|
||||
metadata = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"pos_count": len(pos_data),
|
||||
"total_amount": total_amount,
|
||||
"critical_count": critical_count,
|
||||
"pos": [
|
||||
{
|
||||
"po_id": po.get("po_id"),
|
||||
"po_number": po.get("po_number"),
|
||||
"supplier_id": po.get("supplier_id"),
|
||||
"total_amount": po.get("total_amount"),
|
||||
"auto_approved": po.get("auto_approved", False)
|
||||
}
|
||||
for po in pos_data
|
||||
],
|
||||
"action_required": True,
|
||||
"action_url": "/app/comprar"
|
||||
}
|
||||
|
||||
await self.publish_item(tenant_id, alert_data, item_type='alert')
|
||||
await self.publisher.publish_alert(
|
||||
event_type="procurement.pos_pending_approval",
|
||||
tenant_id=tenant_id,
|
||||
severity=severity,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info("POs pending approval alert sent",
|
||||
tenant_id=str(tenant_id),
|
||||
@@ -100,25 +91,27 @@ class ProcurementNotificationService(BaseAlertService):
|
||||
Send reminder for POs that haven't been approved within threshold
|
||||
"""
|
||||
try:
|
||||
alert_data = {
|
||||
"type": "procurement_approval_reminder",
|
||||
"severity": "medium" if hours_pending < 36 else "high",
|
||||
"title": f"Recordatorio: Pedido {po_data.get('po_number')} Pendiente",
|
||||
"message": f"El pedido {po_data.get('po_number')} lleva {hours_pending} horas sin aprobarse. Total: €{po_data.get('total_amount', 0):.2f}",
|
||||
"metadata": {
|
||||
"tenant_id": str(tenant_id),
|
||||
"po_id": po_data.get("po_id"),
|
||||
"po_number": po_data.get("po_number"),
|
||||
"supplier_name": po_data.get("supplier_name"),
|
||||
"total_amount": po_data.get("total_amount"),
|
||||
"hours_pending": hours_pending,
|
||||
"created_at": po_data.get("created_at"),
|
||||
"action_required": True,
|
||||
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
|
||||
}
|
||||
# Determine severity based on pending hours
|
||||
severity = "medium" if hours_pending < 36 else "high"
|
||||
|
||||
metadata = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"po_id": po_data.get("po_id"),
|
||||
"po_number": po_data.get("po_number"),
|
||||
"supplier_name": po_data.get("supplier_name"),
|
||||
"total_amount": po_data.get("total_amount"),
|
||||
"hours_pending": hours_pending,
|
||||
"created_at": po_data.get("created_at"),
|
||||
"action_required": True,
|
||||
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
|
||||
}
|
||||
|
||||
await self.publish_item(tenant_id, alert_data, item_type='alert')
|
||||
await self.publisher.publish_alert(
|
||||
event_type="procurement.approval_reminder",
|
||||
tenant_id=tenant_id,
|
||||
severity=severity,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info("Approval reminder sent",
|
||||
tenant_id=str(tenant_id),
|
||||
@@ -141,27 +134,26 @@ class ProcurementNotificationService(BaseAlertService):
|
||||
Send escalation alert for critical/urgent POs not approved in time
|
||||
"""
|
||||
try:
|
||||
alert_data = {
|
||||
"type": "procurement_critical_po",
|
||||
"severity": "critical",
|
||||
"title": f"🚨 URGENTE: Pedido Crítico {po_data.get('po_number')}",
|
||||
"message": f"El pedido crítico {po_data.get('po_number')} lleva {hours_pending} horas sin aprobar. Se requiere acción inmediata.",
|
||||
"metadata": {
|
||||
"tenant_id": str(tenant_id),
|
||||
"po_id": po_data.get("po_id"),
|
||||
"po_number": po_data.get("po_number"),
|
||||
"supplier_name": po_data.get("supplier_name"),
|
||||
"total_amount": po_data.get("total_amount"),
|
||||
"priority": po_data.get("priority"),
|
||||
"required_delivery_date": po_data.get("required_delivery_date"),
|
||||
"hours_pending": hours_pending,
|
||||
"escalated": True,
|
||||
"action_required": True,
|
||||
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
|
||||
}
|
||||
metadata = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"po_id": po_data.get("po_id"),
|
||||
"po_number": po_data.get("po_number"),
|
||||
"supplier_name": po_data.get("supplier_name"),
|
||||
"total_amount": po_data.get("total_amount"),
|
||||
"priority": po_data.get("priority"),
|
||||
"required_delivery_date": po_data.get("required_delivery_date"),
|
||||
"hours_pending": hours_pending,
|
||||
"escalated": True,
|
||||
"action_required": True,
|
||||
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
|
||||
}
|
||||
|
||||
await self.publish_item(tenant_id, alert_data, item_type='alert')
|
||||
await self.publisher.publish_alert(
|
||||
event_type="procurement.critical_po_escalation",
|
||||
tenant_id=tenant_id,
|
||||
severity="urgent",
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.warning("Critical PO escalation sent",
|
||||
tenant_id=str(tenant_id),
|
||||
@@ -191,24 +183,22 @@ class ProcurementNotificationService(BaseAlertService):
|
||||
# No activity, skip notification
|
||||
return
|
||||
|
||||
alert_data = {
|
||||
"type": "procurement_auto_approval_summary",
|
||||
"severity": "low",
|
||||
"title": "Resumen Diario de Pedidos",
|
||||
"message": f"Hoy se aprobaron automáticamente {auto_approved_count} pedidos (€{total_amount:.2f}). {manual_approval_count} requieren aprobación manual.",
|
||||
"metadata": {
|
||||
"tenant_id": str(tenant_id),
|
||||
"auto_approved_count": auto_approved_count,
|
||||
"total_auto_approved_amount": total_amount,
|
||||
"manual_approval_count": manual_approval_count,
|
||||
"summary_date": summary_data.get("date"),
|
||||
"auto_approved_pos": summary_data.get("auto_approved_pos", []),
|
||||
"pending_approval_pos": summary_data.get("pending_approval_pos", []),
|
||||
"action_url": "/app/comprar"
|
||||
}
|
||||
metadata = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"auto_approved_count": auto_approved_count,
|
||||
"total_auto_approved_amount": total_amount,
|
||||
"manual_approval_count": manual_approval_count,
|
||||
"summary_date": summary_data.get("date"),
|
||||
"auto_approved_pos": summary_data.get("auto_approved_pos", []),
|
||||
"pending_approval_pos": summary_data.get("pending_approval_pos", []),
|
||||
"action_url": "/app/comprar"
|
||||
}
|
||||
|
||||
await self.publish_item(tenant_id, alert_data, item_type='notification')
|
||||
await self.publisher.publish_notification(
|
||||
event_type="procurement.auto_approval_summary",
|
||||
tenant_id=tenant_id,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info("Auto-approval summary sent",
|
||||
tenant_id=str(tenant_id),
|
||||
@@ -231,27 +221,23 @@ class ProcurementNotificationService(BaseAlertService):
|
||||
Send confirmation when a PO is approved
|
||||
"""
|
||||
try:
|
||||
approval_type = "automáticamente" if auto_approved else f"por {approved_by}"
|
||||
|
||||
alert_data = {
|
||||
"type": "procurement_po_approved",
|
||||
"severity": "low",
|
||||
"title": f"Pedido {po_data.get('po_number')} Aprobado",
|
||||
"message": f"El pedido {po_data.get('po_number')} ha sido aprobado {approval_type}. Total: €{po_data.get('total_amount', 0):.2f}",
|
||||
"metadata": {
|
||||
"tenant_id": str(tenant_id),
|
||||
"po_id": po_data.get("po_id"),
|
||||
"po_number": po_data.get("po_number"),
|
||||
"supplier_name": po_data.get("supplier_name"),
|
||||
"total_amount": po_data.get("total_amount"),
|
||||
"approved_by": approved_by,
|
||||
"auto_approved": auto_approved,
|
||||
"approved_at": datetime.now(timezone.utc).isoformat(),
|
||||
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
|
||||
}
|
||||
metadata = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"po_id": po_data.get("po_id"),
|
||||
"po_number": po_data.get("po_number"),
|
||||
"supplier_name": po_data.get("supplier_name"),
|
||||
"total_amount": po_data.get("total_amount"),
|
||||
"approved_by": approved_by,
|
||||
"auto_approved": auto_approved,
|
||||
"approved_at": datetime.now(timezone.utc).isoformat(),
|
||||
"action_url": f"/app/comprar?po={po_data.get('po_id')}"
|
||||
}
|
||||
|
||||
await self.publish_item(tenant_id, alert_data, item_type='notification')
|
||||
await self.publisher.publish_notification(
|
||||
event_type="procurement.po_approved_confirmation",
|
||||
tenant_id=tenant_id,
|
||||
data=metadata
|
||||
)
|
||||
|
||||
logger.info("PO approved confirmation sent",
|
||||
tenant_id=str(tenant_id),
|
||||
@@ -262,4 +248,4 @@ class ProcurementNotificationService(BaseAlertService):
|
||||
logger.error("Error sending PO approved confirmation",
|
||||
tenant_id=str(tenant_id),
|
||||
po_id=po_data.get("po_id"),
|
||||
error=str(e))
|
||||
error=str(e))
|
||||
Reference in New Issue
Block a user