#!/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())