""" Main FastAPI application for Tracker Microservice. """ import os from fastapi import FastAPI, Request, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError from jose import JWTError from app.core.config import settings from app.core.logging import setup_logging, get_logger from app.nosql import connect_to_mongo, close_mongo_connection from app.postgres import connect_to_postgres, close_postgres_connection from app.tracker.attendance.router import router as attendance_router from app.tracker.tasks.router import router as tasks_router # Initialize logging first log_level = getattr(settings, 'LOG_LEVEL', 'INFO').strip().upper() if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: log_level = 'INFO' setup_logging( level=log_level, format_type="colored", app_name="tracker-microservice", include_correlation=True ) logger = get_logger(__name__) # Create FastAPI app app = FastAPI( title=settings.APP_NAME, description="Employee Tracker - Attendance and Location Tracking", version=settings.APP_VERSION, docs_url="/docs", redoc_url="/redoc", root_path=os.getenv("ROOT_PATH", ""), ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], expose_headers=["*"], ) # Startup and shutdown events @app.on_event("startup") async def startup_event(): """Initialize connections on startup""" logger.info("Starting Tracker Microservice") await connect_to_mongo() await connect_to_postgres() # Create schema and tables try: from app.tracker.attendance.models import ScmAttendance conn = None try: from app.postgres import get_postgres_connection, release_postgres_connection conn = await get_postgres_connection() # Create trans schema await conn.execute("CREATE SCHEMA IF NOT EXISTS trans") logger.info("✅ TRANS schema exists") # Create table create_table_sql = """ CREATE TABLE IF NOT EXISTS trans.scm_attendance ( id UUID PRIMARY KEY, merchant_id UUID NOT NULL, employee_id UUID NOT NULL, work_date DATE NOT NULL, check_in_time BIGINT, check_in_lat DOUBLE PRECISION, check_in_lon DOUBLE PRECISION, check_in_geofence_id UUID, check_out_time BIGINT, check_out_lat DOUBLE PRECISION, check_out_lon DOUBLE PRECISION, total_minutes INTEGER, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now(), UNIQUE (employee_id, work_date) ) """ await conn.execute(create_table_sql) logger.info("✅ scm_attendance table created/verified") # Create indexes index_sql = """ CREATE INDEX IF NOT EXISTS idx_scm_attendance_work_date ON trans.scm_attendance (employee_id, work_date) """ await conn.execute(index_sql) index_merchant_sql = """ CREATE INDEX IF NOT EXISTS idx_scm_attendance_merchant ON trans.scm_attendance (merchant_id, work_date) """ await conn.execute(index_merchant_sql) logger.info("✅ Indexes created/verified") finally: if conn: await release_postgres_connection(conn) except Exception as e: logger.error("Failed to create database schema", exc_info=e) raise logger.info("Tracker Microservice started successfully") @app.on_event("shutdown") async def shutdown_event(): """Close connections on shutdown""" logger.info("Shutting down Tracker Microservice") await close_mongo_connection() await close_postgres_connection() logger.info("Tracker Microservice shut down successfully") # Health check endpoint @app.get("/health", tags=["health"]) async def health_check(): """Health check endpoint""" return { "status": "healthy", "service": "tracker-microservice", "version": settings.APP_VERSION } # Include routers app.include_router(attendance_router, prefix="/tracker") app.include_router(tasks_router, prefix="/tracker") # Global exception handlers @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): errors = [ { "field": " -> ".join(str(loc) for loc in error["loc"]), "message": error["msg"], "type": error["type"] } for error in exc.errors() ] logger.warning( "Validation error", extra={ "path": request.url.path, "method": request.method, "error_count": len(errors), "errors": errors } ) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={ "success": False, "error": "Validation Error", "errors": errors } ) @app.exception_handler(JWTError) async def jwt_exception_handler(request: Request, exc: JWTError): logger.warning( "JWT authentication failed", extra={ "path": request.url.path, "error": str(exc), "client_ip": request.client.host if request.client else None } ) return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "success": False, "error": "Unauthorized", "detail": "Invalid or expired token" } ) @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception): logger.error( "Unhandled exception", extra={ "method": request.method, "path": request.url.path, "error": str(exc), "error_type": type(exc).__name__, "client_ip": request.client.host if request.client else None }, exc_info=True ) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "success": False, "error": "Internal Server Error", "detail": "An unexpected error occurred" } ) if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host="0.0.0.0", port=int(os.getenv("PORT", "8003")), reload=True, log_level=log_level.lower() )