417 lines
12 KiB
Python
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
|
|
)
|