Files
bakery-ia/services/procurement/app/services/procurement_event_service.py
2025-12-05 20:07:01 +01:00

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
)