This page traces a single electronic invoice request from the exact moment it enters the FastAPI controller — as raw JSON bytes on a TCP socket — to the moment a DIAN track ID and CUFE are serialised back to the caller. Every layer crossing is explicit: HTTP deserialization, dependency injection, business validation, domain mapping, PostgreSQL audit logging, async SOAP dispatch, and HTTP response. Understanding this flow is essential for debugging, for extending the system with new document types, and for reasoning about where failures originate.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/farojas85/fast-rest-api/llms.txt
Use this file to discover all available pages before exploring further.
The 7-Step Flow
HTTP Request
The client sends
POST /api/v1/pedidos/consumo-local/ with a JSON body describing the restaurant order. FastAPI reads the request body and deserialises it into a PedidoLocalCreateRequest Pydantic model, running full field-type coercion and constraint validation (e.g. items must have at least one element, cantidad must be > 0). Any schema violation is rejected immediately with HTTP 422 Unprocessable Entity before the route handler is ever invoked.Dependency Injection
FastAPI evaluates the
Depends() graph declared on the route handler before calling it. First, get_dian_adapter() is called: it reads settings.ENVIRONMENT to select the correct WSDL URL (habilitación vs. producción), then constructs a DianSoapAdapter with the WSDL URL, PFX certificate path, and certificate password. Second, get_use_case() receives the adapter via Depends(get_dian_adapter) and constructs FacturarPedidoLocalUseCase, injecting the adapter as the dian_port argument.Business Validation
FacturarPedidoLocalUseCase.execute() performs domain-level validation that cannot be expressed as a Pydantic field constraint: it checks that total_factura equals the arithmetic sum of subtotal + impuestos_totales + propina_voluntaria. The voluntary tip (propina_voluntaria) is included in the invoice total but is not part of the taxable base — a Colombian DIAN fiscal rule — so the validation confirms the totals are self-consistent before any external call is attempted.ValueError here returns HTTP 500 to the caller and no log entry is written, since the request was malformed at the business level.Domain Mapping
The validated Pydantic DTO (
PedidoLocalCreateRequest) is mapped field-by-field into pure domain entities. Each ItemPedidoRequest becomes an ItemPedido with its nested list of ConceptoImpuesto. Each PagoRequest becomes a MetodoPago. The optional AdquirenteRequest becomes an Adquirente; if omitted, an Adquirente() is constructed using Colombian fiscal defaults (tipo_documento="13", numero_documento="222222222222", razon_social="Consumidor Final"). All assembled into a PedidoLocal domain entity.This mapping step is the architectural boundary: from here forward, only domain types travel through the application core. Infrastructure types (PedidoLocalCreateRequest, Pydantic HTTP schemas) do not cross into the DIAN port call.Log Attempt (PROCESANDO)
Before any outbound network call is made, the use case persists the attempt to the audit log via This guarantees that even if the DIAN SOAP call never returns (network timeout, process crash), a
IInvoiceLogRepositoryPort.save_log():PROCESANDO record exists in PostgreSQL with the full inbound payload. Operations teams can query for stale PROCESANDO entries to identify stuck invoices.DIAN SOAP Call
IDianSoapPort.send_invoice(invoice_data) is awaited. The concrete DianSoapAdapter uses a zeep.AsyncClient backed by an httpx.AsyncClient with a 30-second timeout to call service.SendTestSetAsync(fileName, contentFile, testSetId) against the DIAN WSDL:Response and Log Update
On success, the use case extracts On failure, the exception is caught, the log is updated with
track_id, cufe, and status from the DIAN response dict, updates the audit log with status="SUCCESS" and the full response payload, then returns a PedidoLocalResponse:status="FAILED" and the error string, and the exception is re-raised so the controller can return HTTP 500:WSDL Environment Selection
The adapter is configured with the correct DIAN WSDL endpoint at dependency injection time.settings.ENVIRONMENT drives the selection — any value other than "production" routes to the DIAN habilitación (testing) environment:
DIAN_WSDL_URL_HABILITACION and DIAN_WSDL_URL_PRODUCCION) are required fields in Settings — the application fails at startup if either is missing from the environment, preventing a misconfigured container from ever serving traffic.
Error Handling
DianSoapAdapter.send_invoice() catches all exceptions internally and converts them to a normalised error dict rather than propagating zeep or httpx exceptions up through the application layer:
| Condition | error value | Meaning |
|---|---|---|
| DIAN did not respond within 30 s | "Timeout" | httpx.TimeoutException from the async transport |
| Any other exception | type(e).__name__ | WSDL parse error, connection refused, XML fault, etc. |
result.get("success") on the returned PedidoLocalResponse. If False, it returns HTTP 500 with the error detail: