Files
eamco_authorize/app/main.py
Edwin Eames 285cac54a3 feat: standardize main.py with health checks, structured logging, and API docs
- Add module-level docstring with endpoint documentation
- Add /health endpoint with database connectivity check
- Add /root endpoint redirecting to docs
- Add FastAPI metadata (title, description, version)
- Reorganize imports and add section separators
- Add startup/shutdown event handlers with emoji logging
- Add check_db_connection() helper using SQLAlchemy
2026-02-27 18:32:48 -05:00

278 lines
9.3 KiB
Python

"""
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")