"""FastAPI application for ZUGFeRD invoice processing.""" import base64 import json import logging from datetime import datetime import uvicorn from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from src.extractor import ExtractionError, extract_zugferd from src.models import ( ExtractRequest, ExtractResponse, HealthResponse, ValidateRequest, ValidateResponse, ) from src.validator import validate_invoice class JSONFormatter(logging.Formatter): def format(self, record): log_data = { "timestamp": datetime.utcnow().isoformat() + "Z", "level": record.levelname, "message": record.getMessage(), } if hasattr(record, "data"): log_data["data"] = record.data return json.dumps(log_data) logger = logging.getLogger(__name__) if not logger.handlers: handler = logging.StreamHandler() handler.setFormatter(JSONFormatter()) logger.addHandler(handler) logger.setLevel(logging.INFO) app = FastAPI( title="ZUGFeRD Service", version="1.0.0", description="REST API for ZUGFeRD invoice extraction and validation", ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(ExtractionError) async def extraction_error_handler(request: Request, exc: ExtractionError): return JSONResponse( status_code=400, content={ "error": exc.error_code, "message": exc.message, "details": exc.details, }, ) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): if isinstance(exc.detail, dict) and "error" in exc.detail: return JSONResponse( status_code=exc.status_code, content={ "error": exc.detail.get("error"), "message": exc.detail.get("message"), }, ) return JSONResponse( status_code=exc.status_code, content={ "error": "http_error", "message": str(exc.detail), }, ) @app.exception_handler(Exception) async def generic_error_handler(request: Request, exc: Exception): logger.error(f"Internal error: {exc}") return JSONResponse( status_code=500, content={ "error": "internal_error", "message": "An internal error occurred", }, ) @app.get("/health", response_model=HealthResponse) async def health_check() -> HealthResponse: """Health check endpoint. Returns: HealthResponse with status and version. """ return HealthResponse(status="healthy", version="1.0.0") @app.post("/extract", response_model=ExtractResponse) async def extract_pdf(request: ExtractRequest) -> ExtractResponse: """Extract ZUGFeRD data from PDF. Args: request: ExtractRequest with pdf_base64 field Returns: ExtractResponse with extraction results """ try: pdf_bytes = base64.b64decode(request.pdf_base64) except Exception: raise HTTPException( status_code=400, detail={"error": "invalid_base64", "message": "Invalid base64 encoding"}, ) return extract_zugferd(pdf_bytes) @app.post("/validate", response_model=ValidateResponse) async def validate_invoice_endpoint(request: ValidateRequest) -> ValidateResponse: """Validate ZUGFeRD invoice data. Args: request: ValidateRequest with xml_data, pdf_text, checks Returns: ValidateResponse with validation results """ result = validate_invoice(request) return ValidateResponse(result=result) def run(host: str = "0.0.0.0", port: int = 5000) -> None: """Run the FastAPI application. Args: host: Host to bind to. port: Port to listen on. """ uvicorn.run(app, host=host, port=port)