175 lines
7.5 KiB
Python
175 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Script to emit alerts for existing pending purchase orders.
|
|
|
|
This is a one-time migration script to create alerts for POs that were
|
|
created before the alert emission feature was implemented.
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
|
|
# Add parent directories to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / 'shared'))
|
|
|
|
from sqlalchemy import select, text
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
from sqlalchemy.orm import sessionmaker
|
|
import structlog
|
|
|
|
from app.models.purchase_order import PurchaseOrder
|
|
from shared.messaging.rabbitmq import RabbitMQClient
|
|
from app.core.config import settings
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
async def main():
|
|
"""Emit alerts for all pending purchase orders"""
|
|
|
|
# Create database engine
|
|
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
|
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
|
|
# Create RabbitMQ client
|
|
rabbitmq_client = RabbitMQClient(settings.RABBITMQ_URL, "procurement")
|
|
await rabbitmq_client.connect()
|
|
|
|
try:
|
|
async with async_session() as session:
|
|
# Get all pending approval POs
|
|
query = select(PurchaseOrder).where(
|
|
PurchaseOrder.status == 'pending_approval'
|
|
)
|
|
result = await session.execute(query)
|
|
pending_pos = result.scalars().all()
|
|
|
|
logger.info(f"Found {len(pending_pos)} pending purchase orders")
|
|
|
|
for po in pending_pos:
|
|
try:
|
|
# Get supplier info from suppliers service (simplified - using stored data)
|
|
# In production, you'd fetch from suppliers service
|
|
supplier_name = f"Supplier-{po.supplier_id}"
|
|
|
|
# Prepare alert payload
|
|
from uuid import uuid4
|
|
from datetime import datetime as dt, timedelta, timezone
|
|
|
|
# Get current UTC time with timezone
|
|
now_utc = dt.now(timezone.utc)
|
|
|
|
# Calculate deadline and urgency for priority scoring
|
|
if po.required_delivery_date:
|
|
# Ensure deadline is timezone-aware
|
|
deadline = po.required_delivery_date
|
|
if deadline.tzinfo is None:
|
|
deadline = deadline.replace(tzinfo=timezone.utc)
|
|
else:
|
|
# Default: 7 days for normal priority, 3 days for critical
|
|
days_until = 3 if po.priority == 'critical' else 7
|
|
deadline = now_utc + timedelta(days=days_until)
|
|
|
|
# Calculate hours until consequence (for urgency scoring)
|
|
hours_until = (deadline - now_utc).total_seconds() / 3600
|
|
|
|
alert_data = {
|
|
'id': str(uuid4()), # Generate unique alert ID
|
|
'tenant_id': str(po.tenant_id),
|
|
'service': 'procurement',
|
|
'type': 'po_approval_needed',
|
|
'alert_type': 'po_approval_needed', # Added for dashboard filtering
|
|
'type_class': 'action_needed', # Critical for dashboard action queue
|
|
'severity': 'high' if po.priority == 'critical' else 'medium',
|
|
'title': f'Purchase Order #{po.po_number} requires approval',
|
|
'message': f'Purchase order totaling {po.currency} {po.total_amount:.2f} is pending approval.',
|
|
'timestamp': now_utc.isoformat(),
|
|
'metadata': {
|
|
'po_id': str(po.id),
|
|
'po_number': po.po_number,
|
|
'supplier_id': str(po.supplier_id),
|
|
'supplier_name': supplier_name,
|
|
'total_amount': float(po.total_amount),
|
|
'currency': po.currency,
|
|
'priority': po.priority,
|
|
'required_delivery_date': po.required_delivery_date.isoformat() if po.required_delivery_date else None,
|
|
'created_at': po.created_at.isoformat(),
|
|
# Enrichment metadata for priority scoring
|
|
'financial_impact': float(po.total_amount), # Business impact
|
|
'deadline': deadline.isoformat(), # Urgency deadline
|
|
'hours_until_consequence': int(hours_until), # Urgency hours
|
|
},
|
|
'actions': [
|
|
{
|
|
'action_type': 'approve_po',
|
|
'label': 'Approve PO',
|
|
'variant': 'primary',
|
|
'disabled': False,
|
|
'endpoint': f'/api/v1/tenants/{po.tenant_id}/purchase-orders/{po.id}/approve',
|
|
'method': 'POST'
|
|
},
|
|
{
|
|
'action_type': 'reject_po',
|
|
'label': 'Reject',
|
|
'variant': 'ghost',
|
|
'disabled': False,
|
|
'endpoint': f'/api/v1/tenants/{po.tenant_id}/purchase-orders/{po.id}/reject',
|
|
'method': 'POST'
|
|
},
|
|
{
|
|
'action_type': 'modify_po',
|
|
'label': 'Modify',
|
|
'variant': 'ghost',
|
|
'disabled': False,
|
|
'endpoint': f'/api/v1/tenants/{po.tenant_id}/purchase-orders/{po.id}',
|
|
'method': 'GET'
|
|
}
|
|
],
|
|
'item_type': 'alert'
|
|
}
|
|
|
|
# Publish to RabbitMQ
|
|
success = await rabbitmq_client.publish_event(
|
|
exchange_name='alerts.exchange',
|
|
routing_key=f'alert.{alert_data["severity"]}.procurement',
|
|
event_data=alert_data
|
|
)
|
|
|
|
if success:
|
|
logger.info(
|
|
f"✓ Alert emitted for PO {po.po_number}",
|
|
po_id=str(po.id),
|
|
tenant_id=str(po.tenant_id)
|
|
)
|
|
else:
|
|
logger.error(
|
|
f"✗ Failed to emit alert for PO {po.po_number}",
|
|
po_id=str(po.id)
|
|
)
|
|
|
|
# Small delay to avoid overwhelming the system
|
|
await asyncio.sleep(0.5)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
logger.error(
|
|
f"Error processing PO {po.po_number}",
|
|
error=str(e),
|
|
po_id=str(po.id),
|
|
traceback=traceback.format_exc()
|
|
)
|
|
continue
|
|
|
|
logger.info(f"✅ Finished emitting alerts for {len(pending_pos)} purchase orders")
|
|
|
|
finally:
|
|
await rabbitmq_client.disconnect()
|
|
await engine.dispose()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|