Skip to content

Ventas

Ventas

Prefijo: /api/ventas
Requiere Auth:

Módulo principal de ventas del POS. Incluye CRUD completo, agregaciones por fecha/medio de pago, gestión de detalles DIAN y pendientes de facturación electrónica.

Nota: Las ventas utilizan soft delete — al eliminar una venta se marca como eliminada sin borrarse físicamente.

Idempotencia y Prevención de Duplicados

NeoPOS implementa idempotencia robusta en la creación de ventas para prevenir duplicados causados por retries de red, doble clic, timeouts o reconexiones offline-first.

Cómo funciona (UUID por ciclo de vida del carrito)

  1. El cliente genera UNA key UUID al abrir la pantalla de ventas (initCartPersistence()).
  2. Reutiliza la misma key en cada clic de “Cobrar” y en cada retry de timeout.
  3. Al limpiar el carrito (venta exitosa o cancelación), genera una key nueva para la siguiente venta.
  4. El backend recibe la key y verifica si ya existe en BD.
  5. Si la key ya existe → retorna HTTP 409 con la venta existente (no duplica).
  6. Si el cliente no envía key → el backend genera un UUID4 como fallback (sin protección de duplicados).

Triple protección contra duplicados

CapaMecanismoCuándo actúa
UIFlag volatile isProcessingBloquea doble clic instantáneo
Redidempotency_key (UUID por carrito)Detecta retries con la misma key
BDUNIQUE (tenant_id, idempotency_key)Última línea de defensa contra race conditions

Header de trazabilidad

Todas las respuestas incluyen el header X-Request-ID (UUID). Útil para correlacionar logs del backend con peticiones del cliente.

Cuándo enviar la misma key vs. key nueva

EscenarioAcción
Primer clic en “Cobrar”Enviar la key del carrito
Timeout del servidor (500, 503)Reintentar con la misma key
Conexión cortada post-commitReintentar con la misma key → recibirá 409
Doble clic rápidoEl segundo clic es bloqueado por isProcessing
Venta exitosa → nueva ventaGenerar key nueva (nuevo carrito = nueva key)
Cancelar venta → nueva ventaGenerar key nueva

Paginación y Total Count

Todos los endpoints de listado y búsqueda que retornan arrays incluyen el header HTTP X-Total-Count con el número total de registros que cumplen los filtros sin paginación.

Cómo funciona

GET /api/ventas?skip=0&limit=20&fecha_desde=2026-01-01&fecha_hasta=2026-01-31
HTTP/1.1 200 OK
X-Total-Count: 342 ← Total de ventas en ese rango de fechas (sin paginar)
Content-Type: application/json
[{"id":1,"vendedor":"Juan",...}, ... 20 items ...]

El frontend calcula:

  • totalPages = Math.ceil(X-Total-Count / limit)
  • currentPage = (skip / limit) + 1
  • hasNext = currentPage < totalPages

Filtros que afectan X-Total-Count

EndpointQué cuenta X-Total-Count
GET /api/ventas?fecha_desde=...&fecha_hasta=...Ventas en ese rango de fechas
GET /api/ventas/search?q=juan&tipo=vendedorVentas donde vendedor/cliente matchea “juan”

CRUD Ventas

GET /api/ventas

Descripción: Lista todas las ventas con paginación y filtro por fechas.
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
skipintRegistros a saltar (default 0)
limitintMáximo de registros (default 100, máx 1000)
fecha_desdedateFiltro fecha inicial (YYYY-MM-DD)
fecha_hastadateFiltro fecha final (YYYY-MM-DD)
fechadateFecha exacta (alias para fecha_desde y fecha_hasta)
mesintMes (1-12)
aniointAño

Respuesta exitosa: 200 OK

Headers de respuesta:

HeaderTipoDescripción
X-Total-CountintegerTotal de ventas que cumplen los filtros (fecha_desde, fecha_hasta, mes, anio). No considera skip/limit.

Response:

[
{
"id": 1,
"tenant_id": "uuid-tenant",
"id_cliente": 1,
"vendedor": "Juan Pérez",
...
}
]

cURL:

Terminal window
curl "http://localhost/api/ventas?skip=0&limit=10&fecha_desde=2025-01-01" -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/ventas",
params={"skip": 0, "limit": 10, "fecha_desde": "2025-01-01"},
headers={"Authorization": f"Bearer :token"})
ventas = resp.json()
total = int(resp.headers.get("X-Total-Count", "0"))
total_pages = (total + 10 - 1) // 10 # ceil division

JavaScript:

