# services/inventory/app/main.py """ Inventory Service FastAPI Application """ import os from fastapi import FastAPI from sqlalchemy import text # Import core modules from app.core.config import settings from app.core.database import database_manager from app.services.inventory_alert_service import InventoryAlertService from app.consumers.delivery_event_consumer import DeliveryEventConsumer from shared.service_base import StandardFastAPIService import asyncio from app.api import ( ingredients, stock_entries, transformations, inventory_operations, food_safety_compliance, temperature_logs, food_safety_alerts, food_safety_operations, dashboard, analytics, sustainability, internal_demo, audit, ml_insights ) class InventoryService(StandardFastAPIService): """Inventory Service with standardized setup""" expected_migration_version = "20251123_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 inventory_expected_tables = [ 'ingredients', 'stock', 'stock_movements', 'product_transformations', 'stock_alerts', 'food_safety_compliance', 'temperature_logs', 'food_safety_alerts' ] # Initialize delivery consumer and rabbitmq client self.delivery_consumer = None self.delivery_consumer_task = None self.rabbitmq_client = None super().__init__( service_name="inventory-service", app_name=settings.APP_NAME, description=settings.DESCRIPTION, version=settings.VERSION, log_level=settings.LOG_LEVEL, cors_origins=settings.CORS_ORIGINS, api_prefix="", # Empty because RouteBuilder already includes /api/v1 database_manager=database_manager, expected_tables=inventory_expected_tables, enable_messaging=True # Enable RabbitMQ for event consumption ) async def _setup_messaging(self): """Setup messaging for inventory service""" from shared.messaging.rabbitmq import RabbitMQClient try: self.rabbitmq_client = RabbitMQClient(settings.RABBITMQ_URL, service_name="inventory-service") await self.rabbitmq_client.connect() self.logger.info("Inventory service messaging setup completed") except Exception as e: self.logger.error("Failed to setup inventory messaging", error=str(e)) raise async def _cleanup_messaging(self): """Cleanup messaging for inventory service""" try: if self.rabbitmq_client: await self.rabbitmq_client.disconnect() self.logger.info("Inventory service messaging cleanup completed") except Exception as e: self.logger.error("Error during inventory messaging cleanup", error=str(e)) async def on_startup(self, app: FastAPI): """Custom startup logic for inventory service""" # Verify migrations first await self.verify_migrations() # Call parent startup (includes database, messaging, etc.) await super().on_startup(app) # Initialize alert service alert_service = InventoryAlertService(settings) await alert_service.start() self.logger.info("Inventory alert service started") # Store alert service in app state app.state.alert_service = alert_service # Initialize and start delivery event consumer self.delivery_consumer = DeliveryEventConsumer() # Start consuming delivery.received events in background if self.rabbitmq_client and self.rabbitmq_client.connected: self.delivery_consumer_task = asyncio.create_task( self.delivery_consumer.consume_delivery_received_events(self.rabbitmq_client) ) self.logger.info("Delivery event consumer started successfully") else: self.logger.warning("RabbitMQ not connected, delivery event consumer not started") app.state.delivery_consumer = self.delivery_consumer async def on_shutdown(self, app: FastAPI): """Custom shutdown logic for inventory service""" # Cancel delivery consumer task if self.delivery_consumer_task and not self.delivery_consumer_task.done(): self.delivery_consumer_task.cancel() try: await self.delivery_consumer_task except asyncio.CancelledError: self.logger.info("Delivery event consumer task cancelled") # Stop alert service if hasattr(app.state, 'alert_service'): await app.state.alert_service.stop() self.logger.info("Alert service stopped") await super().on_shutdown(app) def get_service_features(self): """Return inventory-specific features""" return [ "ingredient_management", "stock_tracking", "expiration_alerts", "low_stock_alerts", "batch_tracking", "fifo_consumption", "barcode_support", "food_safety_compliance", "temperature_monitoring", "dashboard_analytics", "business_model_detection", "real_time_alerts", "regulatory_reporting", "sustainability_tracking", "sdg_compliance", "environmental_impact", "grant_reporting" ] # Create service instance service = InventoryService() # Create FastAPI app with standardized setup app = service.create_app() # Setup standard endpoints service.setup_standard_endpoints() # Include new standardized routers # IMPORTANT: Register audit router FIRST to avoid route matching conflicts service.add_router(audit.router) service.add_router(ingredients.router) service.add_router(stock_entries.router) service.add_router(transformations.router) service.add_router(inventory_operations.router) service.add_router(food_safety_compliance.router) service.add_router(temperature_logs.router) service.add_router(food_safety_alerts.router) service.add_router(food_safety_operations.router) service.add_router(dashboard.router) service.add_router(analytics.router) service.add_router(sustainability.router) service.add_router(internal_demo.router) service.add_router(ml_insights.router) # ML insights endpoint if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host="0.0.0.0", port=8000, reload=os.getenv("RELOAD", "false").lower() == "true", log_level="info" )