Skip to content

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.

MétodoPara
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.

Crea un movimiento de inventario tipo ENTRADA sin venir de una compra SII. Útil para inventario inicial, donaciones, devoluciones de cliente, etc.

Validaciones:

  1. validateRequired(['existencia_id', 'cantidad', 'costo_unitario', 'fecha_documento', 'tipo_documento']).
  2. cantidad > 0 (estricto).
  3. costo_unitario > 0 (estricto).
  4. 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.

DTO
interface AjustarSaldoInventarioDTO {
existencia_id: string;
fecha_documento: string; // YYYY-MM-DD
stock_objetivo: number; // ≥ 0
costo_unitario?: number; // ≥ 0; default según política del repo
glosa?: string;
}

Validaciones:

CampoRegla
stock_objetivoNúmero finito ≥ 0.
costo_unitarioSi 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' (el stock_objetivo era menor al actual → ajuste negativo).
  • result.movimiento_id está definido (la operación realmente creó un movimiento).
  • findExistenciaCastigadaEventPayload retornó 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 estado se normaliza con String(estado).trim().toUpperCase() antes de validar — acepta variantes de casing.

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.

EventoCuándoPayload
existencia:castigadaajustarSaldoInventario cuando el resultado es SALIDA{ movimientoCostoId, categoriaId, monto, motivo }

motivo viene de data.glosa.trim() o el default "Ajuste negativo de saldo".

MétodoRutaService call
GET/api/inventario/stockgetStockActual(ctx, filtros)
GET/api/inventario/movimientos/:existenciaIdgetMovimientos(ctx, existenciaId)
POST/api/inventario/movimientos/entradaregistrarEntrada(ctx, data)
PATCH/api/inventario/movimientos/:id/estadoactualizarEstadoStock(ctx, id, data)
DELETE/api/inventario/movimientos/:ideliminarStockMovimiento(ctx, id)
POST/api/inventario/ajustar-saldoajustarSaldoInventario(ctx, data)
GET/api/inventario/saldo-stock/:año/:mesgetSaldoStockMensual(ctx, año, mes)
POST/api/inventario/saldo-stock/procesarprocesarSaldoStock(ctx, data)
POST/api/inventario/saldo-stock/reversarreversarSaldoStock(ctx, data)