const resp = await fetch('http://localhost/api/ventas?skip=0&limit=10&fecha_desde=2025-01-01', {
headers: {'Authorization': `Bearer $:token`}
});
const ventas = await resp.json();
const totalCount = parseInt(resp.headers.get('X-Total-Count') || '0', 10);
const totalPages = Math.ceil(totalCount / 10);

GET /api/ventas/:venta_id

Descripción: Obtiene una venta por ID
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK
Errores: 404 (no encontrada)

cURL:

Terminal window
curl http://localhost/api/ventas/1 -H "Authorization: Bearer <token>"

POST /api/ventas

Descripción: Crea una nueva venta con detalles y pagos. Soporta idempotencia para prevenir duplicados en retries.
Requiere Auth:
Permiso requerido: ventas, crear

Parámetros body:

CampoTipoObligatorioDescripción
id_clienteintID del cliente
vendedorstringNombre del vendedor
montoRecibidofloatMonto recibido del cliente
totalfloatTotal de la venta
descuentofloatDescuento aplicado
medioPagostringEfectivo, Transferencia, Credito
tipoPagostringCONTADO o CREDITO
detallesarrayLíneas de la venta
pagosarrayPagos asociados
idempotency_keystringUUID único por ciclo de vida del carrito. Se genera UNA vez al abrir la vista de ventas y se reutiliza en cada clic/retry. Si se omite, el backend genera un UUID4 (sin protección de duplicados).
terminal_idstringID del terminal POS que originó la venta. Se obtiene del JWT automáticamente si no se envía.

Comportamiento de idempotencia:

  • Si idempotency_key se envía y ya existe → retorna HTTP 409 con {"mensaje": "Venta ya procesada", "venta_id": N} (no duplica).
  • Si idempotency_key se omite → el backend genera un UUID4 como fallback. Sin protección de duplicados.
  • Si idempotency_key es nueva → crea la venta normalmente.
  • Doble clic → bloqueado por flag isProcessing en el cliente (nunca llega al backend).

Respuesta exitosa: 201 Created

Respuesta duplicado: 409 Conflict — la venta con esa key ya existe

Request:

{
"id_cliente": 1,
"vendedor": "Juan Pérez",
"montoRecibido": 12000,
"total": 10000,
"descuento": 0,
"vueltos": 2000,
"totalf": "10000",
"medioPago": "Efectivo",
"tipoPago": "CONTADO",
"fecha": "2025-01-15",
"hora": "14:30:00",
"tipo_pago": "CONTADO",
"detalles": [
{
"cod_pro": "PROD001",
"nombre": "Café Latte",
"cantidad": 2,
"cliente_id": "1",
"precio": 3500,
"medioPago": "Efectivo",
"id_producto": 1,
"tipo_impuesto": "IVA",
"valor_base": 7000,
"valor_impuesto": 1330,
"codigo_impuesto": "01"
}
],
"pagos": [
{
"id_venta": 0,
"medio_pago": "Efectivo",
"monto": 12000
}
],
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}

Response 201 (exitoso):

{
"id": 1,
"tenant_id": "uuid-tenant",
"vendedor": "Juan Pérez",
"total": 10000,
"medioPago": "Efectivo",
"tipoPago": "CONTADO",
"fecha": "2025-01-15",
"hora": "14:30:00",
"fecha_hora": "2025-01-15T14:30:00",
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}

Response 409 (duplicado):

{
"detail": {
"mensaje": "Venta ya procesada",
"venta_id": 366
}
}

Errores: 400 (datos inválidos), 409 (venta duplicada por idempotency_key)

cURL:

Terminal window
curl -X POST http://localhost/api/ventas \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"id_cliente":1,"vendedor":"Juan Pérez","montoRecibido":12000,"total":10000,"medioPago":"Efectivo","tipoPago":"CONTADO","detalles":[{"cod_pro":"PROD001","nombre":"Café Latte","cantidad":2,"precio":3500,"id_producto":1,"tipo_impuesto":"IVA","valor_base":7000,"valor_impuesto":1330,"codigo_impuesto":"01"}],"pagos":[{"medio_pago":"Efectivo","monto":12000}]}'

Python:

import httpx
resp = httpx.post("http://localhost/api/ventas",
json={"id_cliente": 1, "vendedor": "Juan Pérez", "montoRecibido": 12000, "total": 10000, "medioPago": "Efectivo", "tipoPago": "CONTADO", "detalles": [{"cod_pro": "PROD001", "nombre": "Café Latte", "cantidad": 2, "precio": 3500, "id_producto": 1, "tipo_impuesto": "IVA", "valor_base": 7000, "valor_impuesto": 1330, "codigo_impuesto": "01"}], "pagos": [{"medio_pago": "Efectivo", "monto": 12000}]},
headers={"Authorization": f"Bearer :token"})
venta = resp.json()

