293 lines
8.7 KiB
Python
293 lines
8.7 KiB
Python
"""
|
|
Procurement Event Service - Simplified
|
|
|
|
Emits minimal events using EventPublisher.
|
|
All enrichment handled by alert_processor.
|
|
|
|
ALERTS (actionable):
|
|
- po_approval_needed: Purchase order requires approval
|
|
- delivery_overdue: Delivery past expected date
|
|
|
|
NOTIFICATIONS (informational):
|
|
- po_approved: Purchase order approved
|
|
- po_sent_to_supplier: PO sent to supplier
|
|
- delivery_scheduled: Delivery confirmed
|
|
- delivery_arriving_soon: Delivery arriving within hours
|
|
- delivery_received: Delivery arrived
|
|
|
|
This service demonstrates the mixed event model where a single domain
|
|
emits both actionable alerts and informational notifications.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Optional, Dict, Any, List
|
|
from uuid import UUID
|
|
import structlog
|
|
|
|
from shared.messaging import UnifiedEventPublisher, EVENT_TYPES
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
class ProcurementEventService:
|
|
"""
|
|
Service for emitting procurement/supply chain events using EventPublisher.
|
|
"""
|
|
|
|
def __init__(self, event_publisher: UnifiedEventPublisher):
|
|
self.publisher = event_publisher
|
|
|
|
# ============================================================
|
|
# ALERTS (Actionable)
|
|
# ============================================================
|
|
|
|
async def emit_po_approval_needed_alert(
|
|
self,
|
|
tenant_id: UUID,
|
|
po_id: str,
|
|
supplier_name: str,
|
|
total_amount_eur: float,
|
|
items_count: int,
|
|
urgency_reason: str,
|
|
delivery_needed_by: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Emit ALERT when purchase order requires approval.
|
|
"""
|
|
metadata = {
|
|
"po_id": po_id,
|
|
"po_number": po_id, # Add po_number for template compatibility
|
|
"supplier_name": supplier_name,
|
|
"total_amount_eur": float(total_amount_eur),
|
|
"total_amount": float(total_amount_eur), # Add total_amount for template compatibility
|
|
"currency": "EUR", # Add currency for template compatibility
|
|
"items_count": items_count,
|
|
"urgency_reason": urgency_reason,
|
|
"delivery_needed_by": delivery_needed_by,
|
|
"required_delivery_date": delivery_needed_by, # Add for template compatibility
|
|
}
|
|
|
|
# Determine severity based on amount and urgency
|
|
if total_amount_eur > 1000 or "expedited" in urgency_reason.lower():
|
|
severity = "high"
|
|
else:
|
|
severity = "medium"
|
|
|
|
await self.publisher.publish_alert(
|
|
event_type="supply_chain.po_approval_needed",
|
|
tenant_id=tenant_id,
|
|
severity=severity,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"po_approval_needed_alert_emitted",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_id,
|
|
total_amount_eur=total_amount_eur
|
|
)
|
|
|
|
async def emit_delivery_overdue_alert(
|
|
self,
|
|
tenant_id: UUID,
|
|
delivery_id: str,
|
|
po_id: str,
|
|
supplier_name: str,
|
|
expected_date: str,
|
|
days_overdue: int,
|
|
items_affected: List[Dict[str, Any]],
|
|
) -> None:
|
|
"""
|
|
Emit ALERT when delivery is overdue.
|
|
"""
|
|
# Determine severity based on days overdue
|
|
if days_overdue > 7:
|
|
severity = "urgent"
|
|
elif days_overdue > 3:
|
|
severity = "high"
|
|
else:
|
|
severity = "medium"
|
|
|
|
metadata = {
|
|
"delivery_id": delivery_id,
|
|
"po_id": po_id,
|
|
"supplier_name": supplier_name,
|
|
"expected_date": expected_date,
|
|
"days_overdue": days_overdue,
|
|
"items_affected": items_affected,
|
|
}
|
|
|
|
await self.publisher.publish_alert(
|
|
event_type="supply_chain.delivery_overdue",
|
|
tenant_id=tenant_id,
|
|
severity=severity,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"delivery_overdue_alert_emitted",
|
|
tenant_id=str(tenant_id),
|
|
delivery_id=delivery_id,
|
|
days_overdue=days_overdue
|
|
)
|
|
|
|
# ============================================================
|
|
# NOTIFICATIONS (Informational)
|
|
# ============================================================
|
|
|
|
async def emit_po_approved_notification(
|
|
self,
|
|
tenant_id: UUID,
|
|
po_id: str,
|
|
supplier_name: str,
|
|
total_amount_eur: float,
|
|
approved_by: str,
|
|
expected_delivery_date: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Emit NOTIFICATION when purchase order is approved.
|
|
"""
|
|
metadata = {
|
|
"po_id": po_id,
|
|
"supplier_name": supplier_name,
|
|
"total_amount_eur": float(total_amount_eur),
|
|
"approved_by": approved_by,
|
|
"expected_delivery_date": expected_delivery_date,
|
|
"approved_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
await self.publisher.publish_notification(
|
|
event_type="supply_chain.po_approved",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"po_approved_notification_emitted",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_id
|
|
)
|
|
|
|
async def emit_po_sent_to_supplier_notification(
|
|
self,
|
|
tenant_id: UUID,
|
|
po_id: str,
|
|
supplier_name: str,
|
|
supplier_email: str,
|
|
) -> None:
|
|
"""
|
|
Emit NOTIFICATION when PO is sent to supplier.
|
|
"""
|
|
metadata = {
|
|
"po_id": po_id,
|
|
"supplier_name": supplier_name,
|
|
"supplier_email": supplier_email,
|
|
"sent_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
await self.publisher.publish_notification(
|
|
event_type="supply_chain.po_sent_to_supplier",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"po_sent_to_supplier_notification_emitted",
|
|
tenant_id=str(tenant_id),
|
|
po_id=po_id
|
|
)
|
|
|
|
async def emit_delivery_scheduled_notification(
|
|
self,
|
|
tenant_id: UUID,
|
|
delivery_id: str,
|
|
po_id: str,
|
|
supplier_name: str,
|
|
expected_delivery_date: str,
|
|
tracking_number: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Emit NOTIFICATION when delivery is scheduled/confirmed.
|
|
"""
|
|
metadata = {
|
|
"delivery_id": delivery_id,
|
|
"po_id": po_id,
|
|
"supplier_name": supplier_name,
|
|
"expected_delivery_date": expected_delivery_date,
|
|
"tracking_number": tracking_number,
|
|
}
|
|
|
|
await self.publisher.publish_notification(
|
|
event_type="supply_chain.delivery_scheduled",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"delivery_scheduled_notification_emitted",
|
|
tenant_id=str(tenant_id),
|
|
delivery_id=delivery_id
|
|
)
|
|
|
|
async def emit_delivery_arriving_soon_notification(
|
|
self,
|
|
tenant_id: UUID,
|
|
delivery_id: str,
|
|
supplier_name: str,
|
|
expected_arrival_time: str,
|
|
hours_until_arrival: int,
|
|
) -> None:
|
|
"""
|
|
Emit NOTIFICATION when delivery is arriving soon (within hours).
|
|
"""
|
|
metadata = {
|
|
"delivery_id": delivery_id,
|
|
"supplier_name": supplier_name,
|
|
"expected_arrival_time": expected_arrival_time,
|
|
"hours_until_arrival": hours_until_arrival,
|
|
}
|
|
|
|
await self.publisher.publish_notification(
|
|
event_type="supply_chain.delivery_arriving_soon",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"delivery_arriving_soon_notification_emitted",
|
|
tenant_id=str(tenant_id),
|
|
delivery_id=delivery_id
|
|
)
|
|
|
|
async def emit_delivery_received_notification(
|
|
self,
|
|
tenant_id: UUID,
|
|
delivery_id: str,
|
|
po_id: str,
|
|
supplier_name: str,
|
|
items_received: int,
|
|
received_by: str,
|
|
) -> None:
|
|
"""
|
|
Emit NOTIFICATION when delivery is received.
|
|
"""
|
|
metadata = {
|
|
"delivery_id": delivery_id,
|
|
"po_id": po_id,
|
|
"supplier_name": supplier_name,
|
|
"items_received": items_received,
|
|
"received_by": received_by,
|
|
"received_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
await self.publisher.publish_notification(
|
|
event_type="supply_chain.delivery_received",
|
|
tenant_id=tenant_id,
|
|
data=metadata
|
|
)
|
|
|
|
logger.info(
|
|
"delivery_received_notification_emitted",
|
|
tenant_id=str(tenant_id),
|
|
delivery_id=delivery_id
|
|
) |