Validación backend de un movimiento de venta con Java Servlets

 

Cuando desarrollamos un sistema de ventas, una de las partes más importantes no es solamente capturar los datos ingresados por el usuario, sino comprobar que la operación sea válida antes de procesarla.

En una aplicación web podemos utilizar JavaScript para realizar cálculos y entregar validaciones inmediatas en la interfaz. Sin embargo, las reglas importantes deben comprobarse nuevamente en el backend.

En este ejemplo veremos cómo estructurar la validación de un movimiento comercial utilizando Java Servlets, una capa de servicios y excepciones específicas.

El controlador utilizado en el ejemplo recibe una operación que posteriormente puede utilizarse para procesar distintos tipos de documentos, como facturas, facturas exentas, boletas, guías de despacho, notas de crédito o notas de débito.

1. El problema

Una operación de venta contiene bastante más información que un formulario tradicional.

Por ejemplo, necesitamos recibir:

  • Cliente o proveedor.

  • Tipo de documento.

  • Bodega.

  • Tipo de despacho.

  • Tipo de traslado.

  • Uno o más productos.

  • Cantidades.

  • Precios.

  • Totales.

  • IVA.

  • Montos afectos o exentos.

Además, no basta con comprobar que estos datos existan.

También debemos verificar que sean coherentes entre sí.

Por ejemplo, una factura afecta no debería contener un monto exento y una factura exenta no debería contener IVA.

Por esta razón, antes de construir y procesar el movimiento utilizaremos una serie de validaciones en el backend.

2. Flujo general del Servlet

El procesamiento puede representarse de la siguiente manera:

Formulario
    ↓
POST
    ↓
Validar cliente
    ↓
Validar existencia de detalles
    ↓
Validar totales
    ↓
Validar líneas del documento
    ↓
Obtener despacho y traslado
    ↓
Obtener cliente
    ↓
Construir Movimiento
    ↓
Construir DetalleMovimiento
    ↓
MovimientoService

Una característica importante de este flujo es que intentamos detectar los errores antes de entregar el movimiento a la capa de servicio.

3. Primera validación: datos básicos

El Servlet comienza comprobando que exista un cliente o proveedor seleccionado.

String auxcliprov_id =
        request.getParameter("CliProvId");

if (auxcliprov_id == null ||
        auxcliprov_id.isBlank()) {

    throw new MovimientoException(
        "Debe seleccionar Cliente/Proveedor"
    );
}

También comprobamos que exista al menos una línea de detalle:

int nro_filas =
        Integer.parseInt(
            request.getParameter("NRO_FILAS")
        );

if (nro_filas < 1) {

    throw new MovimientoException(
        "Debe ingresar al menos un detalle"
    );
}

Después ejecutamos las validaciones más específicas:

validaTotales(request);
validaDetalle(request);

Si alguna de ellas genera una excepción, el procesamiento se interrumpe.

4. Validación del detalle

Un documento comercial puede contener una cantidad variable de líneas.

Por este motivo recorremos el detalle y comprobamos individualmente cada posición.

private void validaDetalle(
        HttpServletRequest request)
        throws MovimientoException {

    int nro_filas =
            Integer.parseInt(
                request.getParameter("NRO_FILAS")
            );

    for (int i = 0; i < nro_filas; i++) {

        String producto =
                request.getParameter(
                    "ProductoId" + i
                );

        if (producto == null ||
                producto.isBlank()) {

            throw new MovimientoException(
                "Debe seleccionar producto en linea "
                + (i + 1)
            );
        }

        String cantidad =
                request.getParameter(
                    "Cantidad" + i
                );

        if (cantidad == null ||
                cantidad.isBlank()) {

            throw new MovimientoException(
                "Debe ingresar cantidad en linea "
                + (i + 1)
            );
        }

        if (Integer.parseInt(cantidad) <= 0) {

            throw new MovimientoException(
                "Cantidad en linea "
                + (i + 1)
                + " no válida"
            );
        }

        String precioUnidad =
                request.getParameter(
                    "PrecioUnidad" + i
                );

        if (precioUnidad == null ||
                precioUnidad.isBlank()) {

            throw new MovimientoException(
                "Debe ingresar precio unitario en linea "
                + (i + 1)
            );
        }

        String total =
                request.getParameter(
                    "Total" + i
                );

        if (total == null ||
                total.isBlank()) {

            throw new MovimientoException(
                "Debe ingresar total en linea "
                + (i + 1)
            );
        }
    }
}

