# ================================================================ # services/procurement/app/main.py # ================================================================ """ Procurement Service - FastAPI Application Procurement planning, purchase order management, and supplier integration """ from fastapi import FastAPI, Request from sqlalchemy import text from app.core.config import settings from app.core.database import database_manager from shared.service_base import StandardFastAPIService from app.jobs.overdue_po_scheduler import OverduePOScheduler class ProcurementService(StandardFastAPIService): """Procurement Service with standardized setup""" expected_migration_version = "001_unified_initial_schema" async def verify_migrations(self): """Verify database schema matches the latest migrations""" try: async with self.database_manager.get_session() as session: result = await session.execute(text("SELECT version_num FROM alembic_version")) version = result.scalar() if version != self.expected_migration_version: self.logger.error(f"Migration version mismatch: expected {self.expected_migration_version}, got {version}") raise RuntimeError(f"Migration version mismatch: expected {self.expected_migration_version}, got {version}") self.logger.info(f"Migration verification successful: {version}") except Exception as e: self.logger.error(f"Migration verification failed: {e}") raise def __init__(self): # Define expected database tables for health checks procurement_expected_tables = [ 'procurement_plans', 'procurement_requirements', 'purchase_orders', 'purchase_order_items', 'deliveries', 'delivery_items', 'supplier_invoices', 'replenishment_plans', 'replenishment_plan_items', 'inventory_projections', 'supplier_allocations', 'supplier_selection_history' ] # Initialize scheduler, delivery tracking, and rabbitmq client self.overdue_po_scheduler = None self.delivery_tracking_service = None self.rabbitmq_client = None self.event_publisher = None super().__init__( service_name="procurement-service", app_name=settings.APP_NAME, description=settings.DESCRIPTION, version=settings.VERSION, api_prefix="", # Empty because RouteBuilder already includes /api/v1 database_manager=database_manager, expected_tables=procurement_expected_tables, enable_messaging=True # Enable RabbitMQ for event publishing ) async def _setup_messaging(self): """Setup messaging for procurement service""" from shared.messaging import RabbitMQClient, UnifiedEventPublisher try: self.rabbitmq_client = RabbitMQClient(settings.RABBITMQ_URL, service_name="procurement-service") await self.rabbitmq_client.connect() # Create unified event publisher self.event_publisher = UnifiedEventPublisher(self.rabbitmq_client, "procurement-service") self.logger.info("Procurement service messaging setup completed") except Exception as e: self.logger.error("Failed to setup procurement messaging", error=str(e)) raise async def _cleanup_messaging(self): """Cleanup messaging for procurement service""" try: if self.rabbitmq_client: await self.rabbitmq_client.disconnect() self.logger.info("Procurement service messaging cleanup completed") except Exception as e: self.logger.error("Error during procurement messaging cleanup", error=str(e)) async def on_startup(self, app: FastAPI): """Custom startup logic for procurement service""" await super().on_startup(app) self.logger.info("Procurement Service starting up...") # Start delivery tracking service (APScheduler with leader election) from app.services.delivery_tracking_service import DeliveryTrackingService self.delivery_tracking_service = DeliveryTrackingService( event_publisher=self.event_publisher, config=settings, database_manager=self.database_manager ) await self.delivery_tracking_service.start() self.logger.info("Delivery tracking service started") # Store in app state for internal API access app.state.delivery_tracking_service = self.delivery_tracking_service app.state.event_publisher = self.event_publisher # Start overdue PO scheduler if self.rabbitmq_client and self.rabbitmq_client.connected: self.overdue_po_scheduler = OverduePOScheduler( rabbitmq_client=self.rabbitmq_client, check_interval_seconds=3600 # Check every hour ) await self.overdue_po_scheduler.start() self.logger.info("Overdue PO scheduler started") else: self.logger.warning("RabbitMQ not available, overdue PO scheduler not started") async def on_shutdown(self, app: FastAPI): """Custom shutdown logic for procurement service""" self.logger.info("Procurement Service shutting down...") # Stop delivery tracking service if self.delivery_tracking_service: await self.delivery_tracking_service.stop() self.logger.info("Delivery tracking service stopped") # Stop overdue PO scheduler if self.overdue_po_scheduler: await self.overdue_po_scheduler.stop() self.logger.info("Overdue PO scheduler stopped") await super().on_shutdown(app) def get_service_features(self): """Return procurement-specific features""" return [ "procurement_planning", "purchase_order_management", "delivery_tracking", "invoice_management", "supplier_integration", "local_production_support", "recipe_explosion" ] # Create service instance service = ProcurementService() # Create FastAPI app with standardized setup app = service.create_app() # Setup standard endpoints (health, readiness, metrics) service.setup_standard_endpoints() # Include routers from app.api.procurement_plans import router as procurement_plans_router from app.api.purchase_orders import router as purchase_orders_router from app.api import internal_transfer # Internal Transfer Routes from app.api import replenishment # Enhanced Replenishment Planning Routes from app.api import analytics # Procurement Analytics Routes from app.api import internal_delivery # Internal Delivery Tracking Routes from app.api import ml_insights # ML insights endpoint from app.api import internal_demo # Internal demo data cloning from app.api.expected_deliveries import router as expected_deliveries_router # Expected Deliveries Routes from app.api.internal_delivery_tracking import router as internal_delivery_tracking_router # NEW: Internal trigger endpoint service.add_router(procurement_plans_router) service.add_router(purchase_orders_router) service.add_router(internal_transfer.router, tags=["internal-transfer"]) # Internal transfer routes service.add_router(replenishment.router, tags=["replenishment"]) # RouteBuilder already includes full path service.add_router(analytics.router, tags=["analytics"]) # RouteBuilder already includes full path service.add_router(internal_demo.router, tags=["internal-demo"]) # Internal demo data cloning service.add_router(internal_delivery.router, tags=["internal-delivery"]) # Internal delivery tracking service.add_router(internal_delivery_tracking_router, tags=["internal-delivery-tracking"]) # NEW: Delivery alert trigger service.add_router(ml_insights.router) # ML insights endpoint service.add_router(ml_insights.internal_router) # Internal ML insights endpoint service.add_router(expected_deliveries_router, tags=["expected-deliveries"]) # Expected deliveries endpoint @app.middleware("http") async def logging_middleware(request: Request, call_next): """Add request logging middleware""" import time start_time = time.time() response = await call_next(request) process_time = time.time() - start_time service.logger.info("HTTP request processed", method=request.method, url=str(request.url), status_code=response.status_code, process_time=round(process_time, 4)) return response if __name__ == "__main__": import uvicorn uvicorn.run( "main:app", host="0.0.0.0", port=8000, reload=settings.DEBUG )