""" Overdue PO Scheduler Background scheduler that periodically checks for overdue purchase orders and publishes alerts for them. """ import asyncio from typing import Optional from datetime import datetime, timezone import structlog from app.services.overdue_po_detector import OverduePODetector from shared.messaging import RabbitMQClient logger = structlog.get_logger() class OverduePOScheduler: """ Overdue PO Scheduler Background task that periodically checks for overdue POs and publishes alerts. """ def __init__( self, rabbitmq_client: Optional[RabbitMQClient] = None, check_interval_seconds: int = 3600, # 1 hour default ): """ Initialize overdue PO scheduler. Args: rabbitmq_client: RabbitMQ client for publishing events check_interval_seconds: Seconds between checks (default: 3600 = 1 hour) """ self.detector = OverduePODetector() self.rabbitmq_client = rabbitmq_client self.check_interval_seconds = check_interval_seconds self._task: Optional[asyncio.Task] = None self._running = False logger.info( "Overdue PO Scheduler initialized", check_interval_seconds=check_interval_seconds ) async def start(self): """Start the scheduler background task""" if self._running: logger.warning("Overdue PO Scheduler already running") return self._running = True self._task = asyncio.create_task(self._run_scheduler()) logger.info("Overdue PO Scheduler started") async def stop(self): """Stop the scheduler background task""" if not self._running: return self._running = False if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass logger.info("Overdue PO Scheduler stopped") async def _run_scheduler(self): """Main scheduler loop""" logger.info("Overdue PO Scheduler loop started") while self._running: try: await self._process_cycle() except Exception as e: logger.error( "Overdue PO scheduler cycle failed", error=str(e), exc_info=True ) # Wait for next cycle try: await asyncio.sleep(self.check_interval_seconds) except asyncio.CancelledError: break logger.info("Overdue PO Scheduler loop ended") async def _process_cycle(self): """Process one scheduler cycle - detect and alert on overdue POs""" logger.info("Starting overdue PO detection cycle") try: # Detect all overdue POs across all tenants overdue_pos = await self.detector.detect_overdue_pos() if not overdue_pos: logger.info("No overdue POs detected in this cycle") return # Group by severity by_severity = { 'critical': [], 'high': [], 'medium': [], 'low': [] } for po in overdue_pos: severity = po.get('severity', 'medium') by_severity[severity].append(po) # Log summary logger.warning( "Overdue POs detected", total=len(overdue_pos), critical=len(by_severity['critical']), high=len(by_severity['high']), medium=len(by_severity['medium']), low=len(by_severity['low']) ) # Publish events for critical and high severity if self.rabbitmq_client and self.rabbitmq_client.connected: critical_and_high = by_severity['critical'] + by_severity['high'] for po in critical_and_high: await self._publish_overdue_alert(po) logger.info( "Published overdue alerts", count=len(critical_and_high) ) else: logger.warning( "RabbitMQ not available, skipping alert publishing", overdue_count=len(by_severity['critical'] + by_severity['high']) ) except Exception as e: logger.error( "Error in overdue PO detection cycle", error=str(e), exc_info=True ) async def _publish_overdue_alert(self, po_summary: dict): """ Publish an overdue PO alert event. Args: po_summary: Overdue PO summary from detector """ try: event_data = { 'po_id': po_summary['po_id'], 'tenant_id': po_summary['tenant_id'], 'po_number': po_summary['po_number'], 'supplier_id': po_summary['supplier_id'], 'status': po_summary['status'], 'total_amount': po_summary['total_amount'], 'currency': po_summary['currency'], 'estimated_delivery_date': po_summary['estimated_delivery_date'], 'days_overdue': po_summary['days_overdue'], 'severity': po_summary['severity'], 'priority': po_summary['priority'], 'detected_at': datetime.now(timezone.utc).isoformat() } # Create event data structure event_data_full = { 'service_name': 'procurement', 'event_type': 'po.overdue_detected', 'timestamp': datetime.now(timezone.utc).isoformat(), **event_data # Include the original event_data } # Publish to RabbitMQ success = await self.rabbitmq_client.publish_event( exchange_name='procurement.events', routing_key='po.overdue', event_data=event_data_full, persistent=True ) if success: logger.info( "Published overdue alert", po_number=po_summary['po_number'], days_overdue=po_summary['days_overdue'], severity=po_summary['severity'] ) else: logger.error( "Failed to publish overdue alert", po_number=po_summary['po_number'] ) except Exception as e: logger.error( "Error publishing overdue alert", error=str(e), po_number=po_summary.get('po_number'), exc_info=True )