Plataforma técnica · Orchestrator
Stock Service
Orchestrator Inventarios Stock
StockService administra stock actual y movimientos de inventario (movimientos_inventario). Es el único service del dominio que emite existencia:castigada (cuando un ajuste genera una salida neta) y el responsable del cierre mensual del saldo de stock por categoría.
Operaciones
Section titled “Operaciones”| Método | Para |
|---|---|
getStockActual(ctx, filtros?) | Stock vigente (consolidado por existencia). |
getMovimientos(ctx, existenciaId) | Historial de movimientos de una existencia. |
registrarEntrada(ctx, data) | ENTRADA manual al inventario (no vía compra SII). |
actualizarEstadoStock(ctx, movimientoId, data) | Cambia estado del movimiento: CONFIRMADO/ANULADO/CONTABILIZADO. |
eliminarStockMovimiento(ctx, movimientoId) | Borra movimiento (con cuidado contable). |
ajustarSaldoInventario(ctx, data) | Ajusta saldo al stock_objetivo; genera ENTRADA o SALIDA según diff. Emite hook si SALIDA. |
getSaldoStockMensual(ctx, año, mes) | Saldo por categoría al cierre del mes. |
procesarSaldoStock(ctx, data) | Cierre mensual: consolida y persiste saldo_stock_mensual. |
reversarSaldoStock(ctx, data) | Revierte el cierre del período. |
registrarEntrada — ENTRADA manual
Section titled “registrarEntrada — ENTRADA manual”Crea un movimiento de inventario tipo ENTRADA sin venir de una compra SII. Útil para inventario inicial, donaciones, devoluciones de cliente, etc.
Validaciones:
validateRequired(['existencia_id', 'cantidad', 'costo_unitario', 'fecha_documento', 'tipo_documento']).cantidad > 0(estricto).costo_unitario > 0(estricto).- La existencia debe existir (lookup en TX).
flowchart TB
IN["registrarEntrada(data)"]
V["validateRequired + cantidad>0 + costo>0"]
TX["withTransaction"]
EXI["ExistenciasRepository.findExistenciaById"]
CHK{"existe?"}
ERR["NotFoundError 'Existencia'"]
INS["StockRepository.registrarEntrada"]
RET["{ movimiento_id }"]
IN --> V --> TX --> EXI --> CHK
CHK -- no --> ERR
CHK -- sí --> INS --> RET ajustarSaldoInventario — algoritmo con detección de SALIDA
Section titled “ajustarSaldoInventario — algoritmo con detección de SALIDA”Lleva el saldo de una existencia al stock_objetivo calculando la diferencia y generando ENTRADA o SALIDA según corresponda. Si el resultado es SALIDA (castigado), emite existencia:castigada.
interface AjustarSaldoInventarioDTO {existencia_id: string;fecha_documento: string; // YYYY-MM-DDstock_objetivo: number; // ≥ 0costo_unitario?: number; // ≥ 0; default según política del repoglosa?: string;}Validaciones:
| Campo | Regla |
|---|---|
stock_objetivo | Número finito ≥ 0. |
costo_unitario | Si se pasa: número finito ≥ 0. |
Flujo interno:
flowchart TB
IN["ajustarSaldoInventario(dto)"]
V["validaciones"]
TX["withTransaction"]
REP["StockRepository.ajustarSaldoInventario<br/>(calcula diff vs stock actual,<br/>genera ENTRADA o SALIDA)"]
CHK{"result.tipo_movimiento === SALIDA?"}
PAY["StockRepository<br/>.findExistenciaCastigadaEventPayload<br/>(movimiento_id, glosa)"]
EMIT["outbox.queue<br/>(existencia:castigada, payload)"]
RET["return result"]
IN --> V --> TX --> REP --> CHK
CHK -- sí --> PAY --> EMIT --> RET
CHK -- no --> RET Cuándo se emite el hook:
result.tipo_movimiento === 'SALIDA'(elstock_objetivoera menor al actual → ajuste negativo).result.movimiento_idestá definido (la operación realmente creó un movimiento).findExistenciaCastigadaEventPayloadretornó un payload (la existencia tiene categoría y datos suficientes para el evento).
Si las 3 condiciones no se cumplen, el ajuste se persiste pero no se emite hook. Por ejemplo, un ajuste que sube el stock genera ENTRADA y no notifica.
Glosa del payload: usa data.glosa?.trim() o, por defecto, "Ajuste negativo de saldo".
Estados de movimiento — actualizarEstadoStock
Section titled “Estados de movimiento — actualizarEstadoStock”Estados permitidos: CONFIRMADO, ANULADO, CONTABILIZADO.
Estado inicial típico tras registrar entrada o ingreso por compra. El movimiento es válido y participa en el cálculo de saldo y costo de ventas.
El movimiento queda visible pero no se considera en saldos ni cálculos. No se borra para preservar trazabilidad.
Aplicado por flujos posteriores (CostoVentasService, asientos manuales). Indica que el movimiento ya impactó el libro mayor.
El estado se normaliza con String(estado).trim().toUpperCase() antes de validar — acepta variantes de casing.
eliminarStockMovimiento
Section titled “eliminarStockMovimiento”Borrado físico dentro de TX. No verifica si el movimiento ya está contabilizado — el operador es responsable. Si necesitas el guard, encapsular el borrado tras un check del estado:
const mov = await getMovimientos(...);if (mov.estado === 'CONTABILIZADO') throw new Error('Movimiento contabilizado');await eliminarStockMovimiento(ctx, mov.id);(El service no lo hace por defecto — confía en el operador o la UI.)
Cierre mensual — procesarSaldoStock / reversarSaldoStock
Section titled “Cierre mensual — procesarSaldoStock / reversarSaldoStock”procesarSaldoStock({ año, mes, categoria_id? }) consolida el stock al cierre del mes en saldo_stock_mensual. Si se pasa categoria_id, solo procesa esa categoría; si no, todas.
Validaciones:
año ∈ [2000, 2100], entero.mes ∈ [1, 12], entero.
ctx.userId se sanitiza con safeUserId = ctx.userId && ctx.userId !== "undefined" ? ctx.userId : null — protección contra strings literales "undefined" que pueden colarse desde rutas mal formadas.
reversarSaldoStock aplica la operación inversa (borra el saldo consolidado del período/categoría). Las mismas validaciones.
Ninguno emite hook — el cierre es una operación interna; los hooks ya se emitieron cuando los movimientos individuales se procesaron.
Hook emitido
Section titled “Hook emitido”| Evento | Cuándo | Payload |
|---|---|---|
existencia:castigada | ajustarSaldoInventario cuando el resultado es SALIDA | { movimientoCostoId, categoriaId, monto, motivo } |
motivo viene de data.glosa.trim() o el default "Ajuste negativo de saldo".
Endpoints
Section titled “Endpoints”| Método | Ruta | Service call |
|---|---|---|
GET | /api/inventario/stock | getStockActual(ctx, filtros) |
GET | /api/inventario/movimientos/:existenciaId | getMovimientos(ctx, existenciaId) |
POST | /api/inventario/movimientos/entrada | registrarEntrada(ctx, data) |
PATCH | /api/inventario/movimientos/:id/estado | actualizarEstadoStock(ctx, id, data) |
DELETE | /api/inventario/movimientos/:id | eliminarStockMovimiento(ctx, id) |
POST | /api/inventario/ajustar-saldo | ajustarSaldoInventario(ctx, data) |
GET | /api/inventario/saldo-stock/:año/:mes | getSaldoStockMensual(ctx, año, mes) |
POST | /api/inventario/saldo-stock/procesar | procesarSaldoStock(ctx, data) |
POST | /api/inventario/saldo-stock/reversar | reversarSaldoStock(ctx, data) |