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):
CONSUMIDOyVENCIDOson 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 (isValeVigente → getVale). 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:
- Una sequence
dbo.seq_vales_nrovale(SELECT NEXT VALUE FOR …es atómico en la DB). - Un unique index
UX_Vales_nrovalecomo 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 quedaDISPONIBLEpara 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/).EnvaseBloccarga los envases disponibles de la sucursal;ValeBlocacumula la selección y disparaCreateVale. - 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.mdyAutocompra_Modo_Autoservicio.md.
Concurrencia y validaciones¶
- Doble consumo:
updateValeusa 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. |