JavaScript:

const resp = await fetch('http://localhost/api/ventas', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Authorization': `Bearer $:token`},
body: JSON.stringify({id_cliente: 1, vendedor: 'Juan Pérez', montoRecibido: 12000, total: 10000, medioPago: 'Efectivo', tipoPago: 'CONTADO', detalles: [{cod_pro: 'PROD001', nombre: 'Café Latte', cantidad: 2, precio: 3500, id_producto: 1, tipo_impuesto: 'IVA', valor_base: 7000, valor_impuesto: 1330, codigo_impuesto: '01'}], pagos: [{medio_pago: 'Efectivo', monto: 12000}]})
});
const venta = await resp.json();

PUT /api/ventas/:venta_id

Descripción: Actualiza una venta existente
Requiere Auth:
Permiso requerido: ventas, actualizar

Respuesta exitosa: 200 OK
Errores: 404 (no encontrada)

cURL:

Terminal window
curl -X PUT http://localhost/api/ventas/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"total":11000,"montoRecibido":15000}'

DELETE /api/ventas/:venta_id

Descripción: Elimina (soft delete) una venta
Requiere Auth:
Permiso requerido: ventas, eliminar

Respuesta exitosa: 204 No Content
Errores: 404 (no encontrada)

cURL:

Terminal window
curl -X DELETE http://localhost/api/ventas/1 -H "Authorization: Bearer <token>"

PUT /api/ventas/:venta_id/abono

Descripción: Alias de compatibilidad — acepta {"abono": N} y lo mapea internamente a montoRecibido. Existe para clientes legacy (Java) que envían campo abono. Funcionalmente idéntico a PATCH /abono.
Requiere Auth:
Permiso requerido: ventas, actualizar

Request:

{
"abono": 5000
}

Campo:

CampoTipoObligatorioDescripción
abonointMonto a sumar a montoRecibido (se mapea a montoRecibido)

Comportamiento / Casos de uso:

  • Cliente Java legacy que usa campo abono en lugar de montoRecibido
  • Suma el valor al montoRecibido actual de la venta
  • No resta — solo incrementa lo ya recibido

Respuesta exitosa: 200 OK

Response:

{
"ok": true
}

Errores:

CódigoCausaSolución
404Venta no encontradaVerificar venta_id
400abono ≤ 0Enviar valor positivo
403Sin permiso ventas, actualizarSolicitar a admin

cURL:

Terminal window
curl -X PUT http://localhost/api/ventas/1/abono \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"abono":5000}'

Python:

import httpx
resp = httpx.put("http://localhost/api/ventas/1/abono",
json={"abono": 5000},
headers={"Authorization": f"Bearer :token"})
assert resp.json()["ok"] == True

JavaScript:

const resp = await fetch('http://localhost/api/ventas/1/abono', {
method: 'PUT',
headers: {'Content-Type': 'application/json', 'Authorization': `Bearer $:token`},
body: JSON.stringify({abono: 5000})
});
const result = await resp.json();

GET /api/ventas/cliente/:cliente_id

Descripción: Obtiene todas las ventas de un cliente
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK

cURL:

Terminal window
curl http://localhost/api/ventas/cliente/1 -H "Authorization: Bearer <token>"

GET /api/ventas/tipo-pago/:tipo_pago

Descripción: Obtiene ventas por tipo de pago (CONTADO, CREDITO)
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK

cURL:

Terminal window
curl http://localhost/api/ventas/tipo-pago/CONTADO -H "Authorization: Bearer <token>"

Agregaciones

GET /api/ventas/total-por-medio-pago

Descripción: Obtiene el total de ventas agrupado por medio de pago en una fecha
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
medio_pagostringEfectivo, Transferencia, Credito, VARIOS
fechadateFecha (default: hoy)

Respuesta exitosa: 200 OK

Response:

{
"total": 500000
}

cURL:

Terminal window
curl "http://localhost/api/ventas/total-por-medio-pago?medio_pago=Efectivo&fecha=2025-01-15" -H "Authorization: Bearer <token>"

GET /api/ventas/total-vueltos-dia

Descripción: Obtiene el total de vueltos del día
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
fechadateFecha (default: hoy)

Respuesta exitosa: 200 OK

Response:

{
"total": 15000
}

cURL:

Terminal window
curl "http://localhost/api/ventas/total-vueltos-dia?fecha=2025-01-15" -H "Authorization: Bearer <token>"

