Skip to content

Modulo Inventario

Jose Navarro edited this page Aug 12, 2026 · 1 revision

Módulo inventario

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.

Endpoints

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

Flujo de negocio

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
Loading

Secuencia HTTP: ajuste manual con conflicto de concurrencia

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
Loading

Movimientos vs. TipoOperacion

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
Loading

Notas

  • crearLineaAutomatica() en registrarEntrada crea líneas con stockMinimo=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 en PUT /{id}.
  • registrarAjuste no valida el nuevo valor contra stockMinimo/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.TRANSFORMACION es código muerto en la práctica: existe en el enum pero ningún flujo actual lo produce.

Clone this wiki locally