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 /configguardaclient_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)enfacturas_electronicas. - Resultado persistido: la emisión guarda
numero_factura,cufe,qr,estadoenfacturas_electronicasy actualizadetalle(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/facturacionimplementará 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ámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
venta_id | int | ✅ | 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 conPUT /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ódigo | Causa | Solución |
|---|---|---|
| 400 | Venta sin cliente/items, o tenant sin credenciales Factus | PUT /config o revisar la venta |
| 401 | Token inválido/expirado | Re-login |
| 403 | Sin permiso facturacion, crear | Solicitar permiso al admin |
| 404 | Venta no encontrada o no es del tenant | Verificar ID |
| 409 | Factura pendiente por enviar a la DIAN | Eliminar y re-emitir |
| 422 | Validación fallida en Factus | Revisar datos cliente/items |
| 502 | Factus caído o credenciales inválidas | Verificar api_url y credenciales |
cURL:
curl -X POST "http://localhost/api/v1/facturacion/ventas/123/emitir" \ -H "Authorization: Bearer <token>"Python:
import httpxresp = 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:
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_ides 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 responde422y hay que elegirlo conGET /api/v1/facturacion/rangos.
cURL:
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:
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ámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
tenant_id | string (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ámetro | Tipo | Obligatorio | Descripción | Valores válidos / Ejemplo |
|---|---|---|---|---|
tenant_id | string (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 (
400si no existen). is_active = 1eis_expired = falseson 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ódigo | Causa | Solución |
|---|---|---|
| 400 | Tenant sin credenciales Factus | Configurar con PUT /api/v1/facturacion/config |
| 401 | Token expirado o sesión sin tenant | Re-login |
| 403 | No es Superadmin o no tiene permiso facturacion, leer | Solicitar acceso a Superadmin |
| 502 | Factus caído o credenciales rechazadas | Verificar credenciales y estado de Factus |
cURL:
curl "http://localhost/api/v1/facturacion/rangos?tenant_id=90af7e06-4211-4f99-82b7-adcf9b0a2382" \ -H "Authorization: Bearer <token>"Python:
import httpxresp = 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:
curl "http://localhost/api/v1/facturacion/ventas/123/pdf" -H "Authorization: Bearer <token>" -o factura.pdfGET /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:
curl "http://localhost/api/v1/facturacion/ventas/123/xml" -H "Authorization: Bearer <token>" -o factura.xmlPOST /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:
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"}'