GET /api/ventas/total-transacciones-dia

Descripción: Obtiene el número total de transacciones del día
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
fechadateFecha (default: hoy)

Respuesta exitosa: 200 OK

Response:

{
"total": 45
}

cURL:

Terminal window
curl "http://localhost/api/ventas/total-transacciones-dia?fecha=2025-01-15" -H "Authorization: Bearer <token>"

GET /api/ventas/diario-por-usuario [NUEVO]

Descripción: Resumen diario de ventas discriminadas por vendedor para una fecha específica. Retorna una lista donde cada elemento representa un vendedor con su desglose por canal de venta (Local/Offline y Electrónico) y por medio de pago (Efectivo, Transferencia). Diseñado para el dashboard del admin que necesita ver qué vendedores están operando y cuánto factura cada uno.
Caso de uso: el admin abre el dashboard y ve la tabla “Ventas por vendedor hoy” con columnas: vendedor, nº ventas, total, local, electrónico, efectivo, transferencia. También útil para reportes diarios por cajero.
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
fechadateFecha del reporte (por defecto hoy, formato YYYY-MM-DD)

Campos de la respuesta:

CampoTipoDescripción
vendedorstringNombre del vendedor/cajero
fechadate | nullFecha del reporte
n_ventasintNº de ventas no eliminadas del vendedor en la fecha
totalintSuma de Venta.total de todas las ventas del vendedor
localintSuma de ventas con tipoPago = “Local” o “Offline/Pendiente” (canal local)
electronicointSuma de ventas con tipoPago = “Electrónico” (facturación DIAN)
efectivointSuma de ventas con medioPago = “Efectivo”
transferenciaintSuma de ventas con medioPago = “Transferencia”
otrosintResiduo del total menos local, electrónico, efectivo y transferencia

Nota: los totales local + electronico + otros = total por vendedor. Los campos efectivo y transferencia son un corte diferente (por medio de pago, no por canal).

Respuesta exitosa: 200 OK

Response:

[
{
"vendedor": "ana",
"fecha": "2026-08-25",
"n_ventas": 18,
"total": 450000,
"local": 380000,
"electronico": 70000,
"efectivo": 300000,
"transferencia": 100000,
"otros": 0
},
{
"vendedor": "carlos",
"fecha": "2026-08-25",
"n_ventas": 12,
"total": 280000,
"local": 280000,
"electronico": 0,
"efectivo": 200000,
"transferencia": 80000,
"otros": 0
}
]

cURL:

Terminal window
# Ventas de hoy por vendedor
curl "http://localhost/api/ventas/diario-por-usuario" -H "Authorization: Bearer <token>"
# Ventas de una fecha específica
curl "http://localhost/api/ventas/diario-por-usuario?fecha=2026-08-25" -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/ventas/diario-por-usuario",
params={"fecha": "2026-08-25"},
headers={"Authorization": f"Bearer :token"})
ventas_por_vendedor = resp.json()

JavaScript:

const resp = await fetch('http://localhost/api/ventas/diario-por-usuario?fecha=2026-08-25', {
headers: {'Authorization': `Bearer $:token`}
});
const ventasPorVendedor = await resp.json();

GET /api/ventas/total-rango

Descripción: Obtiene el total de ventas en un rango de fechas
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
fecha_iniciodateFecha inicial
fecha_findateFecha final

Respuesta exitosa: 200 OK

Response:

{
"total": 2500000.0
}

cURL:

Terminal window
curl "http://localhost/api/ventas/total-rango?fecha_inicio=2025-01-01&fecha_fin=2025-01-31" -H "Authorization: Bearer <token>"

Búsqueda y Conteo

GET /api/ventas/search

Descripción: Busca ventas por texto (cliente, vendedor, factura) y tipo con paginación.
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
qstringTexto a buscar (cliente, vendedor, factura)
tipostringTipo de pago (CONTADO, CREDITO)
skipintRegistros a saltar (default 0)
limitintMáximo de registros (default 50, máx 1000)

Respuesta exitosa: 200 OK

Headers de respuesta:

HeaderTipoDescripción
X-Total-CountintegerTotal de ventas que matchean la búsqueda (texto q y/o tipo). No considera skip/limit.

cURL:

Terminal window
curl "http://localhost/api/ventas/search?q=Cliente&tipo=CONTADO&skip=0&limit=20" -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/ventas/search",
params={"q": "Cliente", "tipo": "CONTADO", "skip": 0, "limit": 20},
headers={"Authorization": f"Bearer :token"})
ventas = resp.json()
total = int(resp.headers.get("X-Total-Count", "0"))

GET /api/ventas/search/count