Aquí estamos comprobando que cada línea tenga producto, cantidad, precio y total.

También evitamos procesar cantidades iguales o inferiores a cero.

5. Validar reglas según el documento

No todos los documentos tienen las mismas reglas.

Por eso recuperamos el tipo de documento:

int tpoDocumentoId =
        Integer.parseInt(
            request.getParameter("tpoDocumentoId")
        );

DocumentoService documentoService =
        new DocumentoService();

Luego obtenemos su código:

int codSii;

try {

    codSii =
        documentoService.getSiiCod(
            tpoDocumentoId
        );

} catch (Exception ex) {

    System.getLogger(
        AddMovimiento.class.getName()
    ).log(
        System.Logger.Level.ERROR,
        "Error al obtener código SII",
        ex
    );

    throw new TotalesInvalidosException(
        "No fue posible validar el tipo de documento."
    );
}

Si no podemos determinar qué documento estamos procesando, detenemos la operación.

6. Validación de los totales

Recuperamos los montos enviados:

int montoNeto =
        Integer.parseInt(
            request.getParameter("MontoNeto")
        );

int montoExento =
        Integer.parseInt(
            request.getParameter("MontoExento")
        );

int iva =
        Integer.parseInt(
            request.getParameter("Iva")
        );

int montoTotal =
        Integer.parseInt(
            request.getParameter("MontoTotal")
        );

Ahora podemos aplicar reglas dependiendo del documento.

Factura afecta

Para el código 33 comprobamos que no exista monto exento:

if (codSii == 33) {

    if (montoExento > 0) {

        throw new TotalesInvalidosException(
            "NO SE PUEDE COLOCAR MONTO EXENTO EN FACTURA AFECTA"
        );
    }

    if (montoNeto + iva != montoTotal) {

        throw new TotalesInvalidosException(
            "Los totales del documento no son consistentes."
        );
    }
}

También verificamos la relación:

Monto Neto + IVA = Monto Total

Factura exenta

Para el código 34 aplicamos una regla diferente:

if (codSii == 34) {

    if (montoNeto > 0 || iva > 0) {

        throw new TotalesInvalidosException(
            "NO SE PUEDE COLOCAR MONTO AFECTO EN FACTURA EXENTA"
        );
    }

    if (montoExento != montoTotal) {

        throw new TotalesInvalidosException(
            "Los totales del documento no son consistentes."
        );
    }
}

En este caso:

Monto Neto = 0
IVA = 0
Monto Exento = Monto Total

Esto muestra la diferencia entre validar un dato y validar una regla de negocio.

Los cuatro valores podrían ser números perfectamente válidos, pero eso no significa que sean correctos para el documento que estamos procesando.

7. Construcción del movimiento

Solamente después de superar las validaciones construimos el objeto Movimiento.

Movimiento movimiento =
        new Movimiento();

movimiento.setMontoneto(montoNeto);
movimiento.setMontoiva(iva);
movimiento.setMontoexento(montoExento);
movimiento.setMontototal(montoTotal);

movimiento.setEmpresaid(empresaid);
movimiento.setTipodespacho(despacho);
movimiento.setTipotraslado(traslado);

movimiento.setBodegaid(
        Integer.parseInt(
            request.getParameter("Bodega")
        )
);

movimiento.setObjcliprov(cliprov);

Esta separación es importante.

Primero comprobamos:

¿La operación es válida?

y solamente después:

Construir Movimiento

8. Construcción de los detalles

Posteriormente transformamos las líneas recibidas desde el formulario en objetos DetalleMovimiento.

ArrayList<DetalleMovimiento> detalleMovimiento =
        new ArrayList<>();

ProductoService productoService =
        new ProductoService(empresaid);

for (int i = 0; i < nro_filas; i++) {

    DetalleMovimiento detalle =
            new DetalleMovimiento();

    int productoid =
            Integer.parseInt(
                request.getParameter(
                    "ProductoId" + i
                )
            );

    Producto producto =
            productoService.selectProducto(
                productoid
            );

    BigDecimal precioUnidad =
            new BigDecimal(
                request.getParameter(
                    "PrecioUnidad" + i
                )
            );

    BigDecimal cantidad =
            new BigDecimal(
                request.getParameter(
                    "Cantidad" + i
                )
            );

    int total =
            Integer.parseInt(
                request.getParameter(
                    "Total" + i
                )
            );

    detalle.setObjProducto(producto);
    detalle.setPrecioVentaAplicado(precioUnidad);
    detalle.setPrecioVentaOriginal(precioUnidad);
    detalle.setCantidad(cantidad);
    detalle.setTotal(total);

    detalleMovimiento.add(detalle);
}

