Files
zugferd-service/src/main.py

155 lines
4.0 KiB
Python

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