Descripción: Cuenta las ventas que coinciden con la búsqueda
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
qstringTexto a buscar
tipostringTipo de pago

Respuesta exitosa: 200 OK

Response:

{
"count": 12
}

cURL:

Terminal window
curl "http://localhost/api/ventas/search/count?q=Cliente" -H "Authorization: Bearer <token>"

GET /api/ventas/count

Descripción: Cuenta ventas con filtros de fecha, mes, texto o tipo
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros query:

ParámetroTipoObligatorioDescripción
fechadateFecha exacta
mesintMes (1-12)
aniointAño
textostringTexto a buscar
tipostringTipo de pago

Respuesta exitosa: 200 OK

Response:

{
"count": 45
}

cURL:

Terminal window
curl "http://localhost/api/ventas/count?fecha=2025-01-15" -H "Authorization: Bearer <token>"

GET /api/ventas/next-id

Descripción: Obtiene el siguiente ID de venta disponible (útil para pre-numeración de facturas)
Requiere Auth:
Permiso requerido: ventas, crear

Respuesta exitosa: 200 OK

Response:

{
"next_id": 102
}

cURL:

Terminal window
curl http://localhost/api/ventas/next-id -H "Authorization: Bearer <token>"

Integración Cliente Java

POST /api/ventas/:venta_id/revert-stock

Descripción: Revierte el stock descontado por una venta (devolución)
Requiere Auth:
Permiso requerido: ventas, actualizar

Respuesta exitosa: 200 OK

Response:

{
"mensaje": "Stock revertido exitosamente"
}

Errores: 404 (venta no encontrada)

cURL:

Terminal window
curl -X POST http://localhost/api/ventas/1/revert-stock -H "Authorization: Bearer <token>"

GET /api/ventas/:venta_id/numero-factura

Descripción: Obtiene el número de factura asociado a una venta
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK

Response:

{
"numero_factura": "FAC-001"
}

Errores: 404 (venta o detalle no encontrado)

cURL:

Terminal window
curl http://localhost/api/ventas/1/numero-factura -H "Authorization: Bearer <token>"

PUT /api/ventas/:venta_id/post-emision

Descripción: Registra los datos de emisión DIAN después de facturar electrónicamente (número de factura, CUFE, fecha de validación, resolución)
Requiere Auth:
Permiso requerido: ventas, actualizar

Request:

{
"numero_factura": "FAC-001",
"cufe": "a1b2c3d4e5f6...",
"fecha_validacion": "2025-01-15T14:35:00",
"resolucion": 123456789
}

Respuesta exitosa: 200 OK

Response:

{
"mensaje": "Emisión DIAN registrada exitosamente"
}

Errores: 404 (venta no encontrada)

cURL:

Terminal window
curl -X PUT http://localhost/api/ventas/1/post-emision \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"numero_factura":"FAC-001","cufe":"a1b2c3d4e5f6...","fecha_validacion":"2025-01-15T14:35:00","resolucion":123456789}'

GET /api/ventas/:venta_id/tipo-pago

Descripción: Obtiene el tipo de pago de una venta (CONTADO, CREDITO)
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK

Response:

{
"tipo_pago": "CONTADO"
}

Errores: 404 (venta no encontrada)

cURL:

Terminal window
curl http://localhost/api/ventas/1/tipo-pago -H "Authorization: Bearer <token>"

GET /api/ventas/:venta_id/medio-pago

Descripción: Obtiene el medio de pago de una venta (Efectivo, Transferencia, Credito)
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK

Response:

{
"medio_pago": "Efectivo"
}

Errores: 404 (venta no encontrada)

cURL:

Terminal window
curl http://localhost/api/ventas/1/medio-pago -H "Authorization: Bearer <token>"

Detalles y Pagos

GET /api/ventas/:venta_id/detalles

Descripción: Obtiene los detalles (líneas) de una venta
Requiere Auth:
Permiso requerido: ventas, leer

Respuesta exitosa: 200 OK

cURL:

Terminal window
curl http://localhost/api/ventas/1/detalles -H "Authorization: Bearer <token>"

PATCH /api/ventas/:venta_id/detalles

Descripción: Actualiza campos DIAN de los detalles (líneas) de una venta tras facturación electrónica exitosa. Recibe array de objetos con id del detalle + campos a actualizar. Útil cuando la facturación es asíncrona y el CUFE/NumeroFact llegan después.
Requiere Auth:
Permiso requerido: ventas, actualizar

Request (array de actualizaciones):

