Skip to content

Facturación Electrónica

Facturación Electrónica [NUEVO]

Prefijo: /api/v1/facturacion
Requiere Auth: Sí
Permiso requerido: facturacion, *

Módulo versionado que integra Factus (proveedor de facturación electrónica DIAN Colombia). La Fase 1 (Factus v1) permite emitir una factura electrónica a partir de una venta existente, con credenciales por tenant (cada cliente tiene su propio NIT/emisor ante la DIAN).

Notas de negocio

  • El POS nunca habla con Factus directamente: solo con NeoPOS API. El backend resuelve las credenciales del tenant (cifradas en factus_config), obtiene el token OAuth (cache por tenant ~1h) y llama a Factus.
  • Credenciales por tenant: cada cliente tiene su propio NIT → PUT /config guarda client_id/client_secret/username/password. Los secretos se cifran con Fernet y nunca se devuelven en respuestas ni se loguean.
  • Idempotencia: re-emitir la misma venta devuelve la factura ya generada (no duplica). UNIQUE(tenant_id, venta_id) en facturas_electronicas.
  • Resultado persistido: la emisión guarda numero_factura, cufe, qr, estado en facturas_electronicas y actualiza detalle (cufe, NumeroFact, numeroBill, estadoFact, fechaValidacion).
  • Emisor: el NIT de la empresa lo toma Factus de la suscripción; no se envía en el payload.
  • Versionado: v1 = Factus v1. En el futuro /api/v2/facturacion implementará Factus v2 sin romper v1.

POST /api/v1/facturacion/ventas/{venta_id}/emitir [NUEVO]

Descripción: Emite la factura electrónica de una venta existente. Lee venta + detalles + cliente + impuestos de la BD (aislado por tenant_id), resuelve credenciales Factus del tenant, obtiene token y envía a POST {api_url}/v1/bills/validate. Úsalo cuando una venta del POS debe generar su factura electrónica ante la DIAN. Requiere Auth: Sí Permiso requerido: facturacion, crear

Parámetros path:

ParámetroTipoObligatorioDescripción
venta_idint✅ID de la venta a facturar

Comportamiento / Casos de uso:

  • Venta POS confirmada → el POS (o el admin) llama a este endpoint para emitir la FE.
  • Si la venta ya fue emitida, devuelve la factura existente (idempotente, no duplica).
  • Si el tenant no tiene credenciales configuradas → 400 (configurar con PUT /config).

Respuesta exitosa: 201 Created

Response:

{
"venta_id": 123,
"reference_code": "NV-123",
"numero_factura": "FES-1045",
"cufe": "9e7f1a...",
"qr": "https://...qr.png",
"estado": "VALIDADA",
"total": 15000.00
}

Errores:

CódigoCausaSolución
400Venta sin cliente/items, o tenant sin credenciales FactusPUT /config o revisar la venta
401Token inválido/expiradoRe-login
403Sin permiso facturacion, crearSolicitar permiso al admin
404Venta no encontrada o no es del tenantVerificar ID
409Factura pendiente por enviar a la DIANEliminar y re-emitir
422Validación fallida en FactusRevisar datos cliente/items
502Factus caído o credenciales inválidasVerificar api_url y credenciales

cURL:

Terminal window
curl -X POST "http://localhost/api/v1/facturacion/ventas/123/emitir" \
-H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.post("http://localhost/api/v1/facturacion/ventas/123/emitir",
headers={"Authorization": f"Bearer {token}"})
data = resp.json()

JavaScript:

const resp = await fetch('http://localhost/api/v1/facturacion/ventas/123/emitir', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await resp.json();

GET /api/v1/facturacion/ventas/{venta_id}/estado [NUEVO]

Descripción: Consulta el estado de emisión de una venta sin llamar a Factus. Útil para la UI de “Ventas electrónicas” y para verificar si una venta ya tiene CUFE/número. Requiere Auth: Sí Permiso requerido: facturacion, leer

Respuesta exitosa: 200 OK

Response:

{
"venta_id": 123,
"emitida": true,
"estado": "VALIDADA",
"numero_factura": "FES-1045",
"cufe": "9e7f1a..."
}

cURL:

Terminal window
curl "http://localhost/api/v1/facturacion/ventas/123/estado" -H "Authorization: Bearer <token>"

PUT /api/v1/facturacion/config [NUEVO]

Descripción: Guarda (upsert) las credenciales Factus del tenant autenticado. Los secretos se cifran con Fernet y nunca se devuelven. Úsalo para dar de alta el emisor (NIT) de cada cliente. Requiere Auth: Sí Permiso requerido: Superadmin (rol=3) y facturacion, actualizar

Body request:

{
"client_id": "9fdc88cb-f45d-4667-8465-aa7035d3d11b",
"client_secret": "zh9eRdDoks7zYoL5FBRWQDbHyjZx2fHacOofGuRD",
"username": "facturacion@empresa.com",
"password": "clave-segura",
"api_url": "https://api-sandbox.factus.com.co",
"municipality_id": 11001,
"numbering_range_id": 157,
"activo": true
}

Respuesta exitosa: 200 OK

Response:

{
"tenant_id": "90af7e06-4211-4f99-82b7-adcf9b0a2382",
"client_id": "9fdc88cb-f45d-4667-8465-aa7035d3d11b",
"username": "facturacion@empresa.com",
"api_url": "https://api-sandbox.factus.com.co",
"municipality_id": 11001,
"numbering_range_id": 157,
"activo": true
}

numbering_range_id es opcional. Si se omite, la emisión no envía el campo y Factus usa el único rango activo; si el tenant tiene más de uno, Factus responde 422 y hay que elegirlo con GET /api/v1/facturacion/rangos.

cURL:

Terminal window
curl -X PUT "http://localhost/api/v1/facturacion/config" \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"client_id":"...","client_secret":"...","username":"...","password":"...","api_url":"https://api-sandbox.factus.com.co","municipality_id":11001,"numbering_range_id":157}'

GET /api/v1/facturacion/config [NUEVO]

Descripción: Devuelve la configuración Factus del tenant enmascarada (sin client_secret ni password). Útil para verificar qué tenant ya tiene emisor configurado. Requiere Auth: Sí Permiso requerido: Superadmin (rol=3) y facturacion, leer

Respuesta exitosa: 200 OK (misma estructura que PUT /config, sin secretos)
Errores: 403 (no es Superadmin), 404 si el tenant no tiene configuración.

cURL:

Terminal window
curl "http://localhost/api/v1/facturacion/config" -H "Authorization: Bearer <token>"

GET /api/v1/facturacion/suscripcion

Descripción: Devuelve si el tenant tiene Factus activo y, si aplica, el cupo, consumo y vencimiento de su suscripción. El Administrador (rol=1) puede consultar solo su propio tenant; Superadmin puede consultar cualquier tenant. Otros roles reciben 403. Requiere Auth: Sí Permiso requerido: facturacion, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
tenant_idstring (UUID)❌Tenant a consultar. Solo Superadmin puede llamar el endpoint para cualquier tenant; el Administrador solo consulta el suyo.

Respuesta exitosa: 200 OK

Response:

{
"facturador_electronico": true,
"suscripcion": {
"documents_quota": 1000,
"documents_consumed": 25,
"documents_available": 975,
"days_until_expiration": 30,
"activated_at": "2026-10-01",
"expires_at": "2026-11-30",
"has_expired": false,
"is_active": true
}
}

Errores: 401 (sesión sin tenant), 403 (rol o permiso no autorizado).


GET /api/v1/facturacion/rangos [NUEVO]

