Skip to content

Plataforma técnica · Orchestrator

Existencias Service

Orchestrator Inventarios Existencias

ExistenciasService administra el catálogo de productos que componen el inventario (administracion.existencias o similar — schema interno). Cada existencia es una entidad maestra que después se mueve vía StockService y se ingresa vía ComprasInventarioService.

No emite hooks de dominio — es CRUD puro con validación cruzada de categoría.

MétodoPara
getExistencias(ctx, filtros?)Lista con filtros opcionales (giro, tipo, categoría, giros empresa).
getExistenciaById(ctx, id)Lookup. Lanza NotFoundError("Existencia", id).
createExistencia(ctx, data)Crea con auto-generación de código si no se especifica.
updateExistencia(ctx, id, data)Edita campos del catálogo.
eliminarExistencia(ctx, id)Borra. Si hay movimientos asociados, lanza ValidationError (FK 23503).
getResumenExistencias(ctx, filtros)Vista agregada (saldos, valorización) para reportes.

Existencia (campos clave):

CampoTipoNotas
idUUID
codigostringÚnico; auto-generado si no se especifica.
nombrestringRequerido en create.
girostringRequerido. Filtra qué existencias ve qué empresa.
tipo_existenciastringRequerido. Influye en el código autogenerado.
unidad_medidastringRequerido (e.g. UND, KG).
categoria_idnumber?FK a categorias_existencia.
categoria (texto)string?Snapshot del nombre de categoría al crear/actualizar.
cuenta_existencia_codigostringHeredada de la categoría (1109xxx).
activabooleanFiltro default en listados.
created_at / updated_at
flowchart TB
  IN["createExistencia(data)"]
  V["validateRequired<br/>(nombre, giro, tipo_existencia, unidad_medida)"]
  TX["withTransaction"]
  CAT{"data.categoria_id?"}
  CHK["CategoriasExistenciaRepository<br/>.findCategoriaById"]
  ERR["NotFoundError<br/>'CategoriaExistencia'"]
  GEN{"data.codigo trimmed<br/>length > 0?"}
  USE["codigo = data.codigo.toUpperCase()"]
  AUTO["ExistenciasRepository<br/>.generateExistenciaCodigo<br/>(categoriaNombre, tipoExistencia)"]
  INS["createExistencia(client, { ...data, codigo }, userId)"]
  RET["ServiceResult(existencia)"]

  IN --> V --> TX --> CAT
  CAT -- sí --> CHK
  CHK -- no encontrado --> ERR
  CHK -- ok --> GEN
  CAT -- no --> GEN
  GEN -- sí --> USE --> INS
  GEN -- no --> AUTO --> INS
  INS --> RET

Cuando se pasa categoria_id, el service también guarda categoria (string) con el nombre actual de la categoría. Esto es un snapshot — si la categoría se renombra después, las existencias mantienen su nombre histórico. Si necesitas el nombre vigente, hacer JOIN con categorias_existencia al consultar.

Si el caller no pasa codigo (o pasa string vacío), ExistenciasRepository.generateExistenciaCodigo({ categoriaNombre, tipoExistencia }) lo genera. La lógica vive en el repository — típicamente concatena un prefijo del tipo + slug de categoría + correlativo, todo en mayúsculas.

Si el caller pasa un codigo, se respeta tal cual (uppercase normalizado).

updateExistencia también valida que el categoria_id (si se cambia) exista. No revalida el resto de campos requeridos — confía en que ya existían correctos en la creación.

Manejo FK violation
async eliminarExistencia(ctx, id) {
const pool = this.getPool(ctx.tenantDb);
try {
const deleted = await ExistenciasRepository.deleteExistencia(pool, id);
return this.success({ deleted });
} catch (e) {
if (isPgError(e) && e.code === '23503') {
throw new ValidationError(
'No se puede eliminar la existencia porque tiene movimientos o referencias asociadas.',
);
}
throw e;
}
}

Postgres 23503 = foreign_key_violation. La existencia se usa como FK desde:

  • movimientos_inventario.existencia_id
  • Posiblemente otras tablas downstream

El servicio traduce el error crudo a un mensaje accionable. No verifica antes del DELETE — confía en que el FK rechazará. Esto es preferible: el check + delete tendrían condición de carrera; el FK es atómico.

Devuelve una vista agregada para reportes (saldo, valorización, último movimiento, etc.). El cálculo vive en ExistenciasRepository.findResumenExistencias. Acepta filtros similares al listado normal pero retorna fields agregados.

Esta vista es read-only — StockService.getStockActual complementa con detalle de movimientos vigentes.

El catálogo es estructural: cambiar un nombre o categoría no dispara cadenas reactivas. Los hooks del dominio inventario se emiten en los movimientos (compra_inv:contabilizada, existencia:castigada, costo:egreso_calculado), no en el CRUD del maestro.

Si en el futuro se quiere notificar cambios de catálogo (e.g. para invalidar cache de UI), agregar existencia:created/updated/deleted al DomainEventMap.