[
{
"id": 1,
"cufe": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"NumeroFact": "SETP990000123",
"estadoFact": 1,
"fechaCreacion": "2025-01-15T14:30:00",
"fechaValidacion": "2025-01-15T14:35:00"
}
]

Campos actualizables por detalle:

CampoTipoDescripciónValores
idintObligatorio - ID del detalleDebe existir en la venta
cufestringCUFE devuelto por DIAN96 chars hex
NumeroFactstringNúmero de factura autorizadoFormato: SETP + 9 dígitos
estadoFactintEstado facturación0=Pendiente, 1=Facturado, 2=Anulado
fechaCreaciondatetimeTimestamp creación facturaISO 8601
fechaValidaciondatetimeTimestamp validación DIANISO 8601

Comportamiento / Casos de uso:

  • Facturación offline: venta se registra, luego sincroniza CUFE cuando hay internet
  • Facturación por lote: enviar múltiples detalles en una request
  • Corrección: re-enviar CUFE si DIAN rechazó inicial

Respuesta exitosa: 200 OK

Response:

{
"ok": true
}

Errores:

CódigoCausaSolución
400Detalle ID no pertenece a la ventaVerificar IDs
404Venta no encontradaVerificar venta_id
403Sin permiso ventas, actualizarSolicitar a admin

cURL:

Terminal window
curl -X PATCH http://localhost/api/ventas/1/detalles \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '[{"id":1,"cufe":"a1b2c3d4e5f6...","NumeroFact":"FAC-001","estadoFact":1}]'

Python:

import httpx
resp = httpx.patch("http://localhost/api/ventas/1/detalles",
json=[{"id": 1, "cufe": "a1b2c3d4e5f6...", "NumeroFact": "FAC-001", "estadoFact": 1}],
headers={"Authorization": f"Bearer :token"})
assert resp.json()["ok"] == True

JavaScript:

const resp = await fetch('http://localhost/api/ventas/1/detalles', {
method: 'PATCH',
headers: {'Content-Type': 'application/json', 'Authorization': `Bearer $:token`},
body: JSON.stringify([{id: 1, cufe: 'a1b2c3d4e5f6...', NumeroFact: 'FAC-001', estadoFact: 1}])
});
const result = await resp.json();

GET /api/ventas/:venta_id/pagos

Descripción: Obtiene todos los pagos (abonos, medios de pago) asociados a una venta. Una venta puede tener múltiples pagos (ej: parcial en efectivo + parcial en transferencia). Para conciliación de caja y verificación de abonos en ventas a crédito.
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros path:

ParámetroTipoObligatorioDescripción
venta_idintID de la venta

Parámetros query:

ParámetroTipoObligatorioDescripción
skipintPaginación (default 0)
limitintPaginación (default 500)

Comportamiento / Casos de uso:

  • Verificar que suma de pagos = total de venta (cuadre)
  • Ventas a crédito: ver historial de abonos
  • Conciliación: cruzar con extracto bancario (transferencias)

Respuesta exitosa: 200 OK

Response:

[
{
"id": 1,
"id_venta": 1,
"medio_pago": "Efectivo",
"monto": 12000,
"fecha": "2025-01-15"
},
{
"id": 2,
"id_venta": 1,
"medio_pago": "Transferencia",
"monto": 50000,
"fecha": "2025-01-15"
}
]

Errores:

CódigoCausaSolución
404Venta no encontradaVerificar venta_id
403Sin permiso ventas, leerSolicitar a admin

cURL:

Terminal window
curl "http://localhost/api/ventas/1/pagos?skip=0&limit=50" -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/ventas/1/pagos",
params={"skip": 0, "limit": 50},
headers={"Authorization": f"Bearer :token"})
pagos = resp.json()

JavaScript:

const resp = await fetch('http://localhost/api/ventas/1/pagos?skip=0&limit=50', {
headers: {'Authorization': `Bearer $:token`}
});
const pagos = await resp.json();

GET /api/ventas/:venta_id/pendiente

Descripción: Verifica si una venta electrónica (tipoPago = “Electronico”) quedó en estado pendiente por fallo de red o error en API DIAN. Significado: la venta se ejecutó localmente pero NO se pudo enviar a DIAN para obtener CUFE/NumeroFact. El POS debe reintentar automáticamente cuando recupere conexión.
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros path:

ParámetroTipoObligatorioDescripción
venta_idintID de la venta

Comportamiento / Casos de uso:

  • POS offline: venta electrónica se guarda local, pendiente: true
  • POS online: al detectar pendiente: true, reintenta facturación automática
  • Admin: consultar ventas atascadas para forzar reintento manual
  • Solo aplica a tipoPago = "Electronico" — ventas CONTADO/CREDITO siempre false