Descripción: Lista los rangos de numeración del emisor consultando Factus (GET /v1/numbering-ranges). Sirve para elegir con qué rango se numeran las facturas cuando el tenant tiene más de uno activo: el ID elegido se guarda con PUT /api/v1/facturacion/config y a partir de ahí viaja siempre en el payload de emisión (numbering_range_id). Requiere Auth: Sí Permiso requerido: Superadmin (rol=3) y facturacion, leer

Parámetros query/path/body:

ParámetroTipoObligatorioDescripciónValores válidos / Ejemplo
tenant_idstring (UUID)❌Parámetro disponible para Superadmin. Los demás roles reciben 403."90af7e06-4211-4f99-82b7-adcf9b0a2382"

Comportamiento / Casos de uso:

  • El panel de administración llama este endpoint al editar un tenant facturador y muestra los rangos activos en un <select>.
  • Requiere credenciales Factus ya guardadas para ese tenant (400 si no existen).
  • is_active = 1 e is_expired = false son los rangos utilizables.

Respuesta exitosa: 200 OK

Response:

[
{
"id": 157,
"document": "Factura de Venta",
"prefix": "SETP",
"current": 990000038,
"is_active": 1,
"is_expired": false
},
{
"id": 158,
"document": "Nota Crédito",
"prefix": "NC",
"current": 11,
"is_active": 1,
"is_expired": false
}
]

Errores:

CódigoCausaSolución
400Tenant sin credenciales FactusConfigurar con PUT /api/v1/facturacion/config
401Token expirado o sesión sin tenantRe-login
403No es Superadmin o no tiene permiso facturacion, leerSolicitar acceso a Superadmin
502Factus caído o credenciales rechazadasVerificar credenciales y estado de Factus

cURL:

Terminal window
curl "http://localhost/api/v1/facturacion/rangos?tenant_id=90af7e06-4211-4f99-82b7-adcf9b0a2382" \
-H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/v1/facturacion/rangos",
params={"tenant_id": "90af7e06-4211-4f99-82b7-adcf9b0a2382"},
headers={"Authorization": f"Bearer {token}"})
rangos = resp.json()

JavaScript:

const resp = await fetch(
`http://localhost/api/v1/facturacion/rangos?tenant_id=${tenantId}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const rangos = await resp.json();

GET /api/v1/facturacion/ventas/{venta_id}/pdf [NUEVO]

Descripción: Descarga el PDF de la factura electrónica de una venta ya emitida. Requiere Auth: Sí Permiso requerido: facturacion, leer Respuesta exitosa: 200 — binario application/pdf. Errores: 404 (sin factura), 400 (sin bill_id), 502.

cURL:

Terminal window
curl "http://localhost/api/v1/facturacion/ventas/123/pdf" -H "Authorization: Bearer <token>" -o factura.pdf

GET /api/v1/facturacion/ventas/{venta_id}/xml [NUEVO]

Descripción: Descarga el XML fiscal de la factura electrónica. Requiere Auth: Sí Permiso requerido: facturacion, leer Respuesta exitosa: 200 — binario application/xml.

cURL:

Terminal window
curl "http://localhost/api/v1/facturacion/ventas/123/xml" -H "Authorization: Bearer <token>" -o factura.xml

POST /api/v1/facturacion/notas-credito [NUEVO]

Descripción: Emite una nota crédito contra la factura de una venta. Los ítems se generan automáticamente desde los detalles de la venta. Requiere Auth: Sí Permiso requerido: facturacion, crear

Body request:

{ "venta_id": 123, "correccion_concepto_code": "1", "observation": "Devolución" }

Respuesta exitosa: 201 Created

{ "id": 7, "venta_id": 123, "reference_code": "NC-123", "numero_nota": "NC-1045", "cufe": "...", "estado": "VALIDADA", "total": 10000.00 }

cURL:

Terminal window
curl -X POST "http://localhost/api/v1/facturacion/notas-credito" \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"venta_id":123,"correccion_concepto_code":"1","observation":"Devolución"}'