Finalmente asociamos los detalles:

movimiento.setObjdetalle(
    detalleMovimiento
);

9. Delegar el procesamiento

El Servlet no realiza directamente toda la lógica de persistencia.

Una vez construido el objeto, lo entrega a la capa de servicio:

MovimientoService movimientoService =
        new MovimientoService();

movimientoService.addMovimiento(
        movimiento
);

Esto permite que el Servlet se encargue principalmente de recibir la petición HTTP, validar los datos necesarios para el caso de uso y construir los objetos requeridos para continuar el procesamiento.

10. Manejo de errores

Podemos utilizar excepciones específicas para representar situaciones conocidas.

Por ejemplo:

catch (StockInsuficienteException |
       TotalesInvalidosException ex) {

    Context context = new Context();

    context.setVariable(
        "mensaje",
        ex.getMessage()
    );

    response.setContentType("text/html");

    String contenido =
        templateEngine.process(
            "/error/error",
            context
        );

    response.getWriter().write(
        contenido
    );
}

De esta manera podemos diferenciar, por ejemplo:

MovimientoException
        → problema con los datos del movimiento

TotalesInvalidosException
        → problema con los montos

StockInsuficienteException
        → problema de existencia

Exception
        → error inesperado

No todos los errores representan lo mismo y utilizar excepciones específicas permite expresar mejor las condiciones que pueden impedir una operación.

11. Servlet completo

Finalmente, podemos observar todas estas piezas dentro del controlador.

public class AddMovimiento extends HttpServlet {

    private TemplateEngine templateEngine;

    @Override
    public void init() {

        ThymeleafConfig thymeleafConfig =
                new ThymeleafConfig();

        templateEngine =
                thymeleafConfig.createTemplateEngine();
    }

