258 lines
10 KiB
Python
258 lines
10 KiB
Python
# services/orders/app/services/procurement_notification_service.py
|
|
"""
|
|
Procurement Notification Service - Send alerts and notifications for procurement events
|
|
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
|
|
import structlog
|
|
|
|
from shared.config.base import BaseServiceSettings
|
|
from shared.alerts.base_service import AlertServiceMixin
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class ProcurementNotificationService(AlertServiceMixin):
|
|
"""Service for sending procurement-related notifications and alerts"""
|
|
|
|
def __init__(self, config: BaseServiceSettings):
|
|
self.config = config
|
|
|
|
async def send_pos_pending_approval_alert(
|
|
self,
|
|
tenant_id: UUID,
|
|
pos_data: List[Dict[str, Any]]
|
|
):
|
|
"""
|
|
Send alert when new POs are created and need approval
|
|
Groups POs and sends a summary notification
|
|
"""
|
|
try:
|
|
if not pos_data:
|
|
return
|
|
|
|
# Calculate totals
|
|
total_amount = sum(float(po.get('total_amount', 0)) for po in pos_data)
|
|
critical_count = sum(1 for po in pos_data if po.get('priority') in ['high', 'critical', 'urgent'])
|
|
|
|
# Determine severity based on amount and urgency
|
|
severity = "medium"
|
|
if critical_count > 0 or total_amount > 5000:
|
|
severity = "high"
|
|
elif total_amount > 10000:
|
|
severity = "critical"
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
await self.publish_item(tenant_id, alert_data, item_type='alert')
|
|
|
|
logger.info("POs pending approval alert sent",
|
|
tenant_id=str(tenant_id),
|
|
pos_count=len(pos_data),
|
|
total_amount=total_amount)
|
|
|
|
except Exception as e:
|
|
logger.error("Error sending POs pending approval alert",
|
|
tenant_id=str(tenant_id),
|
|
error=str(e))
|
|
|
|
async def send_approval_reminder(
|
|
self,
|
|
tenant_id: UUID,
|
|
po_data: Dict[str, Any],
|
|
hours_pending: int
|
|
):
|
|
"""
|
|
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')}"
|
|
}
|
|
}
|
|
|
|
await self.publish_item(tenant_id, alert_data, item_type='alert')
|
|
|
|
logger.info("Approval reminder sent",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_data.get("po_id"),
|
|
hours_pending=hours_pending)
|
|
|
|
except Exception as e:
|
|
logger.error("Error sending approval reminder",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_data.get("po_id"),
|
|
error=str(e))
|
|
|
|
async def send_critical_po_escalation(
|
|
self,
|
|
tenant_id: UUID,
|
|
po_data: Dict[str, Any],
|
|
hours_pending: int
|
|
):
|
|
"""
|
|
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')}"
|
|
}
|
|
}
|
|
|
|
await self.publish_item(tenant_id, alert_data, item_type='alert')
|
|
|
|
logger.warning("Critical PO escalation sent",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_data.get("po_id"),
|
|
hours_pending=hours_pending)
|
|
|
|
except Exception as e:
|
|
logger.error("Error sending critical PO escalation",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_data.get("po_id"),
|
|
error=str(e))
|
|
|
|
async def send_auto_approval_summary(
|
|
self,
|
|
tenant_id: UUID,
|
|
summary_data: Dict[str, Any]
|
|
):
|
|
"""
|
|
Send daily summary of auto-approved POs
|
|
"""
|
|
try:
|
|
auto_approved_count = summary_data.get("auto_approved_count", 0)
|
|
total_amount = summary_data.get("total_auto_approved_amount", 0)
|
|
manual_approval_count = summary_data.get("manual_approval_count", 0)
|
|
|
|
if auto_approved_count == 0 and manual_approval_count == 0:
|
|
# 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"
|
|
}
|
|
}
|
|
|
|
await self.publish_item(tenant_id, alert_data, item_type='notification')
|
|
|
|
logger.info("Auto-approval summary sent",
|
|
tenant_id=str(tenant_id),
|
|
auto_approved_count=auto_approved_count,
|
|
manual_count=manual_approval_count)
|
|
|
|
except Exception as e:
|
|
logger.error("Error sending auto-approval summary",
|
|
tenant_id=str(tenant_id),
|
|
error=str(e))
|
|
|
|
async def send_po_approved_confirmation(
|
|
self,
|
|
tenant_id: UUID,
|
|
po_data: Dict[str, Any],
|
|
approved_by: str,
|
|
auto_approved: bool = False
|
|
):
|
|
"""
|
|
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')}"
|
|
}
|
|
}
|
|
|
|
await self.publish_item(tenant_id, alert_data, item_type='notification')
|
|
|
|
logger.info("PO approved confirmation sent",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_data.get("po_id"),
|
|
auto_approved=auto_approved)
|
|
|
|
except Exception as e:
|
|
logger.error("Error sending PO approved confirmation",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_data.get("po_id"),
|
|
error=str(e))
|