Saltar a contenido

Envases y vales

El cliente devuelve envases retornables (botellas, cajones) y a cambio recibe un vale (un documento de crédito con código y QR) que puede redimir en una compra. Este dominio vive en el módulo envases del backend y en las pantallas de envases del POS. Es el paso 5 del cómputo del ticket.

El código es la fuente de verdad

Algunos detalles (nombres de campo, longitudes) se reproducen del código a la fecha. Ante discrepancias, mandan envases/ (backend) y lib/.../envase, lib/.../vale (POS).

Modelo de datos

Vale (auditado con Envers)

envases/model/Vale.java, tabla Vales en TsEnvases, anotada @Audited:

Campo Rol
id PK.
nroVale Número correlativo único, generado por una sequence atómica (dbo.seq_vales_nrovale).
codVale Código del vale; viaja encriptado AES al cliente.
nroSucursal Sucursal origen.
fechaCreacion / fechaVto Creación y vencimiento (fechaVto = fechaCreacion + diasCaducidad, default 365 días).
terminalOrigen / userOrigen Quién lo emitió.
estado ValeEstado (ver abajo).
envases JSON serializado de la lista de envases que contiene.
cantEnvases Total de envases en el vale.
referencia, trxOrigen, trxModificacion, fechaModificacion, terminalModificacion Trazabilidad.

Por qué Envers

El vale es dinero. Cada cambio de estado genera una fila en Vales_AUD + REVINFO: se puede reconstruir el ciclo de vida completo (creación, reserva, consumo) para auditoría. No se borra ni se pisa historia.

Envase

envases/model/Envase.java, tabla Envases:

ean (clave de búsqueda), plu, descripcion, nroSucursal, imagen (@Lob, se sirve como Base64), habilitado, orden. Se asocia a un artículo del catálogo por EAN cuando el artículo tiene bEnvase = true.

Ciclo de vida del vale

stateDiagram-v2
    [*] --> DISPONIBLE: newVale()
    DISPONIBLE --> RESERVADO: inicia compra con vale
    DISPONIBLE --> CONSUMIDO: consumo directo
    DISPONIBLE --> ANULADO
    DISPONIBLE --> VENCIDO: fechaVto pasada (lazy)
    RESERVADO --> CONSUMIDO: todos los envases usados
    RESERVADO --> DISPONIBLE: revert (compra cancelada)
    CONSUMIDO --> [*]
    VENCIDO --> [*]
    ANULADO --> [*]

ValeEstado: DISPONIBLE, RESERVADO, CONSUMIDO, VENCIDO, ANULADO. Las transiciones las valida updateValeSafty (en tickets):

  • CONSUMIDO y VENCIDO son terminales.
  • Persistir el mismo estado es OK (idempotencia, fix TSTK-009).
  • En un revert, no se puede resucitar un vale ANULADO/VENCIDO (fix TSTK-010).

Control de vencimiento (lazy)

El vencimiento se evalúa al leer el vale: si está DISPONIBLE y fechaVto ya pasó, se marca VENCIDO y se persiste en el acto (isValeVigentegetVale). No hay un job que recorra todos; se resuelve en el momento del uso.

Numeración y código

nroVale atómico

Antes se hacía findMaxNroVale() + 1, con race condition: dos threads leían el mismo máximo y generaban el mismo número. El fix (docs/db/manual/fix_vale_nrovale_dup.sql) introdujo:

  1. Una sequence dbo.seq_vales_nrovale (SELECT NEXT VALUE FOR … es atómico en la DB).
  2. Un unique index UX_Vales_nrovale como defensa en runtime.

codVale encriptado (AES)

El codVale se guarda en claro en la DB pero viaja encriptado AES (AES/ECB/PKCS5Padding, Base64) al cliente. Cuando el cliente lo redime, lo manda encriptado y el backend lo desencripta para buscarlo (GET /vales?codVale=…). El motivo es ofuscación: que nadie lea ni adivine los códigos de vale de otros.

Detalle de implementación sensible

La clave AES vive en configuración (app.security.aes-key). Es un dato sensible: no lo hardcodees ni lo loguees. La desencriptación hace URL-decode primero (para %2F, %2B) antes de descifrar.

Cómputo de envases en el ticket

EnvaseService.computTicketEnvase (en tickets) determina, para cada ítem de tipo ENVASE, cuántos se cobran y cuántos se consumen contra vales:

cantEnvases          = Σ ítems tipo ENVASE (no anulados)
cantEnvasesConsumidos = envases emparejados con vales DISPONIBLE/RESERVADO
cantEnvasesCobrar     = max(0, cantEnvases − cantEnvasesConsumidos)   // TSTK-011: clamp a cero
  • Cada envase del ticket que matchea (por EAN) un envase de un vale lo consume; cuando un vale agota todos sus envases, pasa a CONSUMIDO.
  • Si quedan envases del vale sin usar (saldo), se crea un vale nuevo con ese saldo (createValeDiff), que queda DISPONIBLE para la próxima compra.
  • El crédito por envases impacta el núcleo impositivo del ticket como un movimiento de descuento.

Vales TOMRA (máquinas de terceros)

Los vales TOMRA (de máquinas recicladoras de terceros) son de un solo uso. registrarRedencionTomra inserta una fila en tomra_redimido con PK por operación; un segundo intento viola la PK y se rechaza (ValeEnvaseException: Vale TOMRA ya redimido). Es la defensa anti-doble-redención.

Flujo en la terminal

sequenceDiagram
    participant C as Cliente
    participant POS as EnvasesPage / ValeBloc
    participant API as Backend (envases)
    C->>POS: selecciona envases (+/-)
    POS->>POS: ValeBloc acumula envases
    C->>POS: "Imprimir Vale"
    POS->>API: POST /envases/vale {envases, terminal, usuario}
    API->>API: newVale() → nroVale + codVale AES + fechaVto
    API-->>POS: ValeDTO + voucher ESC/POS
    POS->>POS: imprime vale + QR
  • Pantalla: EnvasesPage (lib/views/envases/). EnvaseBloc carga los envases disponibles de la sucursal; ValeBloc acumula la selección y dispara CreateVale.
  • Redención: en una compra, el cliente presenta el vale (QR/código); el ticket lo consume según el cómputo de arriba.
  • Casos de uso: TiprePOS/doc/documentation/CasosUsos/Autocompra_Modo_Envase.md y Autocompra_Modo_Autoservicio.md.

Concurrencia y validaciones

  • Doble consumo: updateVale usa lock pesimista (findByCodValeForUpdate, SELECT … FOR UPDATE). El segundo thread espera, lee el estado ya actualizado (CONSUMIDO) y se rechaza.
  • Transaccionalidad: las operaciones de vale corren bajo el TransactionManager del datasource de envases, con rollbackFor = Exception.

Endpoints

ValeController (/vales): GET /vales?codVale= (desencripta y busca), GET /vales/search (filtros), POST /vales (alta), PUT /vales (cambio de estado), GET /vales/last. EnvaseController (/envases): GET /envases?nroSucursal=, POST /envases. Desde tickets, EnvasesRestController orquesta estas operaciones para el flujo de compra.

Fix Qué resuelve
TSTK-009 Persistir el mismo estado del vale es idempotente, no error.
TSTK-010 No revertir a DISPONIBLE un vale ANULADO/VENCIDO.
TSTK-011 cantEnvasesCobrar con clamp a cero (no reembolsos espurios).
nroVale dup Sequence atómica + unique index.