Plataforma técnica · Orchestrator
Compras Inventario Service
Orchestrator Inventarios Compras
ComprasInventarioService es el puente entre operaciones SII y el inventario: toma compras ya sincronizadas (vía OperacionesService) y las convierte en movimientos de inventario. Implementa el flujo operacional que el contador usa desde la “bandeja de compras pendientes” en Sevastopol.
Tiene dos modos de procesamiento (procesarCompraExistencia por producto específico vs procesarCompraIngresoCategoria agregado), distingue facturas de notas de crédito por el signo del neto, y emite compra_inv:contabilizada por cada movimiento generado.
Operaciones
Section titled “Operaciones”| Método | Para |
|---|---|
getComprasExistencias(ctx, filtros?) | Bandeja: compras candidatas + resumen período + resumen mensual. |
procesarCompraExistencia(ctx, compraId, data) | Procesar UNA compra como ingreso de UN producto. Emite hook. |
procesarCompraIngresoCategoria(ctx, compraId, data) | Procesar UNA compra como ingreso agregado a una categoría. Emite hook. |
reversarCompraIngreso(ctx, compraId) | Revertir un ingreso previamente procesado. |
getComprasDetallePendientes(ctx, filtros?) | Lista de líneas de detalle pendientes (multi-existencia). |
eliminarCompraDetalle(ctx, id) | Borra una línea de detalle pendiente. |
eliminarComprasDetalleByProveedor(ctx, rut, año, mes?) | Bulk: borra todas las pendientes de un proveedor. |
generarExistenciasPorProveedor(ctx, rut, año, mes?) | Bulk: contabiliza todas las pendientes de un proveedor. |
getComprasContabilizadas(ctx, filtros?) | Bandeja de las ya procesadas (vista agregada por proveedor + categoría). |
getComprasContabilizadasDetalle(ctx, rut, catId, filtros?) | Drill-down a las líneas individuales. |
Tres validaciones siempre presentes
Section titled “Tres validaciones siempre presentes”Los métodos procesarCompraExistencia y procesarCompraIngresoCategoria validan siempre lo mismo antes de procesar:
| Check | Si falla |
|---|---|
findCompraExistenciaProcessInfo(compraId) retorna algo | ValidationError("Compra no encontrada") |
proveedor_activo && proveedor_genera_existencias | ValidationError("La compra no pertenece a un proveedor configurado para existencias") |
procesado_inventario === false | ValidationError("La compra ya fue procesada en inventario") |
Number(monto_neto_signed) !== 0 | ValidationError("El monto neto + otros impuestos del documento es cero y no puede procesarse en inventario") |
El proveedor se “configura para existencias” en ProveedoresClasificacionService (sección menores en index).
Modo 1: procesarCompraExistencia — por producto
Section titled “Modo 1: procesarCompraExistencia — por producto”Cuando el operador conoce exactamente qué producto entra al inventario por esta compra. Valida adicional:
data.existencia_idrequerido + existencia debe existir (lookup en TX).data.cantidad > 0estricto.
Detección automática: factura vs nota de crédito
Section titled “Detección automática: factura vs nota de crédito”flowchart TB
IN["procesarCompraExistencia(compraId, data)"]
V["validaciones (4 base + 2 producto)"]
TX["withTransaction"]
INFO["findCompraExistenciaProcessInfo"]
EXIS["ExistenciasRepository.findExistenciaById"]
META["build fechaDocumento + numeroDocumento + safeUserId"]
CHK{"monto_neto_signed < 0?"}
NC["procesarDevolucionCompraExistencia<br/>(monto positivo, glosa 'NC SII ... devolución')"]
FA["procesarCompraExistencia<br/>(monto tal cual, glosa 'Compra SII ... procesada')"]
PAY["findCompraInvContabilizadaEventPayload(movimiento_id)"]
EMIT["outbox.queue(compra_inv:contabilizada, payload)"]
RET["return result"]
IN --> V --> TX --> INFO --> EXIS --> META --> CHK
CHK -- sí --> NC --> PAY
CHK -- no --> FA --> PAY
PAY --> EMIT --> RET Por qué la rama NC: una compra con monto_neto_signed < 0 es una nota de crédito (devolución). El service la procesa como devolución — usa el método de repository dedicado y reescribe la glosa para reflejar el flujo inverso. El monto se pasa positivo (Math.abs) al repository porque el repository encapsula el signo del movimiento.
Construcción del numero_documento
Section titled “Construcción del numero_documento”Formato fijo: ${año}-${mes_zero_padded}-${nro}. Ejemplos:
- Compra de mayo 2026 número 42 →
"2026-05-42". - Compra de enero 2026 número 7 →
"2026-01-7".
Este número se usa en la glosa del movimiento y como referencia humanamente legible.
Modo 2: procesarCompraIngresoCategoria — por categoría agregada
Section titled “Modo 2: procesarCompraIngresoCategoria — por categoría agregada”Cuando el operador no quiere desglose por producto y solo necesita afectar una categoría contable. Útil para compras genéricas (materiales de oficina, insumos no inventariados a nivel de SKU).
| DTO field | Validación |
|---|---|
categoria_id | Opcional; si se pasa: entero positivo. |
Mismo flujo que el modo 1 pero sin lookup de existencia — el ingreso queda asociado a la categoría, no a un producto. Glosa: "Compra SII {numero} — ingreso inventario".
Emite el mismo hook con el mismo payload shape.
Hook emitido
Section titled “Hook emitido”| Evento | Cuándo | Payload |
|---|---|---|
compra_inv:contabilizada | procesarCompraExistencia y procesarCompraIngresoCategoria, después del COMMIT, por cada movimiento_id que findCompraInvContabilizadaEventPayload resuelva con payload no-null. | { movimientoCostoId, compraDetalleId, categoriaId, monto } |
Si el repository no puede construir el payload (datos incompletos), el evento no se emite pero la operación se completa OK.
reversarCompraIngreso(ctx, compraId)
Section titled “reversarCompraIngreso(ctx, compraId)”Inverso de procesar: borra el movimiento y revierte las líneas de detalle. Devuelve { movimiento_eliminado, lineas_revertidas }.
Bulk operations por proveedor
Section titled “Bulk operations por proveedor”Dos métodos para operar a granel sobre todas las compras de un proveedor en un período:
eliminarComprasDetalleByProveedor(ctx, rutProveedor, año, mes?)
Section titled “eliminarComprasDetalleByProveedor(ctx, rutProveedor, año, mes?)”Borra TODAS las líneas de detalle pendientes del proveedor en el período. Útil para “limpiar” después de un cambio de criterio de clasificación. No emite hook (no hay movimientos asociados a esas líneas pendientes).
generarExistenciasPorProveedor(ctx, rutProveedor, año, mes?)
Section titled “generarExistenciasPorProveedor(ctx, rutProveedor, año, mes?)”Contabiliza TODAS las líneas de detalle pendientes del proveedor. Útil para procesar bandeja completa de un proveedor recurrente.
mes es opcional — si se omite, procesa todo el año.
Vista de bandeja — getComprasExistencias
Section titled “Vista de bandeja — getComprasExistencias”Devuelve { compras, resumen_periodo, resumen_mensual }. El service hace el cálculo de resumen en JS después de la query (no en SQL):
const resumen_periodo = compras.reduce((acc, c) => {acc.documentos += 1;acc.procesadas += c.procesado_inventario ? 1 : 0;acc.pendientes += c.procesado_inventario ? 0 : 1;acc.monto_neto += Number(c.monto_neto_signed ?? 0);acc.monto_otros_impuestos += Number(c.monto_otros_impuestos_signed ?? 0);acc.monto_iva += Number(c.monto_iva_signed ?? 0);acc.monto_total += Number(c.monto_total_signed ?? 0);return acc;}, { documentos: 0, procesadas: 0, pendientes: 0, monto_neto: 0, monto_otros_impuestos: 0, monto_iva: 0, monto_total: 0 });
// resumen_mensual: agrupa por `${año}-${mes}` y suma lo mismo, ordenado por (año, mes) ascendente.Razón: el repository ya filtra y trae todas las filas; agregar en SQL requeriría 3 queries (lista + resumen + resumen mensual). Hacerlo en JS es más simple y, para volúmenes típicos (menos de 1000 compras/período), el costo es despreciable.
Filtros aceptados en la bandeja
Section titled “Filtros aceptados en la bandeja”| Filtro | Tipo | Notas |
|---|---|---|
año | number | Año del documento. |
mes | number | Mes del documento. |
rutProveedor | string | RUT exacto. |
categoriaId | number | Solo compras ya clasificadas en esa categoría. |
estado | 'pendientes' | 'procesadas' | Filtro de procesado_inventario. |
Endpoints
Section titled “Endpoints”| Método | Ruta | Service call |
|---|---|---|
GET | /api/inventario/compras | getComprasExistencias(ctx, filtros) |
POST | /api/inventario/compras/:id/procesar | procesarCompraExistencia(ctx, id, data) |
POST | /api/inventario/compras/:id/procesar-categoria | procesarCompraIngresoCategoria(ctx, id, data) |
POST | /api/inventario/compras/:id/reversar | reversarCompraIngreso(ctx, id) |
GET | /api/inventario/compras/pendientes | getComprasDetallePendientes(ctx, filtros) |
DELETE | /api/inventario/compras/pendiente/:id | eliminarCompraDetalle(ctx, id) |
DELETE | /api/inventario/compras/proveedor/:rut/:año/:mes? | eliminarComprasDetalleByProveedor(ctx, rut, año, mes) |
POST | /api/inventario/compras/generar-por-proveedor/:rut/:año/:mes? | generarExistenciasPorProveedor(ctx, rut, año, mes) |
GET | /api/inventario/compras/contabilizadas | getComprasContabilizadas(ctx, filtros) |
GET | /api/inventario/compras/contabilizadas/:rut/:catId | getComprasContabilizadasDetalle(ctx, rut, catId, filtros) |