Respuesta exitosa: 200 OK

Response:

{
"pendiente": true
}

Valores:

ValorQué significa
trueVenta electrónica sin CUFE/NumeroFact (falló envío a DIAN)
falseVenta facturada OK, o no es electrónica (CONTADO/CREDITO)

Errores:

CódigoCausaSolución
404Venta no encontradaVerificar venta_id
403Sin permiso ventas, leerSolicitar a admin

cURL:

Terminal window
curl http://localhost/api/ventas/1/pendiente -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/ventas/1/pendiente", headers={"Authorization": f"Bearer :token"})
if resp.json()["pendiente"]:
print("Reintentar facturación DIAN")

JavaScript:

const resp = await fetch('http://localhost/api/ventas/1/pendiente', {
headers: {'Authorization': `Bearer $:token`}
});
const :pendiente = await resp.json();
if (pendiente) alert('Venta pendiente DIAN - reintentar');

PATCH /api/ventas/:venta_id/abono

Descripción: Endpoint principal para actualizar montoRecibido (abono) de una venta. Acepta directamente {"montoRecibido": N} que suma al valor actual. Diferencia con PUT /abono: este usa campo nativo montoRecibido; PUT usa alias abono para compatibilidad Java.
Requiere Auth:
Permiso requerido: ventas, actualizar

Request:

{
"montoRecibido": 5000
}

Campo:

CampoTipoObligatorioDescripción
montoRecibidointMonto a sumar al montoRecibido actual de la venta

Comportamiento / Casos de uso:

  • Cliente paga cuota de crédito: sumar abono a montoRecibido
  • Corrección: ajustar monto recibido si se digitó mal
  • Incremental: siempre suma al valor existente, no reemplaza

Respuesta exitosa: 200 OK

Response:

{
"ok": true
}

Errores:

CódigoCausaSolución
404Venta no encontradaVerificar venta_id
400montoRecibido ≤ 0Enviar valor positivo
403Sin permiso ventas, actualizarSolicitar a admin

Diferencia PUT vs PATCH /abono:

EndpointCampo entradaUso recomendado
PUT /abono{"abono": N}Clientes legacy (Java)
PATCH /abono{"montoRecibido": N}Nuevos desarrollos (nativo)

cURL:

Terminal window
curl -X PATCH http://localhost/api/ventas/1/abono \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"montoRecibido":5000}'

Python:

import httpx
resp = httpx.patch("http://localhost/api/ventas/1/abono",
json={"montoRecibido": 5000},
headers={"Authorization": f"Bearer :token"})
assert resp.json()["ok"] == True

JavaScript:

const resp = await fetch('http://localhost/api/ventas/1/abono', {
method: 'PATCH',
headers: {'Content-Type': 'application/json', 'Authorization': `Bearer $:token`},
body: JSON.stringify({montoRecibido: 5000})
});
const result = await resp.json();

Pendientes DIAN

Flujo completo: Cuando una venta electrónica falla al facturar (sin internet, error DIAN), se crea un registro en ventas_pendientes_dian. El POS consulta /pendientes-dian/, reintenta facturación, y al éxito actualiza con CUFE/NumeroFact via PUT. Finalmente elimina el pendiente.

GET /api/ventas/pendientes-dian/

Descripción: Lista todas las ventas pendientes de facturación electrónica DIAN. Incluye venta_id, CUFE parcial, NumeroFact si ya se generó, y fecha. Para que el POS/admon sepa qué facturas reintentar.
Requiere Auth:
Permiso requerido: ventas, leer

Comportamiento / Casos de uso:

  • POS inicia: consulta pendientes → reintenta facturación automática
  • Admin: revisa cola de facturas atascadas
  • Filtro implícito: solo ventas con tipoPago = "Electronico" y sin CUFE válido

Respuesta exitosa: 200 OK

Response:

[
{
"id": 1,
"id_venta": 15,
"cufe": "",
"NumeroFact": "",
"fecha": "2025-01-15T14:30:00"
},
{
"id": 2,
"id_venta": 16,
"cufe": "a1b2c3d4...",
"NumeroFact": "SETP990000124",
"fecha": "2025-01-15T15:00:00"
}
]

cURL:

Terminal window
curl http://localhost/api/ventas/pendientes-dian/ -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.get("http://localhost/api/ventas/pendientes-dian/", headers={"Authorization": f"Bearer :token"})
pendientes = resp.json()
for p in pendientes:
if not p["cufe"]:
print(f"Reintentar venta {p['id_venta']}")

GET /api/ventas/pendientes-dian/:pendiente_id

