Files
bakery-ia/services/orders/app/services/procurement_notification_service.py
2025-10-21 19:50:07 +02:00

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))