""" eamco_authorize - FastAPI Payment Authorization Microservice. This microservice provides endpoints for managing payment processing through Authorize.net, including payment profiles, transactions, and auto-delivery billing. Endpoints: GET /health - Health check with database connectivity status POST /api/payment/... - Payment processing endpoints GET /api/transactions/... - Transaction management endpoints POST /api/auto/... - Auto-delivery billing endpoints GET /user/... - User verification endpoints Usage: # Development uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 # Production (Docker) docker run -p 8000:8000 eamco_authorize """ import logging import sys import uuid from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from starlette.middleware.base import BaseHTTPMiddleware from authorizenet import apicontractsv1 from authorizenet.apicontrollers import getCustomerProfileIdsController from authorizenet.constants import constants from .database import engine from . import models from .routers import payment from .routers.transaction import transaction_router from .routers.auto import auto_router from .routers.user_check import user_check_router from config import load_config from sqlalchemy import text # ============================================================================= # CONFIGURATION # ============================================================================= ApplicationConfig = load_config() # ============================================================================= # LOGGING CONFIGURATION # ============================================================================= def setup_logging(): """Configure structured logging for the application.""" log_level = logging.DEBUG if ApplicationConfig.CURRENT_SETTINGS != 'PRODUCTION' else logging.INFO formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) root_logger = logging.getLogger() root_logger.setLevel(log_level) root_logger.handlers.clear() console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(log_level) console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) # Reduce noise from uvicorn and other libs logging.getLogger('uvicorn.access').setLevel(logging.WARNING) return logging.getLogger('eamco_authorize') logger = setup_logging() # ============================================================================= # DATABASE SETUP # ============================================================================= models.Base.metadata.create_all(bind=engine) def check_db_connection(): """ Test database connectivity. """ try: with engine.connect() as conn: conn.execute(text("SELECT 1")) return True except Exception: return False # ============================================================================= # FASTAPI APPLICATION # ============================================================================= app = FastAPI( title="eamco_authorize", description="Payment authorization microservice using Authorize.net", version="1.0.0", docs_url="/docs", redoc_url="/redoc", ) # ============================================================================= # MIDDLEWARE # ============================================================================= class RequestIDMiddleware(BaseHTTPMiddleware): """Request ID middleware for request tracking/correlation.""" async def dispatch(self, request: Request, call_next): request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())[:8] request.state.request_id = request_id response = await call_next(request) response.headers["X-Request-ID"] = request_id return response app.add_middleware(RequestIDMiddleware) app.add_middleware( CORSMiddleware, allow_origins=ApplicationConfig.origins, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-Request-ID"], ) # ============================================================================= # ROUTERS # ============================================================================= app.include_router(payment.router, prefix="/api", tags=["payment"]) app.include_router(transaction_router, prefix="/api", tags=["transactions"]) app.include_router(auto_router, prefix="/api", tags=["auto"]) app.include_router(user_check_router, prefix="/user", tags=["usercheck"]) # ============================================================================= # ENDPOINTS # ============================================================================= @app.get("/", include_in_schema=False) async def root(): """Root endpoint - redirect to docs.""" return { "service": "eamco_authorize", "version": "1.0.0", "docs": "/docs", } @app.get("/health", tags=["Health"]) async def health_check(): """ Health check endpoint. Returns service status and database connectivity. Use this endpoint for container health checks and monitoring. Returns: JSON with status and db_connected flag """ db_connected = check_db_connection() return { "status": "healthy" if db_connected else "degraded", "db_connected": db_connected, } # ============================================================================= # CREDENTIALS VALIDATION # ============================================================================= def validate_authorize_credentials(): """ Validates Authorize.net credentials at startup. Returns True if credentials are valid, raises exception if not. """ api_login_id = ApplicationConfig.API_LOGIN_ID transaction_key = ApplicationConfig.TRANSACTION_KEY # Check if credentials are present if not api_login_id or not api_login_id.strip(): raise ValueError("AUTHORIZE_API_LOGIN_ID is not configured") if not transaction_key or not transaction_key.strip(): raise ValueError("AUTHORIZE_TRANSACTION_KEY is not configured") # Test the credentials by making a simple API call merchantAuth = apicontractsv1.merchantAuthenticationType( name=api_login_id, transactionKey=transaction_key ) request = apicontractsv1.getCustomerProfileIdsRequest( merchantAuthentication=merchantAuth ) controller = getCustomerProfileIdsController(request) # Set environment based on config if ApplicationConfig.CURRENT_SETTINGS in ['PRODUCTION', 'LOCAL']: controller.setenvironment(constants.PRODUCTION) else: controller.setenvironment(constants.SANDBOX) # Suppress the SDK's XML parsing error (ContentNondeterminismExceededError) # This is a known issue with the authorizenet SDK's PyXB dependency # The error doesn't affect functionality, just creates noise in logs authorizenet_logger = logging.getLogger('authorizenet.sdk') original_level = authorizenet_logger.level authorizenet_logger.setLevel(logging.CRITICAL) try: controller.execute() response = controller.getresponse() finally: # Restore original logging level authorizenet_logger.setLevel(original_level) if response is None: raise ValueError("Could not connect to Authorize.net - check network connectivity") if response.messages.resultCode != "Ok": error_code = response.messages.message[0].code if response.messages.message else "Unknown" error_text = response.messages.message[0].text if response.messages.message else "Unknown error" raise ValueError(f"Authorize.net credential validation failed: {error_code} - {error_text}") return True # ============================================================================= # STARTUP/SHUTDOWN EVENTS # ============================================================================= @app.on_event("startup") async def startup_event(): """Application startup - validate payment credentials and test DB connection.""" logger.info("🚀 eamco_authorize STARTING") mode = ApplicationConfig.CURRENT_SETTINGS.upper() if mode in ['DEVELOPMENT', 'DEV']: logger.info("🤖🤖🤖🤖🤖 Mode: Development 🤖🤖🤖🤖🤖") elif mode in ['PRODUCTION', 'PROD']: logger.info("💀💀💀💀💀💀💀💀💀💀 ⚠️ WARNING PRODUCTION 💀💀💀💀💀💀💀💀💀💀") logger.info(f"CORS: {len(ApplicationConfig.origins)} origins configured") # Test database connection if check_db_connection(): logger.info("DB Connection: ✅ OK") else: logger.warning("DB Connection: ❌ FAILED") # Validate Authorize.net credentials try: validate_authorize_credentials() logger.info("Authorize.net credentials: ✅ VALID") except ValueError as e: logger.critical(f"PAYMENT CREDENTIALS INVALID: {e}") logger.critical("Service will start but ALL PAYMENT OPERATIONS WILL FAIL") @app.on_event("shutdown") async def shutdown_event(): """Application shutdown - cleanup.""" logger.info("🛑 eamco_authorize SHUTTING DOWN")