Descripción: Obtiene el detalle de un pendiente DIAN específico por ID. Para ver qué venta está atascada y su estado actual.
Requiere Auth:
Permiso requerido: ventas, leer

Parámetros path:

ParámetroTipoObligatorioDescripción
pendiente_idintID del pendiente DIAN

Respuesta exitosa: 200 OK

Response:

{
"id": 1,
"id_venta": 15,
"cufe": "",
"NumeroFact": "",
"fecha": "2025-01-15T14:30:00"
}

Errores: 404 (no encontrado), 403 (sin permiso)

cURL:

Terminal window
curl http://localhost/api/ventas/pendientes-dian/1 -H "Authorization: Bearer <token>"

POST /api/ventas/pendientes-dian/

Descripción: Crea un registro de venta pendiente de facturación DIAN. Se usa cuando la venta electrónica se ejecuta pero falla el envío a DIAN (offline, timeout, error API). El POS crea el pendiente para reintentar luego.
Requiere Auth:
Permiso requerido: ventas, crear

Request:

{
"id_venta": 15,
"cufe": "",
"NumeroFact": ""
}

Campos:

CampoTipoObligatorioDescripción
id_ventaintID de la venta electrónica
cufestringCUFE si ya se generó (raro en creación)
NumeroFactstringNúmero factura si ya se asignó

Comportamiento / Casos de uso:

  • Venta electrónica offline: POS crea venta + pendiente DIAN
  • Al reconectar: POS consulta pendientes, factura, actualiza con CUFE

Respuesta exitosa: 201 Created

Response:

{
"id": 3,
"id_venta": 15,
"cufe": "",
"NumeroFact": "",
"fecha": "2025-08-28T10:00:00"
}

Errores: 404 (venta no existe), 409 (ya existe pendiente para esa venta), 403 (sin permiso)

cURL:

Terminal window
curl -X POST http://localhost/api/ventas/pendientes-dian/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"id_venta":15,"cufe":"","NumeroFact":""}'

PUT /api/ventas/pendientes-dian/:pendiente_id

Descripción: Actualiza un pendiente DIAN con el CUFE y NumeroFact devueltos por DIAN tras facturación exitosa. Marca la venta como facturada.
Requiere Auth:
Permiso requerido: ventas, actualizar

Parámetros path:

ParámetroTipoObligatorioDescripción
pendiente_idintID del pendiente DIAN

Request:

{
"cufe": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"NumeroFact": "SETP990000125"
}

Campos:

CampoTipoObligatorioDescripción
cufestringCUFE de 96 chars hex devuelto por DIAN
NumeroFactstringNúmero de factura autorizado (formato SETP + 9 dígitos)

Comportamiento / Casos de uso:

  • Facturación exitosa: POS recibe CUFE/NumeroFact → PUT al pendiente
  • También actualiza detalle.cufe, detalle.NumeroFact, detalle.estadoFact = 1
  • El pendiente puede eliminarse después (opcional, para limpieza)

Respuesta exitosa: 200 OK

Response:

{
"id": 1,
"id_venta": 15,
"cufe": "a1b2c3d4e5f6...",
"NumeroFact": "SETP990000125",
"fecha": "2025-01-15T14:30:00"
}

Errores: 404 (pendiente no encontrado), 400 (CUFE/NumeroFact inválidos), 403 (sin permiso)

cURL:

Terminal window
curl -X PUT http://localhost/api/ventas/pendientes-dian/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"cufe":"a1b2c3d4e5f6...","NumeroFact":"SETP990000125"}'

DELETE /api/ventas/pendientes-dian/:pendiente_id

Descripción: Elimina un pendiente DIAN tras facturación exitosa y actualización de la venta. Limpieza opcional — no obligatoria, pero recomendada para no acumular basura.
Requiere Auth:
Permiso requerido: ventas, eliminar

Parámetros path:

ParámetroTipoObligatorioDescripción
pendiente_idintID del pendiente DIAN

Comportamiento / Casos de uso:

  • Después de PUT con CUFE/NumeroFact exitoso, limpiar cola
  • Admin: borrar pendientes huérfanos (venta anulada)
  • No elimina la venta — solo el registro de cola DIAN

Respuesta exitosa: 204 No Content

Errores: 404 (no encontrado), 403 (sin permiso)

cURL:

Terminal window
curl -X DELETE http://localhost/api/ventas/pendientes-dian/1 -H "Authorization: Bearer <token>"

Python:

import httpx
resp = httpx.delete("http://localhost/api/ventas/pendientes-dian/1", headers={"Authorization": f"Bearer :token"})
assert resp.status_code == 204