MukeshKapoor25's picture
refactor(routing): externalize API path configuration for API gateway compatibility
518b9dc
"""
Main FastAPI application for POS Microservice.
"""
import logging
import os
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pymongo.errors import PyMongoError, ConnectionFailure, OperationFailure
from jose import JWTError
from app.core.config import settings
from app.core.logging import get_logger
from app.utils.response import error_response
from app.middleware.logging_middleware import RequestLoggingMiddleware
from app.nosql import connect_to_mongo, close_mongo_connection
from app.sql import connect_to_database, disconnect_from_database
from app.staff.controllers.router import router as staff_router
from app.catalogue_services.controllers.router import router as catalogue_services_router
from app.customers.controllers.router import router as customers_router
from app.sales.retail.controllers.router import router as sales_router
from app.appointments.controllers.router import router as appointments_router
logger = get_logger(__name__)
logging.basicConfig(level=logging.INFO)
# Create FastAPI app
app = FastAPI(
title="POS Microservice",
description="Point of Sale System - Sales, Inventory, Customer & Payment Management",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
root_path=os.getenv("ROOT_PATH", ""),
)
# Request logging middleware
app.add_middleware(RequestLoggingMiddleware)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Configure properly for production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global Exception Handlers
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Handle validation errors"""
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=error_response(
error="Validation Error",
detail="The request contains invalid data",
errors=errors,
request_id=getattr(request.state, "request_id", None)
)
)
@app.exception_handler(JWTError)
async def jwt_exception_handler(request: Request, exc: JWTError):
"""Handle JWT errors"""
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=error_response(
error="Unauthorized",
detail="Invalid or expired token",
request_id=getattr(request.state, "request_id", None)
)
)
@app.exception_handler(status.HTTP_404_NOT_FOUND)
async def not_found_exception_handler(request: Request, exc: Exception):
"""Handle 404 errors"""
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content=error_response(
error="Not Found",
detail="Resource not found",
request_id=getattr(request.state, "request_id", None)
)
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
"""Handle standard HTTP exceptions"""
error_title = "Error"
headers = None
if exc.status_code == 401:
error_title = "Authentication Error"
headers = {"WWW-Authenticate": "Bearer"}
elif exc.status_code == 403:
error_title = "Permission Denied"
elif exc.status_code == 404:
error_title = "Not Found"
elif exc.status_code == 422:
error_title = "Validation Error"
return JSONResponse(
status_code=exc.status_code,
content=error_response(
error=error_title,
detail=str(exc.detail),
request_id=getattr(request.state, "request_id", None),
headers=headers
)
)
@app.exception_handler(PyMongoError)
async def mongodb_exception_handler(request: Request, exc: PyMongoError):
"""Handle MongoDB errors"""
logger.error(
"Database error",
extra={
"path": request.url.path,
"error": str(exc),
"error_type": type(exc).__name__
},
exc_info=True
)
if isinstance(exc, ConnectionFailure):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
detail = "Database connection failed"
elif isinstance(exc, OperationFailure):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
detail = "Database operation failed"
else:
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
detail = "Database error occurred"
return JSONResponse(
status_code=status_code,
content=error_response(
error="Database Error",
detail=detail,
request_id=getattr(request.state, "request_id", None)
)
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""
Handle all unhandled exceptions.
"""
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=error_response(
error="Internal Server Error",
detail="An unexpected error occurred. Please try again later.",
request_id=getattr(request.state, "request_id", None)
)
)
# Startup and shutdown events
@app.on_event("startup")
async def startup_event():
"""Initialize connections on startup"""
logger.info("Starting POS Microservice")
logger.info(f"DEBUG mode: {settings.DEBUG}")
logger.info(f"JWT Algorithm: {settings.ALGORITHM}")
logger.info(f"Token Expiration: {settings.TOKEN_EXPIRATION_HOURS} hours")
await connect_to_mongo()
await connect_to_database()
logger.info("POS Microservice started successfully")
@app.on_event("shutdown")
async def shutdown_event():
"""Close connections on shutdown"""
logger.info("Shutting down POS Microservice")
await close_mongo_connection()
await disconnect_from_database()
logger.info("POS Microservice shut down successfully")
# Health check endpoint
@app.get("/health", tags=["health"])
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "pos-microservice",
"version": "1.0.0"
}
# Token debug endpoint (for troubleshooting)
@app.post("/debug/verify-token", tags=["debug"])
async def debug_verify_token(token: str):
"""Debug endpoint to verify token format and claims (requires valid token for other endpoints)"""
from app.dependencies.auth import verify_token
result = {
"token_length": len(token),
"token_parts": len(token.split('.')),
"valid": False,
"payload": None,
"error": None
}
if len(token.split('.')) != 3:
result["error"] = "Token does not have 3 parts (invalid JWT format)"
return result
payload = verify_token(token)
result["valid"] = payload is not None
result["payload"] = payload
if payload is None:
result["error"] = "Token verification failed - check SECRET_KEY and signature"
return result
# Include routers
# Authentication is handled by auth-ms, POS just validates tokens
app.include_router(staff_router)
app.include_router(catalogue_services_router)
app.include_router(customers_router)
app.include_router(sales_router)
app.include_router(appointments_router)
# Import and include wallet router
from app.wallet.controllers.wallet_router import router as wallet_router
app.include_router(wallet_router)
# TODO: Add other POS-specific routers as they are implemented:
# app.include_router(sales_router, prefix="/api/v1")
# app.include_router(inventory_router, prefix="/api/v1")
# app.include_router(customer_router, prefix="/api/v1")
# app.include_router(product_router, prefix="/api/v1")
# app.include_router(payment_router, prefix="/api/v1")
# app.include_router(report_router, prefix="/api/v1"r)
# app.include_router(payment_router)
# app.include_router(report_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8002,
reload=True,
log_level="info"
)