251 lines
9.1 KiB
Python
251 lines
9.1 KiB
Python
# services/orders/app/services/procurement_notification_service.py
|
|
"""
|
|
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, timezone
|
|
import structlog
|
|
|
|
from shared.config.base import BaseServiceSettings
|
|
from shared.messaging import UnifiedEventPublisher
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class ProcurementNotificationService:
|
|
"""Service for sending procurement-related notifications and alerts using EventPublisher"""
|
|
|
|
def __init__(self, event_publisher: UnifiedEventPublisher):
|
|
self.publisher = event_publisher
|
|
|
|
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 = "urgent"
|
|
|
|
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.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),
|
|
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:
|
|
# 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.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),
|
|
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:
|
|
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.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),
|
|
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
|
|
|
|
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.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),
|
|
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:
|
|
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.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),
|
|
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)) |