-
Notifications
You must be signed in to change notification settings - Fork 0
Modulo Inventario
Jose Navarro edited this page Aug 12, 2026
·
1 revision
Ledger de stock por (bodega, material), con bloqueo optimista (@Version). No expone HTTP para entrada/salida — esos son métodos internos del servicio, invocados solo desde entrega (ver Módulo Entrega). Los únicos endpoints de escritura son ajuste manual, merma y configuración de topes.
Todo el path /api/inventario/** exige ROLE_ADMIN.
| Método | Path | Request | Response |
|---|---|---|---|
| GET | /api/inventario |
bodegaId (req.), tipoMaterialId?, bajoMinimo?, page, size |
RespuestaPagina<RespuestaLineaInventario> |
| GET | /api/inventario/{id} |
— | RespuestaLineaInventario |
| GET | /api/inventario/{id}/movimientos |
page, size | RespuestaPagina<RespuestaMovimiento> |
| POST | /api/inventario |
CuerpoCrearLinea{bodegaId,tipoMaterialId,stockMinimo,stockMaximo} |
RespuestaLineaInventario (201) |
| PUT | /api/inventario/{id} |
CuerpoTopes{stockMinimo,stockMaximo} |
RespuestaLineaInventario |
| POST | /api/inventario/{id}/ajuste |
CuerpoAjuste{cantidadNueva,motivo} |
RespuestaLineaInventario |
| POST | /api/inventario/{id}/merma |
CuerpoMerma{cantidad,motivo} |
RespuestaLineaInventario |
flowchart TD
subgraph Interno["Métodos internos — SIN endpoint HTTP propio, solo llamados desde entrega"]
ENTRADA["registrarEntrada(bodegaId, materialId, cantidad, ref)"]
ENTRADA --> FIND1{"existe línea\n(bodegaId,materialId)?"}
FIND1 -->|"no"| AUTO["crearLineaAutomatica()\nstockMinimo=0, stockMaximo=0\n(valida Bodega/Material existen)"]
FIND1 -->|"sí"| SUMA["stockActual += cantidad"]
AUTO --> SUMA
SUMA --> MOV1["MovimientoInventario\nTipoOperacion.ENTRADA"]
SALIDA["registrarSalida(bodegaId, materialId, cantidad, ref)"]
SALIDA --> FIND2{"existe línea?"}
FIND2 -->|"no"| E1["StockInvalidoException (400)\n'No hay linea para descontar'"]
FIND2 -->|"sí"| CHECK{"cantidad > stockActual?"}
CHECK -->|"sí"| E2["StockInvalidoException (400)\n'supera el stock actual'"]
CHECK -->|"no"| RESTA["stockActual -= cantidad"]
RESTA --> MOV2["MovimientoInventario\nTipoOperacion.SALIDA"]
end
subgraph HTTP["Endpoints HTTP — ADMIN"]
CREAR["POST /api/inventario"] --> DUP{"línea (bodegaId,materialId)\nya existe?"}
DUP -->|"sí"| E3["LineaDuplicadaException (409)\nrespaldado por UNIQUE constraint"]
DUP -->|"no"| TOPES1{"stockMinimo > stockMaximo?"}
TOPES1 -->|"sí"| E4["StockInvalidoException (400)"]
TOPES1 -->|"no"| CREADA["línea creada, stockActual=0"]
TOPES["PUT /{id} (topes)"] --> TOPES2{"stockMinimo > stockMaximo?"}
TOPES2 -->|"sí"| E4
TOPES2 -->|"no"| ACTOK["topes actualizados"]
AJUSTE["POST /{id}/ajuste"] --> SETDIR["stockActual = cantidadNueva\n(directo, SIN validar contra min/max)"]
SETDIR --> MOV3["MovimientoInventario\nTipoOperacion.AJUSTE\ncantidad=|nueva-anterior|"]
MERMA["POST /{id}/merma"] --> CHECKM{"cantidad > stockActual?"}
CHECKM -->|"sí"| E5["StockInvalidoException (400)"]
CHECKM -->|"no"| RESTAM["stockActual -= cantidad"]
RESTAM --> MOV4["MovimientoInventario\nTipoOperacion.MERMA"]
end
MOV1 & MOV2 & MOV3 & MOV4 --> CONCURRENCIA{"escritura concurrente\nsobre la misma línea\n(@Version)?"}
CONCURRENCIA -->|"conflicto"| E6["ObjectOptimisticLockingFailureException\n→ HTTP 409\n(la segunda escritura falla, no se pisan)"]
CONCURRENCIA -->|"ok"| PERSIST["flush: version++"]
style E1 fill:#f4d4d4
style E2 fill:#f4d4d4
style E3 fill:#f4d4d4
style E4 fill:#f4d4d4
style E5 fill:#f4d4d4
style E6 fill:#f4d4d4
style AUTO fill:#fde9c8
style SETDIR fill:#fde9c8
sequenceDiagram
participant A1 as Admin 1
participant A2 as Admin 2
participant IC as InventarioController
participant IS as InventarioServiceImpl
participant LIR as LineaInventarioRepository
participant DB as PostgreSQL (@Version)
A1->>IC: POST /{id}/ajuste {cantidadNueva:500, motivo:"conteo físico"}
A2->>IC: POST /{id}/ajuste {cantidadNueva:480, motivo:"conteo físico"}
par ambos leen la misma versión
IC->>IS: registrarAjuste (A1)
IS->>LIR: findById(id) → version=3
IC->>IS: registrarAjuste (A2)
IS->>LIR: findById(id) → version=3
end
Note over IS: @LogTransaccional(operacion="INVENTARIO_AJUSTADO")
IS->>DB: UPDATE ... SET stockActual=500, version=4 WHERE version=3 (A1)
DB-->>IS: 1 fila afectada — ok
IS->>DB: UPDATE ... SET stockActual=480, version=4 WHERE version=3 (A2)
DB-->>IS: 0 filas afectadas
IS-->>IC: throw ObjectOptimisticLockingFailureException (A2)
IC-->>A2: 409 — "otro usuario actualizó esta línea"
IC-->>A1: 200 — ajuste aplicado
stateDiagram-v2
note left of ENTRADA: solo desde entrega.registrar()
ENTRADA: ENTRADA
SALIDA: SALIDA (desde entrega.DESPACHADA o entrega.eliminar())
AJUSTE: AJUSTE (HTTP directo, ADMIN)
MERMA: MERMA (HTTP directo, ADMIN)
TRANSFORMACION: TRANSFORMACION — definido en el enum, sin ningún productor en el código actual
-
crearLineaAutomatica()enregistrarEntradacrea líneas constockMinimo=0, stockMaximo=0— una entrega hacia un material/bodega nuevos genera silenciosamente una línea sin topes configurados, que no dispara ninguna alerta de "bajo mínimo" hasta que un admin la edite manualmente enPUT /{id}. -
registrarAjusteno valida el nuevo valor contrastockMinimo/stockMaximo— un ajuste puede dejar la línea en un estado "sobre máximo" o "bajo mínimo" sin bloquear la operación (la UI solo lo muestra como alerta visual después). -
TipoOperacion.TRANSFORMACIONes código muerto en la práctica: existe en el enum pero ningún flujo actual lo produce.
Generada a partir de docs/ en el repo — cualquier cambio de arquitectura real debe actualizar primero el código y CLAUDE.md, y luego reflejarse aquí.