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

417 lines
12 KiB
Python

"""
Procurement Alert Service - Simplified
Emits minimal events using EventPublisher.
All enrichment handled by alert_processor.
"""
import asyncio
from typing import List, Dict, Any, Optional
from uuid import UUID
from datetime import datetime
import structlog
from shared.messaging import UnifiedEventPublisher, EVENT_TYPES
logger = structlog.get_logger()
class ProcurementAlertService:
"""Simplified procurement alert service using UnifiedEventPublisher"""
def __init__(self, event_publisher: UnifiedEventPublisher):
self.publisher = event_publisher
async def emit_po_approval_needed(
self,
tenant_id: UUID,
po_id: UUID,
po_number: str,
supplier_name: str,
total_amount: float,
currency: str,
items_count: int,
required_delivery_date: str
):
"""Emit PO approval needed event"""
metadata = {
"po_id": str(po_id),
"po_number": po_number,
"supplier_name": supplier_name,
"total_amount": total_amount,
"po_amount": total_amount, # Alias for compatibility
"currency": currency,
"items_count": items_count,
"required_delivery_date": required_delivery_date
}
await self.publisher.publish_alert(
event_type="supply_chain.po_approval_needed",
tenant_id=tenant_id,
severity="high",
data=metadata
)
logger.info(
"po_approval_needed_emitted",
tenant_id=str(tenant_id),
po_number=po_number,
total_amount=total_amount
)
async def emit_delivery_overdue(
self,
tenant_id: UUID,
po_id: UUID,
po_number: str,
supplier_name: str,
supplier_contact: Optional[str],
expected_date: str,
days_overdue: int,
items: List[Dict[str, Any]]
):
"""Emit delivery overdue alert"""
# Determine severity based on days overdue
if days_overdue > 7:
severity = "urgent"
elif days_overdue > 3:
severity = "high"
else:
severity = "medium"
metadata = {
"po_id": str(po_id),
"po_number": po_number,
"supplier_name": supplier_name,
"expected_date": expected_date,
"days_overdue": days_overdue,
"items": items,
"items_count": len(items)
}
if supplier_contact:
metadata["supplier_contact"] = supplier_contact
await self.publisher.publish_alert(
event_type="supply_chain.delivery_overdue",
tenant_id=tenant_id,
severity=severity,
data=metadata
)
logger.info(
"delivery_overdue_emitted",
tenant_id=str(tenant_id),
po_number=po_number,
days_overdue=days_overdue
)
async def emit_supplier_performance_issue(
self,
tenant_id: UUID,
supplier_id: UUID,
supplier_name: str,
issue_type: str,
issue_description: str,
affected_orders: int = 0,
total_value_affected: Optional[float] = None
):
"""Emit supplier performance issue alert"""
metadata = {
"supplier_id": str(supplier_id),
"supplier_name": supplier_name,
"issue_type": issue_type,
"issue_description": issue_description,
"affected_orders": affected_orders
}
if total_value_affected:
metadata["total_value_affected"] = total_value_affected
await self.publisher.publish_alert(
event_type="supply_chain.supplier_performance_issue",
tenant_id=tenant_id,
severity="high",
data=metadata
)
logger.info(
"supplier_performance_issue_emitted",
tenant_id=str(tenant_id),
supplier_name=supplier_name,
issue_type=issue_type
)
async def emit_price_increase_alert(
self,
tenant_id: UUID,
supplier_id: UUID,
supplier_name: str,
ingredient_name: str,
old_price: float,
new_price: float,
increase_percent: float
):
"""Emit price increase alert"""
metadata = {
"supplier_id": str(supplier_id),
"supplier_name": supplier_name,
"ingredient_name": ingredient_name,
"old_price": old_price,
"new_price": new_price,
"increase_percent": increase_percent
}
# Determine severity based on increase
if increase_percent > 20:
severity = "high"
elif increase_percent > 10:
severity = "medium"
else:
severity = "low"
await self.publisher.publish_alert(
event_type="supply_chain.price_increase",
tenant_id=tenant_id,
severity=severity,
data=metadata
)
logger.info(
"price_increase_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
increase_percent=increase_percent
)
async def emit_partial_delivery(
self,
tenant_id: UUID,
po_id: UUID,
po_number: str,
supplier_name: str,
ordered_quantity: float,
delivered_quantity: float,
missing_quantity: float,
ingredient_name: str
):
"""Emit partial delivery alert"""
metadata = {
"po_id": str(po_id),
"po_number": po_number,
"supplier_name": supplier_name,
"ordered_quantity": ordered_quantity,
"delivered_quantity": delivered_quantity,
"missing_quantity": missing_quantity,
"ingredient_name": ingredient_name
}
await self.publisher.publish_alert(
event_type="supply_chain.partial_delivery",
tenant_id=tenant_id,
severity="medium",
data=metadata
)
logger.info(
"partial_delivery_emitted",
tenant_id=str(tenant_id),
po_number=po_number,
missing_quantity=missing_quantity
)
async def emit_delivery_quality_issue(
self,
tenant_id: UUID,
po_id: UUID,
po_number: str,
supplier_name: str,
issue_description: str,
affected_items: List[Dict[str, Any]],
requires_return: bool = False
):
"""Emit delivery quality issue alert"""
metadata = {
"po_id": str(po_id),
"po_number": po_number,
"supplier_name": supplier_name,
"issue_description": issue_description,
"affected_items": affected_items,
"requires_return": requires_return,
"affected_items_count": len(affected_items)
}
await self.publisher.publish_alert(
event_type="supply_chain.delivery_quality_issue",
tenant_id=tenant_id,
severity="high",
data=metadata
)
logger.info(
"delivery_quality_issue_emitted",
tenant_id=str(tenant_id),
po_number=po_number,
requires_return=requires_return
)
async def emit_low_supplier_rating(
self,
tenant_id: UUID,
supplier_id: UUID,
supplier_name: str,
current_rating: float,
issues_count: int,
recommendation: str
):
"""Emit low supplier rating alert"""
metadata = {
"supplier_id": str(supplier_id),
"supplier_name": supplier_name,
"current_rating": current_rating,
"issues_count": issues_count,
"recommendation": recommendation
}
await self.publisher.publish_alert(
event_type="supply_chain.low_supplier_rating",
tenant_id=tenant_id,
severity="medium",
data=metadata
)
logger.info(
"low_supplier_rating_emitted",
tenant_id=str(tenant_id),
supplier_name=supplier_name,
current_rating=current_rating
)
# Recommendation methods
async def emit_supplier_consolidation(
self,
tenant_id: UUID,
current_suppliers_count: int,
suggested_suppliers: List[str],
potential_savings_eur: float
):
"""Emit supplier consolidation recommendation"""
metadata = {
"current_suppliers_count": current_suppliers_count,
"suggested_suppliers": suggested_suppliers,
"potential_savings_eur": potential_savings_eur
}
await self.publisher.publish_recommendation(
event_type="supply_chain.supplier_consolidation",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"supplier_consolidation_emitted",
tenant_id=str(tenant_id),
potential_savings=potential_savings_eur
)
async def emit_bulk_purchase_opportunity(
self,
tenant_id: UUID,
ingredient_name: str,
current_order_frequency: int,
suggested_bulk_size: float,
potential_discount_percent: float,
estimated_savings_eur: float
):
"""Emit bulk purchase opportunity recommendation"""
metadata = {
"ingredient_name": ingredient_name,
"current_order_frequency": current_order_frequency,
"suggested_bulk_size": suggested_bulk_size,
"potential_discount_percent": potential_discount_percent,
"estimated_savings_eur": estimated_savings_eur
}
await self.publisher.publish_recommendation(
event_type="supply_chain.bulk_purchase_opportunity",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"bulk_purchase_opportunity_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name,
estimated_savings=estimated_savings_eur
)
async def emit_alternative_supplier_suggestion(
self,
tenant_id: UUID,
ingredient_name: str,
current_supplier: str,
alternative_supplier: str,
price_difference_eur: float,
quality_rating: float
):
"""Emit alternative supplier suggestion"""
metadata = {
"ingredient_name": ingredient_name,
"current_supplier": current_supplier,
"alternative_supplier": alternative_supplier,
"price_difference_eur": price_difference_eur,
"quality_rating": quality_rating
}
await self.publisher.publish_recommendation(
event_type="supply_chain.alternative_supplier_suggestion",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"alternative_supplier_suggestion_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name
)
async def emit_reorder_point_optimization(
self,
tenant_id: UUID,
ingredient_name: str,
current_reorder_point: float,
suggested_reorder_point: float,
rationale: str
):
"""Emit reorder point optimization recommendation"""
metadata = {
"ingredient_name": ingredient_name,
"current_reorder_point": current_reorder_point,
"suggested_reorder_point": suggested_reorder_point,
"rationale": rationale
}
await self.publisher.publish_recommendation(
event_type="supply_chain.reorder_point_optimization",
tenant_id=tenant_id,
data=metadata
)
logger.info(
"reorder_point_optimization_emitted",
tenant_id=str(tenant_id),
ingredient_name=ingredient_name
)