    @Override
    protected void doPost(
            HttpServletRequest request,
            HttpServletResponse response)
            throws IOException, ServletException {

        try {

            String auxcliprov_id =
                    request.getParameter("CliProvId");

            if (auxcliprov_id == null ||
                    auxcliprov_id.isBlank()) {

                throw new MovimientoException(
                    "Debe seleccionar Cliente/Proveedor"
                );
            }

            int nro_filas =
                    Integer.parseInt(
                        request.getParameter("NRO_FILAS")
                    );

            if (nro_filas < 1) {

                throw new MovimientoException(
                    "Debe ingresar al menos un detalle"
                );
            }

            validaTotales(request);
            validaDetalle(request);

            int tpoDocumentoId =
                    Integer.parseInt(
                        request.getParameter(
                            "tpoDocumentoId"
                        )
                    );

            int montoNeto =
                    Integer.parseInt(
                        request.getParameter("MontoNeto")
                    );

            int montoExento =
                    Integer.parseInt(
                        request.getParameter("MontoExento")
                    );

            int iva =
                    Integer.parseInt(
                        request.getParameter("Iva")
                    );

            int montoTotal =
                    Integer.parseInt(
                        request.getParameter("MontoTotal")
                    );

            int despachoId =
                    Integer.parseInt(
                        request.getParameter("despachoId")
                    );

            DespachoService despachoService =
                    new DespachoService();

            Despacho despacho =
                    despachoService.getData(
                        despachoId
                    );

            int trasladoId =
                    Integer.parseInt(
                        request.getParameter("trasladoId")
                    );

            TrasladoService trasladoService =
                    new TrasladoService();

            Traslado traslado =
                    trasladoService.getData(
                        trasladoId
                    );

            int empresaid =
                    (int) request.getSession()
                        .getAttribute("empresaid");

            CliProvService cliprovService =
                    new CliProvService(empresaid);

            CliProv cliprov =
                    cliprovService.selectCliProvById(
                        Integer.parseInt(auxcliprov_id)
                    );

            if (cliprov == null) {

                throw new MovimientoException(
                    "El cliente/proveedor seleccionado no existe."
                );
            }

            Movimiento movimiento =
                    new Movimiento();

            movimiento.setMontoneto(montoNeto);
            movimiento.setMontoiva(iva);
            movimiento.setMontoexento(montoExento);
            movimiento.setMontototal(montoTotal);

            movimiento.setEmpresaid(empresaid);
            movimiento.setTipodespacho(despacho);
            movimiento.setTipotraslado(traslado);

            movimiento.setBodegaid(
                Integer.parseInt(
                    request.getParameter("Bodega")
                )
            );

            movimiento.setObjcliprov(cliprov);

            ArrayList<DetalleMovimiento> detalles =
                    new ArrayList<>();

            ProductoService productoService =
                    new ProductoService(empresaid);

            for (int i = 0; i < nro_filas; i++) {

                DetalleMovimiento detalle =
                        new DetalleMovimiento();

                int productoId =
                        Integer.parseInt(
                            request.getParameter(
                                "ProductoId" + i
                            )
                        );

                Producto producto =
                        productoService.selectProducto(
                            productoId
                        );

                BigDecimal precioUnidad =
                        new BigDecimal(
                            request.getParameter(
                                "PrecioUnidad" + i
                            )
                        );

                BigDecimal cantidad =
                        new BigDecimal(
                            request.getParameter(
                                "Cantidad" + i
                            )
                        );

                int total =
                        Integer.parseInt(
                            request.getParameter(
                                "Total" + i
                            )
                        );

                detalle.setObjProducto(producto);
                detalle.setPrecioVentaAplicado(
                    precioUnidad
                );
                detalle.setPrecioVentaOriginal(
                    precioUnidad
                );
                detalle.setCantidad(cantidad);
                detalle.setTotal(total);

                detalles.add(detalle);
            }

            movimiento.setObjdetalle(detalles);

            MovimientoService movimientoService =
                    new MovimientoService();

            movimientoService.addMovimiento(
                movimiento
            );

        } catch (MovimientoException ex) {

            mostrarError(
                response,
                ex.getMessage()
            );

        } catch (NumberFormatException ex) {

            mostrarError(
                response,
                "Uno de los valores numéricos ingresados no es válido."
            );

        } catch (StockInsuficienteException |
                 TotalesInvalidosException ex) {

            System.getLogger(
                AddMovimiento.class.getName()
            ).log(
                System.Logger.Level.ERROR,
                (String) null,
                ex
            );

            mostrarError(
                response,
                ex.getMessage()
            );

        } catch (Exception ex) {

            System.getLogger(
                AddMovimiento.class.getName()
            ).log(
                System.Logger.Level.ERROR,
                (String) null,
                ex
            );

            mostrarError(
                response,
                "Ha ocurrido un error inesperado."
            );
        }
    }

    // validaDetalle(...)
    // validaTotales(...)
}

El ejemplo completo permite observar que el controlador no representa solamente una inserción.

Está coordinando diferentes etapas de una operación comercial:

HTTP
 ↓
Validación
 ↓
Reglas según documento
 ↓
Construcción del dominio
 ↓
Servicio

12. JavaScript no reemplaza la validación backend

En una interfaz de ventas tiene sentido utilizar JavaScript para calcular IVA, actualizar totales o impedir que el usuario deje campos vacíos.

Eso mejora la experiencia de uso.

Pero existe una diferencia importante:

JavaScript
    ↓
ayuda al usuario

Backend
    ↓
protege la operación

Los parámetros enviados mediante HTTP pueden modificarse. Por ello, una regla importante no debería depender exclusivamente de JavaScript.

Conclusión

Validar desde backend no significa únicamente repetir las comprobaciones realizadas en el frontend.

El servidor también debe validar reglas propias del negocio.

En este ejemplo comprobamos tres niveles diferentes:

  1. Que existan los datos necesarios.

  2. Que las líneas del documento tengan valores válidos.

  3. Que los totales sean coherentes con el tipo de documento.

Solamente después construimos el movimiento y lo entregamos a la capa de servicio.

A medida que se incorporen otros documentos, el controlador puede mantener un flujo común y agregar las reglas particulares correspondientes a cada tipo de operación.

La idea fundamental es:

el frontend facilita la operación; el backend determina si esa operación puede ser procesada.

Comentarios

Entradas populares de este blog

Boleta Electrónica en Chile: Los 3 Ambientes del SII para Desarrolladores

Configurando Servlets y JSP en Jetty

GUIA GENERAL DE GENERACION DE DOCUMENTOS TRIBUTARIOS ELECTRONICOS