ChatGPT Thinking 5.4
MODEL.MD — POS / TICKET / LEDGER / MOTOR DE PROMOCIONES
Documento unificado de referencia del dominio
Versión CORE: 202604161820
Versión MOTOR DE PROMOCIONES: 202604161820
Reemplaza:core.md+promociones.md
Consolida: estrategia COMBO integrada + lineamientos de implementación Java 21
Tabla de contenidos
PARTE I — CORE
- Objetivo y principios
- Agregado raíz: Ticket
- Granos del modelo
- Objetos del dominio
- Regla de itemización
- Catálogos y registros del ticket
- Estructura de Movimiento
- Reglas de pagos
- Reglas de ledger y convenciones de signos
- Regla de excedente y vuelto
- Reglas de reconciliación
- Orden base de cálculo
PARTE II — MOTOR DE PROMOCIONES 13. Modelo de promociones: tres capas 14. Modos de cómputo e inferencia de alcance 15. Catálogos auxiliares de promociones 16. Definición de promoción: listapromociones 17. Promoción aplicada en el ticket: promociones[] 18. Flujo del motor MODO ITEM 19. Flujo específico — método COMBO (estrategia de resolución) 20. Flujo del motor MODO PAGO y MODO_POSTPAGO
PARTE III — IMPLEMENTACIÓN 21. Convenciones Java 21 22. Convenciones de naming 23. Relación con los JSON 24. Criterios de revisión 25. Resumen ejecutivo
PARTE I — CORE
1. Objetivo y principios
1.1 Objetivo
Este documento es la referencia central única del modelo de dominio POS / Ticket / Ledger y del Motor de Promociones. Define el concepto funcional, la estructura del agregado Ticket, las reglas de cálculo, las reglas de itemización, las reglas de ledger, el modelo completo de promociones, las convenciones de nombres y los criterios de reconciliación.
Los archivos JSON del modelo (jsonmodel) son ejemplos estructurales concretos y deben mantenerse consistentes con este documento.
En caso de contradicción entre este documento y archivos JSON o documentos históricos:
- este
model.mdtiene prioridad absoluta jsonmodel- documentos históricos anteriores
1.2 Principios de diseño
El modelo debe ser:
- Detallado — cada monto relevante debe poder explicarse.
- Trazable — toda transformación económica debe poder rastrearse en
movimientos[]. - Reconciliable — la suma algebraica del ledger debe cerrar en 0 al finalizar el ticket.
- Auditable — el modelo debe soportar auditoría fiscal y contable completa.
- Extensible — nuevos tipos de promoción, pago o impuesto se incorporan sin romper el núcleo.
- Conservador — ante dudas de modelado, se elige la solución más conservadora y explicable.
- Claro de implementar en Java 21 — con tipado fuerte, claridad y modernidad sin sobreingeniería.
No se deben introducir reglas no confirmadas.
2. Agregado raíz: Ticket
El agregado raíz es Ticket. Representa el estado completo de una operación POS y es la unidad principal para cálculo, validación, testing, auditoría y reconciliación.
{
"ticket": {
"datosreferenciales": {},
"cliente": {},
"articulos": [],
"items": [],
"promociones": [],
"pagos": [],
"movimientos": []
}
}
| Array | Tipo | Descripción |
|---|---|---|
datosreferenciales |
Objeto | Metadatos: nroTicket, comercio, sucursal, fechaHora, totales |
cliente |
Objeto | Comprador: identificación, tipo, convenio, impuestos |
articulos[] |
Catálogo interno | Artículos reutilizables. Evita duplicar datos de producto |
items[] |
Registro | Captura comercial original. Un registro por ingreso operativo |
promociones[] |
Registro | Registros maestros de promociones aplicadas |
pagos[] |
Registro | Registros maestros de pagos ingresados |
movimientos[] |
Ledger | Detalle económico distribuido. Fuente de verdad fiscal/contable |
3. Granos del modelo
El modelo trabaja con tres granos funcionales claramente separados. Esta separación es obligatoria y no debe perderse.
3.1 articulos[]
Catálogo de artículos reutilizables dentro del ticket. Si al ingresar un EAN el artículo ya existe en articulos[], se reutiliza su id — no se duplica.
3.2 items[]
Captura comercial original. Cada ingreso de ítem tiene id incremental, referencia un articuloid e informa unidades. Conserva el dato tal como fue ingresado operativamente. No debe usarse para resumir el ledger.
3.3 movimientos[]
Ledger distribuido y detallado. Es el core de explicación económica del ticket. Refleja ventas itemizadas, promociones distribuidas, pagos distribuidos, excedentes y vueltos. No reemplaza a items[], pagos[] ni promociones[] — los complementa.
4. Objetos del dominio
4.1 DatosReferenciales
Contiene al menos: nroTicket, comercio, nroSucursal, nroPv, fechaHora, tickettipo, tipocomprobante, total, saldo, vuelto, nucleoimpositivo.
nucleoimpositivo aquí representa el total del ticket sin considerar pagos.
4.2 Cliente
| Campo | Tipo | Descripción |
|---|---|---|
id |
integer | Identificador |
nombre |
string | Nombre o razón social |
cuit |
string | CUIT / documento |
tipodecliente |
objeto | Tipo de cliente y tipo de comprobante asociado |
convenio |
objeto / null | { id, nombre } — un solo convenio por cliente. null si no tiene. El convenio.id es el dato que filtra conveniosclientes[] en el motor de promociones |
impuestos[] |
array | Percepciones y retenciones aplicables |
4.3 Articulo
| Campo | Tipo | Descripción |
|---|---|---|
ean |
string | Código de barras EAN |
plu |
string | Código PLU |
descripcion |
string | Nombre del artículo |
pesable |
boolean | Si se vende por peso |
preciolista |
number | Precio de lista base. Corresponde a la suma de los montos informados en nucleoimpositivo[] del artículo. |
rubro |
string | Rubro |
depto |
string | Departamento |
marca |
string | Marca |
codigoclasificacion |
integer | Código de clasificación genérico |
proveedor |
string | Proveedor |
esarticuloconoferta |
boolean | Si true, identifica al artículo como artículo con oferta. Si una promoción tiene condiciones.excluyearticulosoferta = true, estos artículos se excluyen del cómputo de la promoción. |
nucleoimpositivo[] |
array | Composición impositiva base del artículo |
4.4 Item
| Campo | Tipo | Descripción |
|---|---|---|
id |
integer | Identificador incremental del ingreso |
articuloid |
integer | Referencia al artículo en articulos[] |
unidades |
number | Cantidad ingresada. Admite decimales para pesables |
4.5 NucleoImpositivo
Estructura central de importes e impuestos. Se usa en artículos, movimientos y datosreferenciales. Expresado como colección de componentes { impuesto: { id }, monto }. No asumir que todo impuesto es porcentual.
4.6 TipoDePago
Catálogo de configuración. Define el comportamiento funcional de cada medio de pago. No confundir con la instancia de Pago.
| Campo | Tipo | Descripción |
|---|---|---|
id |
integer | Identificador |
idmdep |
integer | ID del medio de pago |
ididmdep |
integer | Sub-ID del medio de pago |
cuota |
integer | Número de cuotas. 0 = contado |
descripcion |
string | Nombre descriptivo |
davuelto |
boolean | true: vuelto en el mismo medio. false: ver vueltomediodepago |
vueltomediodepago |
integer / null | ID del medio alternativo para el vuelto. null = operación denegada |
La referencia interna del pago al catálogo se realiza por Pago.mediodepagoid -> TipoDePago.id.
La clave funcional compuesta idmdep + ididmdep + cuota se usa para comparar equivalencia de medio de pago entre tiposdepago[] y las entradas de mediosdepago[] en la definición de promoción.
4.7 Pago
Registro maestro de pago dentro del ticket.
| Campo | Tipo | Descripción |
|---|---|---|
id |
integer | Identificador |
mediodepagoid |
integer | Referencia al TipoDePago.id |
descripcion |
string | Nombre descriptivo |
monto |
number | Positivo = ingreso. Negativo = vuelto entregado al cliente |
Un Pago negativo es válido por diseño — representa un medio de pago utilizado como vuelto. No agregar campos adicionales como montoingresado o montoaplicado.
4.8 Promocion (aplicada)
Ver sección 17 para la definición completa.
Campos mínimos:
id,promocionid,descripcion,tipoPromo,monto- opcionalmente
pagoorigenidcuando la promoción aplicada es de tipo POSTPAGO
4.9 Movimiento
Ver sección 7 para la definición completa.
Campos mínimos:
id,concepto,origenid,movimientoid,nucleoimpositivo
5. Regla de itemización
5.1 Regla principal
Un item puede representar varias unidades ingresadas en una sola acción operativa, pero el ledger en movimientos[] debe representar el detalle distribuido.
5.2 Regla obligatoria
Si un item tiene unidades enteras, deben generarse tantos movimientos VENTA_ITEM como unidades registradas, salvo que exista una regla futura explícita en contrario.
items[] = captura comercial · movimientos[] = impacto económico distribuido. Esta distinción no debe perderse.
5.3 Cantidades decimales
El modelo permite unidades decimales para artículos pesables. La distribución para unidades no enteras debe mantener trazabilidad, explicabilidad y reconciliación.
6. Catálogos y registros del ticket
6.1 Catálogos / configuración
tickettipo, tipocomprobante, tipodecliente, tipodeimpuesto, tipoconcepto, tiposdepago, listapromociones, metodocomputo, tipobeneficio, promocionestado, promocionlistatype, promocionlistanumber, promociontipoelemento
6.2 Registros operativos del ticket
datosreferenciales, cliente, articulos[], items[], promociones[], pagos[], movimientos[]
7. Estructura de Movimiento
| Campo | Tipo | Regla |
|---|---|---|
id |
integer | Identificador incremental |
concepto |
enum | VENTA_ITEM · PROMOCION · PAGO |
origenid |
integer | Referencia al registro maestro origen según el concepto |
movimientoid |
integer / null | Referencia al VENTA_ITEM sobre el que aplica. null para excedentes y vuelto |
nucleoimpositivo[] |
array | Estructura de importes e impuestos del movimiento |
Regla general de movimientoid
VENTA_ITEM→movimientoid = nullPROMOCION→ aplica sobre unVENTA_ITEMPAGOdistribuido → aplica sobre unVENTA_ITEMPAGOexcedente o vuelto →movimientoid = null
8. Reglas de pagos
pagos[]contiene registros maestros.movimientos[]contiene el impacto distribuido.- Los pagos se evalúan contra el saldo real del ticket luego de promociones e impuestos.
- El pago negativo está permitido cuando el medio de pago participa como vuelto.
datosreferenciales.vueltoconserva el resultado global ymovimientos[]explica la distribución. - No sobrecargar
Pagocon campos adicionales. La versión base usa solomonto.
9. Reglas de ledger y convenciones de signos
9.1 Principio central
El ledger debe ser explicable línea por línea. Cada movimiento tiene su propio nucleoimpositivo — no reemplazar por un monto agregado si eso implica perder trazabilidad fiscal.
9.2 Convenciones de signos
| Concepto | Signo | Condición especial |
|---|---|---|
VENTA_ITEM |
positivo | — |
PROMOCION descuento |
negativo | Incluye promociones ITEM, PAGO y POSTPAGO cuando representan descuento |
PROMOCION recargo |
positivo | Incluye promociones ITEM, PAGO y POSTPAGO cuando representan recargo |
PAGO aplicado a ítem |
negativo | movimientoid ≠ null |
PAGO excedente |
negativo | movimientoid = null |
PAGO vuelto |
positivo | movimientoid = null |
9.3 Separación maestro / distribuido
promociones[]contiene registros maestros.movimientos[]contiene el impacto distribuido.promociones[].montono sustituye al detalle distribuido del ledger.CUPONes excepción: solo queda enpromociones[]conmonto = 0, no genera movimiento.- Las promociones sintéticas de
MODO_POSTPAGOtambién se registran enpromociones[]; en ese casopromocionid = 0ypagoorigenidreferencia el pago que disparó el ajuste.
10. Regla de excedente y vuelto
Cuando un medio de pago ingresa un monto mayor al necesario para cancelar el ticket:
- La parte aplicada → movimientos
PAGOnegativos contra ítems. - El excedente → movimiento
PAGOnegativo conmovimientoid = null,origenid= id del pago ingresado. - El vuelto → movimiento
PAGOpositivo conmovimientoid = null,origenid= id del pago de vuelto.
La resolución del medio de vuelto depende del TipoDePago:
| Caso | davuelto |
vueltomediodepago |
Resultado |
|---|---|---|---|
| A | true |
— | Vuelto en el mismo medio. Aceptado. |
| B | false |
null |
No se permite vuelto. Operación DENEGADA. |
| C | false |
id_medio |
Vuelto en el medio alternativo indicado. Aceptado. |
11. Reglas de reconciliación
11.1 Regla fuerte
La suma algebraica de todos los nucleoimpositivo.monto en movimientos[] debe ser exactamente 0 al cierre final del ticket.
11.2 Coherencia esperada
El modelo debe poder verificar:
- total de ítems
- total de promociones
- total de impuestos
- total pagado
- saldo y vuelto
- suma de movimientos distribuidos
11.3 Ejemplo conceptual
VENTA_ITEM: +1310 +1310 +1310 = +3930.00
PROMOCION: -655 -655 = -1310.00
PAGO items: -655 -655 -1310 = -2620.00
PAGO exc: -380 = -380.00
PAGO vuelto: +380 = +380.00
─────────────────────────────────────────
TOTAL = 0.00 ✓
11.4 Lectura del ledger completo
mov1: VENTA_ITEM item1/u1 +1310.00
mov2: VENTA_ITEM item1/u2 +1310.00
mov3: VENTA_ITEM item2/u1 +1310.00
mov4: PROMOCION → mov1 -655.00
mov5: PROMOCION → mov2 -655.00
mov6: PAGO → mov1 -655.00
mov7: PAGO → mov2 -655.00
mov8: PAGO → mov3 -1310.00
mov9: PAGO exc mid=null -380.00
mov10: PAGO vuelto mid=null +380.00
12. Orden base de cálculo
| Paso | Acción |
|---|---|
| 1 | Identificar cliente (incluye convenio) |
| 2 | Ingresar ítems — deduplicar articulos[], crear items[] |
| 3 | Resolver referencias a artículos |
| 4 | Determinar bases iniciales de cálculo para todos los ítems |
| 5 | MODO ITEM — aplicar promociones en orden: MAYORISTA → VENTAXBULTO → GRUPOMAYORISTA → COMBO → CANTIDAD |
| 6 | Recalcular bases afectadas según el último precio vigente del ítem en cada paso |
| 7 | Calcular impuestos y núcleo del ticket |
| 8 | Determinar total (datosreferenciales.total) |
| 9 | MODO PAGO CONSULTA — promos disponibles por medio de pago |
| 10 | MODO PAGO APLICAR — registrar en pagos[] y movimientos[] |
| 11 | Calcular excedente y vuelto |
| 12 | Distribuir pagos en ledger |
| 13 | Calcular saldo final |
| 14 | Validar reconciliación — Σ movimientos[] = 0 |
PARTE II — MOTOR DE PROMOCIONES
13. Modelo de promociones: tres capas
Las tres capas son obligatorias y no deben mezclarse:
| Capa | Estructura | Tipo | Descripción |
|---|---|---|---|
| Definición | listapromociones |
Catálogo / configuración | Regla configurable de negocio. No es registro operativo del ticket. |
| Aplicada | promociones[] |
Registro operativo del ticket | Resultado maestro de la aplicación en una operación concreta. |
| Ledger | movimientos[] |
Ledger distribuido | Impacto económico distribuido. Fuente de verdad fiscal. |
14. Modos de cómputo e inferencia de alcance
14.1 Cuatro modos operativos
| Modo | Descripción |
|---|---|
ITEM |
Computa promociones sobre ítems. |
PAGO (CONSULTA) |
Devuelve promociones disponibles para un medio de pago. No modifica el ticket. |
PAGO (APLICAR) |
Aplica la promoción de pago sobre el monto ingresado. |
POSTPAGO |
Aplica un ajuste posterior al pago ya autorizado, distribuido proporcionalmente sobre los ítems según su saldo vigente. |
14.2 Inferencia de modo — no hay campo explícito
La inferencia aplica solo a promociones configuradas en listapromociones para los modos ITEM y PAGO:
mediosdepago[]vacío → promoción de MODO ITEMmediosdepago[]con entradas → promoción de MODO PAGO, se computa solo para esos medios y solo sobre los ítems delistasitems[]listasitems[]vacío → promoción inválida (no debe procesarse)
MODO_POSTPAGO no se infiere desde listapromociones: se invoca explícitamente cuando un sistema externo informa que, luego de autorizar un pago, se obtuvo un recargo o descuento adicional asociado a ese pago.
14.3 Orden de procesamiento por método
| Orden | metodocomputo |
Modos habilitados desde listapromociones |
|---|---|---|
| 1 | MAYORISTA |
ITEM solamente |
| 2 | VENTAXBULTO |
ITEM solamente |
| 3 | GRUPOMAYORISTA |
ITEM solamente |
| 4 | COMBO |
ITEM y PAGO |
| 5 | CANTIDAD |
ITEM y PAGO |
14.4 Exclusión por promociones de precio previas
El estado de resolución de un ítem determina si sigue siendo evaluado en los métodos siguientes:
- Si el ítem ya fue alcanzado por una promoción del método
MAYORISTA→ no se evalúa enVENTAXBULTOniGRUPOMAYORISTA. - Si el ítem ya fue alcanzado por una promoción del método
VENTAXBULTO(sin haber sido alcanzado porMAYORISTA) → no se evalúa enGRUPOMAYORISTA. - Tras
MAYORISTA → VENTAXBULTO → GRUPOMAYORISTA, el ítem pasa aCOMBOyCANTIDADcon su último precio vigente.
14.5 Lógica acumulativa / no acumulativa
Aplica exclusivamente en los métodos COMBO y CANTIDAD.
Regla del motor:
- Primero se evalúan y computan todas las promociones con
condiciones.acumulativa = true. - Las no acumulativas solo se evalúan si ninguna acumulativa se cumplió para ese ítem.
Un ítem puede acumular múltiples promociones acumulativas. Las acumulativas y no acumulativas son mutuamente excluyentes en su aplicación efectiva.
14.6 Base de cálculo del precio
- Promociones ITEM:
- si
condiciones.usapreciolistaarticulo = true→ base =articulo.preciolista, definido como la suma de los montos informados enarticulo.nucleoimpositivo[] - si
condiciones.usapreciolistaarticulo = false→ base = último precio vigente del ítem en ese momento del cómputo, luego de promociones previas ya computadas
- si
- Promociones de PAGO: base = último precio resultante del ítem tras todas las promociones ITEM ya computadas.
15. Catálogos auxiliares de promociones
Todos los catálogos se referencian como objetos { "id": "VALOR" }.
15.1 metodocomputo
| id | Modos | Descripción |
|---|---|---|
MAYORISTA |
ITEM | Precio mayorista por volumen o condición de cliente. |
VENTAXBULTO |
ITEM | Precio especial por venta de bulto completo. |
GRUPOMAYORISTA |
ITEM | Precio mayorista por grupo de artículos. |
COMBO |
ITEM + PAGO | Beneficio por combinación de ítems de distintas listas. |
CANTIDAD |
ITEM + PAGO | Beneficio por cantidad de unidades de uno o más ítems. |
15.2 tipobeneficio
| id | Descripción |
|---|---|
PORCENTAJE_DESCUENTO |
Descuento porcentual. valorbeneficio positivo (ej: 10 = 10%). |
PORCENTAJE_RECARGO |
Recargo porcentual. valorbeneficio positivo. |
MONTO_DESCUENTO |
Descuento de monto fijo. |
MONTO_RECARGO |
Recargo de monto fijo. |
NUEVOPRECIOITEM |
Reemplaza el precio del ítem. valorbeneficio = precio final. |
NO_PERMITIR_VENTA_ITEM |
Bloquea la venta del ítem. Sin monto. |
EAN_COMBO |
El beneficio es un ítem adicional identificado por EAN. |
CUPON |
Genera cupones de beneficio para otras compras o sorteos. Sin impacto en ledger. Solo queda en promociones[] con monto = 0. Formato configurable diferido. |
15.3 promocionestado
| id | Descripción |
|---|---|
POSIBLE |
Evaluada, podría aplicar, aún no aplicada. |
APLICADA |
Aplicada efectivamente. |
NOAPLICA |
Evaluada, no corresponde aplicar. |
ANULADA |
Aplicada pero luego anulada. |
15.4 promocionlistatype
| id | |
|---|---|
INCLUSION |
El elemento está alcanzado por la promoción. |
EXCLUSION |
El elemento está excluido explícitamente. |
15.5 promocionlistanumber
| id | Descripción |
|---|---|
LISTA1 |
Primera lista de ítems (o primer grupo del combo). |
LISTA2 |
Segunda lista de ítems (segundo grupo del combo). |
LISTA3 |
Precio de venta unitario del combo. tipoelemento = MONTO (valor directo en monto) o EAN (indirección a artículo en catálogo). |
15.6 promociontipoelemento
| id | Descripción |
|---|---|
EAN |
Código de barras EAN. |
PLU |
Código PLU. |
DEPARTAMENTO |
Departamento. |
RUBRO |
Rubro. |
SECTOR |
Sector. |
FAMILIA |
Familia de productos. |
CODIGOCLASIFICACION |
Código de clasificación genérico. |
PROVEEDOR |
Proveedor. |
MARCA |
Marca. |
MONTO |
Monto de venta. Usado en LISTA3 para precio unitario del combo. |
TICKET |
La condición aplica al conjunto completo de ítems del ticket. |
Eliminados:
SUCURSAL(ahorasucursales[]en la promoción),CANTIDAD_MAX_PROMOS(ahoracondiciones.maximacantidadpromosxticket),MEDIODEPAGO(ahoramediosdepago[]en la promoción).
16. Definición de promoción: listapromociones
16.1 Estructura completa
{
"listapromociones": [
{
"id": 1,
"descripcion": "PROMO_2X1_ARROZ",
"metodocomputo": { "id": "CANTIDAD" },
"tipobeneficio": { "id": "PORCENTAJE_DESCUENTO" },
"valorbeneficio": 50.0,
"codigodescarga": null,
"conveniosclientes": [],
"condiciones": {
"acumulativa": false,
"maximacantidadpromosxticket": 1,
"montominimo": 0.0,
"excluyearticulosoferta": false,
"usapreciolistaarticulo": true
},
"vigencia": {
"fechadesde": "20260301",
"fechahasta": "20260331",
"diassemana": ["MIERCOLES"],
"horadesde": "10",
"horahasta": "11"
},
"sucursales": [],
"mediosdepago": [],
"listasitems": [
{
"listaindex": 1,
"tipodeLista": { "id": "INCLUSION" },
"nrolista": { "id": "LISTA1" },
"tipoelemento": { "id": "EAN" },
"valores": ["7791234567890"],
"cantidad": 2.0,
"monto": null
}
]
}
]
}
16.2 Campos del objeto Promoción
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
id |
integer | Sí | Identificador único. |
descripcion |
string | Sí | Nombre descriptivo. |
metodocomputo |
objeto {id} |
Sí | Método de cómputo. |
tipobeneficio |
objeto {id} |
Sí | Tipo de beneficio. |
valorbeneficio |
number / null | Sí* | Valor del beneficio. Null para NO_PERMITIR_VENTA_ITEM, EAN_COMBO, CUPON. |
codigodescarga |
string / null | No | PLU para grabar la promo como producto en el detalle de ventas. |
conveniosclientes[] |
array | Sí | Lista de { id } de convenios. Vacío = aplica a todos. |
condiciones |
objeto | Sí | Ver 16.3. |
vigencia |
objeto | Sí | Ver 16.4. |
sucursales[] |
array | Sí | Sucursales habilitadas. Vacío = todas. Sin exclusión. |
mediosdepago[] |
array | Sí | Vacío = MODO ITEM. Con entradas = MODO PAGO. |
listasitems[] |
array | Sí | Debe tener al menos una entrada. |
16.3 condiciones
| Campo | Tipo | Descripción |
|---|---|---|
acumulativa |
boolean | true = acumulativa. Solo aplica a COMBO y CANTIDAD. |
maximacantidadpromosxticket |
integer | 0 = sin límite. Otro valor = máximo de aplicaciones por ticket. |
montominimo |
number | Monto mínimo de ítems incluidos para habilitar el beneficio. 0 = sin mínimo. |
excluyearticulosoferta |
boolean | Si true, los artículos/items cuyo artículo tenga esarticuloconoferta = true se excluyen del cómputo de la promoción. |
usapreciolistaarticulo |
boolean | Si true, la promoción se calcula usando como precio del ítem el articulo.preciolista original. Si false, se calcula usando el último precio vigente del ítem luego de promociones previas ya computadas. |
16.4 vigencia
| Campo | Formato | Descripción |
|---|---|---|
fechadesde |
AAAAMMDD |
Fecha de inicio. |
fechahasta |
AAAAMMDD |
Fecha de fin (inclusive). |
diassemana[] |
array string | LUNES MARTES MIERCOLES JUEVES VIERNES SABADO DOMINGO |
horadesde |
HH |
Hora de inicio en cada día habilitado. |
horahasta |
HH |
Hora de fin. |
16.5 sucursales[]
"sucursales": [
{ "nrosucursal": 1, "descripcion": "CASA CENTRAL" }
]
Lista simple de sucursales donde la promoción es válida. Sin exclusión — vacío significa válida en todas.
16.6 mediosdepago[]
"mediosdepago": [
{ "idmdep": 1, "ididmdep": 10, "cuota": 0, "descripcion": "EFECTIVO" }
]
| Campo | Tipo | Descripción |
|---|---|---|
idmdep |
integer | ID del medio de pago. |
ididmdep |
integer | Sub-ID del medio de pago. |
cuota |
integer | Número de cuotas. 0 = contado. |
descripcion |
string | Nombre descriptivo. |
El match con tiposdepago se hace por idmdep + ididmdep + cuota ↔ idmdep + ididmdep + cuota.
16.7 listasitems[]
| Campo | Tipo | Descripción |
|---|---|---|
listaindex |
integer | Orden de la entrada. |
tipodeLista |
objeto {id} |
INCLUSION o EXCLUSION. |
nrolista |
objeto {id} |
LISTA1, LISTA2 o LISTA3. |
tipoelemento |
objeto {id} |
Tipo del elemento evaluado. |
valores[] |
array string | Lista de valores concretos a comparar. Para LISTA1 y LISTA2 puede contener uno o más valores candidatos. |
cantidad |
number | Cantidad necesaria para cumplir la condición. |
monto |
number / null | Precio nuevo del ítem. Usado en mayorista/grupo/vtaxbulto y en LISTA3 con tipoelemento = MONTO. |
Semántica de valores[] en listasitems[]:
- Para
LISTA1yLISTA2,valores[]contiene uno o más valores candidatos válidos para satisfacer esa lista. cantidadexpresa la cantidad requerida para cumplir esa lista.tipoelementoindica cómo interpretar todos los elementos devalores[](por ejemploEAN,PLU,RUBRO, etc.).
Semántica de LISTA3 en combos: define el precio de venta unitario del combo.
tipoelemento = MONTO→ precio directo en campomonto, convalores[]vacío.tipoelemento = EAN→valores[]debe contener exactamente un EAN, y el precio se obtiene del artículo con ese EAN en el catálogo (indirección que permite actualizar el precio del combo sin tocar la config de la promo).
17. Promoción aplicada en el ticket: promociones[]
17.1 Estructura
{
"promociones": [
{
"id": 1,
"promocionid": 1,
"pagoorigenid": null,
"descripcion": "PROMO_2X1_ARROZ",
"tipoPromo": "ITEM",
"promocionestado": { "id": "APLICADA" },
"monto": -1310.0,
"elementos": [
{ "movimientoid": 1, "articuloid": 1, "unidadesimpactadas": 1.0, "monto": -655.0 },
{ "movimientoid": 2, "articuloid": 1, "unidadesimpactadas": 1.0, "monto": -655.0 }
]
}
]
}
17.2 Campos
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
id |
integer | Sí | Identificador del registro aplicado. |
promocionid |
integer | Sí | Referencia a listapromociones. Valor 0 reservado para promociones sintéticas generadas por el sistema, como POSTPAGO. |
pagoorigenid |
integer / null | No | Referencia al Pago.id que disparó la promoción cuando se trata de un ajuste POSTPAGO. null en los demás casos. |
descripcion |
string | Sí | Nombre heredado de la definición o descripción sintética generada por el sistema. |
tipoPromo |
string | Sí | ITEM o PAGO. Naming conservado por consistencia con JSON del modelo. |
monto |
number | Sí | Monto total del beneficio o recargo. Negativo = descuento. Positivo = recargo. 0 para CUPON. |
promocionestado |
objeto {id} |
No | Estado de la promoción aplicada. |
elementos[] |
array | No | Trazabilidad: movimientoid, articuloid, unidadesimpactadas, monto. |
Regla: suma de elementos[].monto = promociones[].monto. El detalle fiscal definitivo sigue en movimientos[].
Regla CUPON: solo queda en promociones[] con monto = 0. No genera movimiento en movimientos[]. No afecta el saldo ni la reconciliación.
Regla POSTPAGO: se registra en promociones[] con tipoPromo = "PAGO", promocionid = 0, pagoorigenid informado y distribución proporcional en elementos[].
18. Flujo del motor MODO ITEM
1. Recibir ticket con items[]
2. Tomar como precio vigente inicial de cada ítem el `articulo.preciolista`, definido como la suma de los montos informados en `articulo.nucleoimpositivo[]`
3. Filtrar listapromociones con mediosdepago[] vacío y:
- vigencia activa (fecha, hora, día de semana)
- sucursales (si no vacío, nroSucursal del ticket debe estar incluido)
- conveniosclientes (si no vacío, cliente.convenio.id debe estar incluido)
- condiciones.excluyearticulosoferta
- si `condiciones.excluyearticulosoferta = true`, excluir del cómputo los ítems cuyo artículo tenga `esarticuloconoferta = true`
- resolver la base de cálculo de cada promoción ITEM según `condiciones.usapreciolistaarticulo`
4. Procesar por metodocomputo en orden:
── MAYORISTA ──────────────────────────────────────────────────────────────
Para cada ítem, si cumple condiciones de alguna promo MAYORISTA:
→ Aplicar el beneficio o precio correspondiente
→ Registrar PROMOCION en movimientos[]
→ Registrar en promociones[]
→ Ese ítem NO se evalúa en VENTAXBULTO ni GRUPOMAYORISTA
── VENTAXBULTO ────────────────────────────────────────────────────────────
Para cada ítem que no haya sido alcanzado por MAYORISTA, si cumple condiciones:
→ Aplicar el beneficio o precio correspondiente
→ Registrar PROMOCION en movimientos[]
→ Ese ítem NO se evalúa en GRUPOMAYORISTA
── GRUPOMAYORISTA ─────────────────────────────────────────────────────────
Para cada ítem que no haya sido alcanzado por MAYORISTA ni VENTAXBULTO, si cumple condiciones:
→ Aplicar el beneficio o precio correspondiente
→ Registrar PROMOCION en movimientos[]
── COMBO y CANTIDAD ───────────────────────────────────────────────────────
Para todos los ítems con su último precio vigente:
Paso A — Evaluar y computar todas las promos ACUMULATIVAS:
Para cada promo acumulativa que se cumpla:
· Calcular beneficio usando la base resuelta por `condiciones.usapreciolistaarticulo`
· Registrar PROMOCION en movimientos[]
· Registrar en promociones[]
Paso B — Evaluar promos NO ACUMULATIVAS solo si ninguna acumulativa se cumplió:
Para cada promo no acumulativa que se cumpla:
· Calcular beneficio usando la base resuelta por `condiciones.usapreciolistaarticulo`
· Registrar PROMOCION en movimientos[]
· Registrar en promociones[]
5. Retornar ticket actualizado (sin promos de pago)
19. Flujo específico — método COMBO (estrategia de resolución)
19.1 Alcance y relación con el legacy
El método COMBO conserva una semántica funcional observada en el motor legacy: la resolución no se hace promoción por promoción de forma completamente aislada, sino mediante una estrategia global e iterativa que compite entre múltiples promociones posibles sobre un mismo conjunto de ítems.
Sin embargo, el modelo actual no replica la estructura legacy de hasta 5 listas ni su storage transaccional intermedio. La semántica vigente del modelo es:
LISTA1yLISTA2= listas funcionales de ítems candidatosLISTA3= precio del combo, directo (MONTO) o indirecto (EAN)- la trazabilidad definitiva del efecto económico vive en
promociones[]ymovimientos[], no en acumulados auxiliares
19.2 Unidad de combo
Una promoción COMBO se computa en términos de una unidad de combo.
Una unidad de combo está formada por:
- la cantidad requerida de
LISTA1 - la cantidad requerida de
LISTA2
LISTA3 no participa como lista de ítems. Solo define el precio de venta unitario del combo.
Ejemplo conceptual:
LISTA1.cantidad = 2LISTA2.cantidad = 1
Entonces una unidad de combo requiere exactamente 3 ítems: 2 de LISTA1 y 1 de LISTA2.
19.3 Selección de ítems dentro de una unidad
Para formar una unidad de combo, el motor selecciona, dentro de cada lista funcional, los ítems disponibles de mayor precio vigente.
Esto implica explícitamente una estrategia:
- greedy
- orientada a beneficio inmediato
- no exhaustiva
Es decir, el motor no está obligado a evaluar todas las combinaciones posibles entre candidatos de LISTA1 y LISTA2. La semántica aprobada es priorizar la combinación unitaria más conveniente según los ítems de mayor precio vigente disponibles en cada lista.
19.4 Definición de precio vigente
El precio vigente de cada ítem para el cálculo de COMBO se determina así:
- si
condiciones.usapreciolistaarticulo = true→ base =articulo.preciolista - si
condiciones.usapreciolistaarticulo = false→ base = último precio vigente del ítem luego de promociones previas ya aplicadas
Esto debe respetarse tanto al evaluar si una unidad de combo resulta conveniente como al distribuir el beneficio final.
19.5 Disponibilidad y consumo de ítems
Un ítem unitario es elegible para una unidad de combo si:
- todavía no fue consumido por una aplicación previa incompatible
- no está reservado temporalmente en la evaluación actual
Regla fuerte del modelo actual:
cada unidad de ítem solo puede participar en una única unidad efectiva de combo dentro de la misma resolución
Esto reemplaza la semántica legacy basada en contadores agregados por EAN. En el modelo actual la disponibilidad se resuelve a nivel de unidad trazable del ledger.
19.6 Estrategia global de resolución
Para promociones COMBO de MODO ITEM, la resolución se hace con un ciclo global:
Mientras existan promociones COMBO aplicables:
1. Evaluar todas las promociones COMBO candidatas
2. Intentar formar UNA unidad de cada promoción
3. Calcular el beneficio económico de esa unidad
4. Seleccionar la mejor unidad según criterio económico
5. Consumir los ítems involucrados
6. Registrar la promoción aplicada en promociones[]
7. Registrar el efecto distribuido en movimientos[]
8. Recalcular el estado vigente y repetir
Esto significa que COMBO se comporta como una competencia iterativa entre promociones sobre un pool compartido de ítems disponibles.
19.7 Criterio de selección
El criterio por defecto es seleccionar la unidad de combo con mayor beneficio económico inmediato.
Convención de interpretación:
- descuento más grande = mejor resultado
- recargo más grande = peor resultado, salvo regla de negocio explícita en contrario
Dado que el modelo admite promociones tanto negativas como positivas, la implementación debe comparar los candidatos con una convención de ordenamiento explícita y consistente.
19.8 Optimalidad global
La resolución de COMBO:
- no garantiza optimalidad global
- depende del estado vigente del ticket
- depende del orden de consumo de ítems
Esto es intencional. El comportamiento heredado del legacy es esencialmente heurístico. El modelo actual adopta una versión controlada, trazable y determinística de esa misma idea, privilegiando claridad de implementación y explicabilidad de negocio por sobre búsqueda exhaustiva.
19.9 Interacción con acumulatividad
La lógica de acumulatividad definida para COMBO y CANTIDAD sigue vigente también en la resolución global de combos:
- primero se procesan promociones
acumulativa = true - luego las
acumulativa = false, solo si ninguna acumulativa se cumplió para el universo de ítems considerado
Dentro de cada grupo se ejecuta la estrategia global iterativa de selección de unidades.
19.10 Registro en promociones[] y movimientos[]
Cada unidad de combo aplicada debe generar:
- un registro maestro en
promociones[] - movimientos
PROMOCIONdistribuidos enmovimientos[]sobre losVENTA_ITEMalcanzados
La suma de los movimientos distribuidos debe coincidir con promociones[].monto.
La implementación no debe registrar solo un acumulado agregado por combo. La explicación económica y fiscal definitiva debe vivir en el ledger.
19.11 Signo del beneficio
Aunque el legacy estuvo orientado principalmente a combos con descuento, el modelo actual no debe asumir esa restricción.
Por lo tanto:
- descuento → monto negativo
- recargo → monto positivo
La semántica de signos del ledger definida en la Parte I es obligatoria también para COMBO.
19.12 Consideraciones de implementación
La implementación de COMBO debe ser:
- determinística
- greedy
- trazable
- compatible con itemización unitaria
- compatible con reconciliación final del ticket
Cualquier cambio futuro en:
- orden de evaluación
- criterio de selección
- forma de consumir ítems
- forma de distribuir el beneficio
puede modificar resultados de negocio y por lo tanto debe considerarse un cambio funcional relevante.
20. Flujo del motor MODO PAGO y MODO_POSTPAGO
20.1 Sub-etapa CONSULTA
1. Recibir los datos identificadores del medio de pago que se quiere consultar (idmdep + ididmdep + cuota)
2. Filtrar listapromociones donde mediosdepago[] contiene ese medio
3. Para cada promo encontrada verificar: vigencia, sucursal, convenio cliente,
condiciones, y que los ítems del ticket están en listasitems[]
4. Calcular beneficio sobre precio resultante post-promos ITEM de cada ítem
5. Retornar: promocionesdisponibles[], saldoneto
6. No modificar el ticket
20.2 Sub-etapa APLICAR — escenarios de monto
El saldo de referencia es el saldo pendiente neto (ya considerando la promo del medio de pago):
| Escenario | Condición | Comportamiento |
|---|---|---|
| Pago parcial | monto < saldo neto |
Promo aplicada proporcionalmente. Queda saldo pendiente. |
| Pago exacto | monto = saldo neto |
Promo aplicada completa. Ticket saldado. |
| Pago con excedente | monto > saldo neto |
Consume hasta el saldo neto. Excedente resuelto según tiposdepago (Casos A/B/C). |
Ver sección 10 para la lógica A/B/C de vuelto.
20.3 MODO_POSTPAGO
MODO_POSTPAGO se utiliza cuando, luego de aplicar un pago y obtener autorización en un sistema externo al modelo, dicho sistema informa un recargo o descuento adicional asociado a ese pago.
1. Recibir `pagoorigenid`, el medio de pago utilizado y el monto de ajuste `rd`
2. Determinar el saldo vigente de cada ítem inmediatamente antes de aplicar el pago que disparó el POSTPAGO
3. Para calcular ese saldo vigente por ítem: considerar ventas, promociones previas y pagos previos ya aplicados, pero NO considerar todavía el pago que disparó el POSTPAGO
4. Distribuir proporcionalmente `rd` sobre todos los ítems con saldo vigente positivo
5. Registrar una promoción sintética en `promociones[]` con `promocionid = 0`, `pagoorigenid` informado, `tipoPromo = "PAGO"` y `promocionestado = APLICADA`
6. Registrar movimientos `PROMOCION` distribuidos sobre cada `VENTA_ITEM` alcanzado
7. Si `rd < 0`, el POSTPAGO es descuento; si `rd > 0`, el POSTPAGO es recargo
20.4 Estructura resultante de POSTPAGO
{
"promociones": [
{
"id": 2,
"promocionid": 0,
"pagoorigenid": 1,
"descripcion": "PROMO POSTPAGO CHEQUE 0 CUOTAS",
"tipoPromo": "PAGO",
"promocionestado": { "id": "APLICADA" },
"monto": -20.0,
"elementos": [
{ "movimientoid": 1, "articuloid": 1, "unidadesimpactadas": 1.0, "monto": -10.0 },
{ "movimientoid": 2, "articuloid": 1, "unidadesimpactadas": 1.0, "monto": -10.0 }
]
}
]
}
PARTE III — IMPLEMENTACIÓN
21. Convenciones Java 21
Implementación objetivo: Java 21, priorizando claridad del dominio, tipado fuerte y código mantenible.
- Clases para agregados y entidades mutables
recordpermitido y recomendado para value objects inmutables, referencias simples de catálogo y DTOs internosenumJava para catálogos cerrados- Constructores, factorías y métodos explícitos cuando mejoren la legibilidad
switchexpressions, pattern matching y otras features de Java 21 permitidas cuando simplifiquen el código- Lombok no es necesario; usarlo solo si aporta un beneficio concreto y no degrada la claridad del modelo
- Sin frameworks de mapping obligatorios
Paquetes sugeridos
| Paquete | Contenido |
|---|---|
com.tipre.ruleengine.model |
Ticket, Item, Articulo, Movimiento, Pago, Promocion, NucleoImpositivo |
com.tipre.ruleengine.model.enums |
TipoConcepto, MetodoComputo, TipoBeneficio, PromoEstado |
com.tipre.ruleengine.model.catalog |
ListaPromociones, DefinicionPromocion, TipoDePago, Vigencia, Condiciones |
com.tipre.ruleengine.logic |
MotorPromociones |
com.tipre.ruleengine.logic.item |
ModoItemProcessor, MayoristaEvaluator, CantidadEvaluator, ComboEvaluator |
com.tipre.ruleengine.logic.pago |
ModoPagoConsulta, ModoPagoAplicar, VueltoResolver |
com.tipre.ruleengine.validation |
ReconciliacionValidator, LedgerConsistencyChecker |
com.tipre.ruleengine.example |
TicketEjemplo, TicketConPromocion2x1 |
22. Convenciones de naming
21.1 Nombres canónicos vigentes
articuloid · mediodepagoid · promocionid · origenid · movimientoid · nucleoimpositivo · tipoPromo · tipodecliente · tipocomprobante · tickettipo · convenio · esarticuloconoferta · excluyearticulosoferta · metodocomputo · tipobeneficio · valorbeneficio · listasitems · valores · condiciones · sucursales · mediosdepago · conveniosclientes · codigodescarga
21.2 Nombres obsoletos — no reutilizar
| Nombre obsoleto | Reemplazado por |
|---|---|
promocionalcance |
Inferido por mediosdepago[] |
promocionmetodo |
metodocomputo |
promocionbeneficio |
tipobeneficio |
valor (campo beneficio) |
valorbeneficio |
lista[] en promociones |
listasitems[] |
valor dentro de listasitems[] |
valores[] |
valordeelemento |
valores[] dentro de listasitems[] |
CANTIDAD_MAX_PROMOS (tipoelemento) |
condiciones.maximacantidadpromosxticket |
SUCURSAL (tipoelemento) |
sucursales[] en la promoción |
MEDIODEPAGO (tipoelemento) |
mediosdepago[] en la promoción |
23. Relación con los JSON
Los archivos JSON (jsonmodel) son ejemplos estructurales del modelo, sirven como referencia de generación de código y deben respetar este documento.
Jerarquía ante contradicción:
- este
model.md jsonmodel- documentos históricos anteriores
24. Criterios de revisión
Cada revisión futura debe validar:
| Área | Verificación |
|---|---|
| Consistencia de dominio | Objetos separados, responsabilidades claras, sin pérdida de granularidad |
| Consistencia de cálculo | Orden lógico correcto y bases de precio vigentes recalculadas correctamente durante el cómputo ITEM |
| Consistencia de ledger | Separación maestro/distribuido y movimientos trazables |
| Auditabilidad | Todo monto explicable, toda transformación rastreable |
| Reconciliación | Σ movimientos[] = 0 al cierre |
25. Resumen ejecutivo
Ticketes el agregado raíz.articulos[]evita duplicación de datos de producto.items[]es la captura comercial original.movimientos[]es el ledger distribuido — fuente de verdad fiscal.- La itemización es obligatoria a nivel ledger: una unidad = un
VENTA_ITEM. promociones[]ypagos[]son registros maestros. Su efecto económico vive enmovimientos[].Pago.montopuede ser negativo cuando el medio actúa como vuelto.nucleoimpositivoes la base común para importes e impuestos.articulo.preciolistacorresponde a la suma de los montos informados enarticulo.nucleoimpositivo[].movimientos[]no conserva niveles de precio. El efecto económico de promociones y pagos se explica exclusivamente por sus montos distribuidos.cliente.convenioes un único{ id, nombre }. Suidfiltraconveniosclientes[].articulo.esarticuloconofertaidentifica artículos con oferta. Si una promoción tienecondiciones.excluyearticulosoferta = true, esos artículos se excluyen del cómputo.tiposdepagotiene clave compuesta funcionalidmdep + ididmdep + cuota.- El modo de la promoción se infiere de
mediosdepago[]para ITEM/PAGO;POSTPAGOse invoca explícitamente por un evento externo. - Los métodos se procesan en orden:
MAYORISTA → VENTAXBULTO → GRUPOMAYORISTA → COMBO → CANTIDAD. - Acumulativa/no acumulativa aplica solo en
COMBOyCANTIDAD. condiciones.usapreciolistaarticulodefine la base de cálculo de promociones ITEM:trueusaarticulo.preciolista;falseusa el último precio vigente tras promociones previas.CUPONsolo queda enpromociones[]conmonto = 0. No genera movimiento.POSTPAGOgenera una promoción sintética conpromocionid = 0ypagoorigenidinformado.- La excedente/vuelto se resuelve por los Casos A/B/C de
tiposdepago. - El ticket debe cerrar: Σ
movimientos[]= 0.
FIN — model_v2.md versión 202604161820
ANEXO A — Semántica legacy observada del motor COMBO
Este anexo documenta, sin convertirlo automáticamente en regla obligatoria del modelo futuro, el comportamiento observado en el código legacy del motor de combos. Su finalidad es preservar conocimiento funcional y evitar perder decisiones implícitas del sistema histórico.
A.1 Entrada operativa legacy
El motor legacy no parte directamente del agregado moderno Ticket, sino de una estructura operativa/intermedia donde ya existen candidatos item/promo precargados. Sobre esa estructura se cargan:
- candidatos por promoción y por lista funcional
- cantidades vendidas
- EAN
- precio unitario
- tax
- impuestos internos
- descripciones y costos
Además, el mismo storage intermedio se usa para resetear acumuladores temporales y luego persistir resultados del cómputo.
A.2 Definición legacy de combo
La definición legacy permitía hasta 5 listas funcionales de productos. El modelo actual reduce esta complejidad a:
LISTA1LISTA2LISTA3como precio del combo
La reducción de alcance es deliberada y debe considerarse una decisión de simplificación del dominio actual.
A.3 Selección interna de una unidad de combo
La lógica observada en legacy para formar una unidad de combo es:
- determinar cuántos elementos requiere cada lista
- buscar candidatos disponibles por lista
- seleccionar primero los de mayor precio
- reservarlos temporalmente
- si se completa la cantidad requerida, calcular el beneficio
- si no se completa, descartar la unidad
Este patrón confirma una estrategia greedy por lista, orientada a maximizar el beneficio económico inmediato de una unidad de combo.
A.4 Competencia global entre promociones
En combos ITEM, el legacy no evaluaba cada promoción en forma completamente aislada. El comportamiento observado es:
- calcular una unidad potencial para múltiples promociones candidatas
- comparar sus beneficios
- consolidar la unidad ganadora
- consumir ítems involucrados
- recalcular el problema sobre el nuevo estado
- repetir hasta agotar combinaciones válidas o máximos por promoción
Esto es la base conceptual de la estrategia global e iterativa adoptada en el modelo actual.
A.5 Bloqueo y no reutilización
El legacy evitaba reutilizaciones mediante contadores operativos del tipo:
- vendidos
- ya promocionados
- usados por competencia
- en evaluación temporal
El modelo actual traduce esa necesidad a una regla más fuerte y clara:
una unidad trazable del ledger solo puede ser consumida por una unidad efectiva de combo incompatible dentro de la misma resolución.
A.6 Diferencia ITEM vs medio de pago en legacy
El comportamiento observado era distinto según el alcance:
- combos ITEM: competencia global entre promociones
- combos por medio de pago: evaluación por promoción, en ciclos propios, hasta agotar aplicabilidad
El modelo actual preserva la separación conceptual ITEM/PAGO, aunque la implementación moderna vive sobre promociones[] y movimientos[].
A.7 Heurística y no optimalidad
El legacy probaba más de una variante heurística de recorrido antes de consolidar el resultado final. Eso indica que el algoritmo histórico:
- no garantizaba optimalidad global
- dependía del orden y del criterio local
- aceptaba una heurística como compromiso práctico
El modelo actual conserva explícitamente esta verdad de negocio:
la resolución de combos no debe asumirse como óptimo global, sino como una estrategia determinística, explicable y consistente.
A.8 Persistencia económica e impositiva en legacy
El sistema legacy acumulaba resultado económico e impositivo por promoción en estructuras operativas auxiliares. El modelo actual traslada esa responsabilidad a:
promociones[]como registro maestromovimientos[]como fuente de verdad económica/fiscal
La intención no es negar lo que hacía el legacy, sino reemplazarlo por una representación más auditable.
ANEXO B — Puente de implementación Java 21
Este anexo fija criterios de traducción del modelo de dominio a una implementación Java 21 compatible con el agregado Ticket, con las reglas de ledger y con la separación entre definición, aplicación y efecto distribuido.
B.1 Principio de implementación
La implementación moderna no debe operar sobre agregados opacos por EAN como unidad principal de cálculo final. Debe operar sobre unidades trazables derivadas del ticket, especialmente sobre los movimientos VENTA_ITEM y sus precios vigentes.
B.2 Vista transitoria de cálculo
Se recomienda construir una vista transitoria en memoria para evaluación de promociones ITEM, por ejemplo ItemPricingView, que contenga al menos:
movimientoVentaIditemIdarticuloId- EAN/PLU u otros atributos comparables
- precio lista del artículo
- precio vigente del ítem
- flags de disponibilidad / reserva / consumo
- promociones ya aplicadas
Esta vista es una ayuda de implementación. No reemplaza el modelo persistido.
B.3 Servicios lógicos sugeridos
La arquitectura sugerida por el modelo es:
MotorPromocionesModoItemProcessorMayoristaEvaluatorVentaXBultoEvaluatorGrupoMayoristaEvaluatorComboEvaluatorCantidadEvaluatorModoPagoConsultaModoPagoAplicarPostPagoProcessorPromoApplicationServiceReconciliacionValidator
B.4 Responsabilidad de PromoApplicationService
Conviene centralizar en un único servicio la aplicación efectiva de una promoción al ticket. Ese servicio debería:
- crear el registro en
promociones[] - distribuir el beneficio/recargo sobre
movimientos[] - actualizar el precio vigente de las vistas transitorias
- respetar signos y reglas de
nucleoimpositivo[] - garantizar trazabilidad entre promoción aplicada y movimientos afectados
B.5 Pseudocódigo directriz para COMBO
La lógica objetivo de ComboEvaluator es:
mientras existan combos aplicables:
evaluar una unidad potencial para cada promo COMBO candidata
calcular su beneficio sobre precios vigentes
seleccionar la mejor unidad según criterio económico
consumir los ítems seleccionados
aplicar la promoción al ticket
recalcular el estado
B.6 Pseudocódigo directriz para CANTIDAD
La lógica objetivo de CantidadEvaluator es similar en estructura, pero sin la semántica de listas múltiples del combo. Debe:
- resolver candidatos según
listasitems[] - verificar cantidad requerida
- tomar precio vigente o precio lista según
usapreciolistaarticulo - computar beneficio
- aplicar por grupos/unidades repetibles hasta agotar aplicabilidad
B.7 Regla de cierre técnico
Ninguna estrategia de implementación queda aprobada si rompe la regla fuerte del modelo:
la suma algebraica de todos los montos de
movimientos[]debe cerrar en 0 al final del ticket.
La implementación puede variar en estructura interna, pero no en esta garantía de reconciliación.
ANEXO C — Pseudocódigo Java 21 detallado
ChatGPT Thinking 5.4
Pseudocódigo Java 21 — MotorPromociones / COMBO / CANTIDAD
Documento auxiliar de implementación alineado con
model.mdactualizado. Objetivo: traducir la semántica del modelo a una arquitectura Java 21 clara, trazable y compatible con ledger.
1. Objetivo
Este documento baja a pseudocódigo Java 21 la lógica del motor de promociones, con foco en:
COMBOCANTIDAD- interacción entre promociones ITEM
- generación de
promociones[]ymovimientos[]
No es código final compilable. Es una guía estructural para implementación.
2. Arquitectura objetivo
com.tipre.ruleengine.logic
MotorPromociones
com.tipre.ruleengine.logic.item
ModoItemProcessor
MayoristaEvaluator
VentaXBultoEvaluator
GrupoMayoristaEvaluator
ComboEvaluator
CantidadEvaluator
ItemPricingViewBuilder
PromoApplicationService
com.tipre.ruleengine.logic.pago
ModoPagoConsulta
ModoPagoAplicar
PostPagoProcessor
com.tipre.ruleengine.validation
ReconciliacionValidator
LedgerConsistencyChecker
3. Idea central de implementación
La implementación moderna no debe operar sobre EAN agregados como el legacy.
Debe operar sobre unidades trazables derivadas de movimientos VENTA_ITEM.
Para eso conviene construir una vista transitoria de cálculo, por ejemplo ItemPricingView, que represente cada unidad elegible del ticket con:
- referencia al
MovimientoVENTA_ITEM - referencia al
Item/Articulo - precio vigente
- precio lista artículo
- flags de disponibilidad
- promociones ya aplicadas
4. Clases y records sugeridos
public enum MetodoComputo {
MAYORISTA,
VENTAXBULTO,
GRUPOMAYORISTA,
COMBO,
CANTIDAD
}
public enum TipoPromoAplicada {
ITEM,
PAGO
}
public record ItemPricingView(
long movimientoVentaId,
long itemId,
long articuloId,
String ean,
BigDecimal unidadesImpactables,
BigDecimal precioListaArticulo,
BigDecimal precioVigente,
boolean consumidoPorPromo,
boolean reservadoTemporalmente
) {}
public record ComboUnitCandidate(
DefinicionPromocion promocion,
List<ItemPricingView> itemsLista1,
List<ItemPricingView> itemsLista2,
BigDecimal baseCalculo,
BigDecimal montoBeneficio
) {
public boolean esAplicable() {
return montoBeneficio != null && montoBeneficio.compareTo(BigDecimal.ZERO) != 0;
}
public List<ItemPricingView> itemsAfectados() {
return Stream.concat(itemsLista1.stream(), itemsLista2.stream()).toList();
}
}
public record CantidadCandidate(
DefinicionPromocion promocion,
List<ItemPricingView> itemsAfectados,
BigDecimal baseCalculo,
BigDecimal montoBeneficio
) {}
5. Orquestación principal
public final class MotorPromociones {
private final ModoItemProcessor modoItemProcessor;
private final ModoPagoConsulta modoPagoConsulta;
private final ModoPagoAplicar modoPagoAplicar;
private final PostPagoProcessor postPagoProcessor;
public Ticket computarModoItem(Ticket ticket, List<DefinicionPromocion> definiciones) {
return modoItemProcessor.procesar(ticket, definiciones);
}
public ResultadoConsultaPago consultarPromocionesPago(
Ticket ticket,
MedioPagoQuery medioPago,
List<DefinicionPromocion> definiciones) {
return modoPagoConsulta.consultar(ticket, medioPago, definiciones);
}
public Ticket aplicarPromocionPago(
Ticket ticket,
Pago pago,
List<DefinicionPromocion> definiciones) {
return modoPagoAplicar.aplicar(ticket, pago, definiciones);
}
public Ticket aplicarPostPago(Ticket ticket, PostPagoRequest request) {
return postPagoProcessor.aplicar(ticket, request);
}
}
6. ModoItemProcessor
public final class ModoItemProcessor {
private final MayoristaEvaluator mayoristaEvaluator;
private final VentaXBultoEvaluator ventaXBultoEvaluator;
private final GrupoMayoristaEvaluator grupoMayoristaEvaluator;
private final ComboEvaluator comboEvaluator;
private final CantidadEvaluator cantidadEvaluator;
private final ItemPricingViewBuilder itemPricingViewBuilder;
public Ticket procesar(Ticket ticket, List<DefinicionPromocion> definiciones) {
List<DefinicionPromocion> promocionesItem = filtrarPromocionesModoItem(ticket, definiciones);
List<ItemPricingView> vistas = itemPricingViewBuilder.build(ticket);
mayoristaEvaluator.evaluar(ticket, vistas, promocionesPorMetodo(promocionesItem, MetodoComputo.MAYORISTA));
refrescarPreciosVigentes(ticket, vistas);
ventaXBultoEvaluator.evaluar(ticket, vistas, promocionesPorMetodo(promocionesItem, MetodoComputo.VENTAXBULTO));
refrescarPreciosVigentes(ticket, vistas);
grupoMayoristaEvaluator.evaluar(ticket, vistas, promocionesPorMetodo(promocionesItem, MetodoComputo.GRUPOMAYORISTA));
refrescarPreciosVigentes(ticket, vistas);
comboEvaluator.evaluar(ticket, vistas, promocionesPorMetodo(promocionesItem, MetodoComputo.COMBO));
refrescarPreciosVigentes(ticket, vistas);
cantidadEvaluator.evaluar(ticket, vistas, promocionesPorMetodo(promocionesItem, MetodoComputo.CANTIDAD));
refrescarPreciosVigentes(ticket, vistas);
return ticket;
}
}
7. ComboEvaluator
7.1 Responsabilidad
- resolver promociones
COMBOde modo ITEM - respetar acumulatividad
- evaluar competencia global entre promociones
- seleccionar una unidad por iteración
- registrar
promociones[]ymovimientos[]
7.2 Pseudocódigo
public final class ComboEvaluator {
private final PromoApplicationService promoApplicationService;
public void evaluar(Ticket ticket, List<ItemPricingView> vistas, List<DefinicionPromocion> promocionesCombo) {
List<DefinicionPromocion> acumulativas = promocionesCombo.stream()
.filter(p -> p.condiciones().acumulativa())
.toList();
List<DefinicionPromocion> noAcumulativas = promocionesCombo.stream()
.filter(p -> !p.condiciones().acumulativa())
.toList();
boolean aplicoAcumulativa = resolverGrupo(ticket, vistas, acumulativas);
if (!aplicoAcumulativa) {
resolverGrupo(ticket, vistas, noAcumulativas);
}
}
private boolean resolverGrupo(
Ticket ticket,
List<ItemPricingView> vistas,
List<DefinicionPromocion> promociones) {
boolean aplicoAlguna = false;
while (true) {
List<ComboUnitCandidate> candidatos = promociones.stream()
.map(promo -> evaluarUnidadCombo(ticket, vistas, promo))
.filter(Objects::nonNull)
.filter(ComboUnitCandidate::esAplicable)
.toList();
if (candidatos.isEmpty()) {
break;
}
ComboUnitCandidate mejor = seleccionarMejor(candidatos);
promoApplicationService.aplicarComboUnitario(ticket, vistas, mejor);
recalcularVistas(ticket, vistas);
aplicoAlguna = true;
}
return aplicoAlguna;
}
private ComboUnitCandidate evaluarUnidadCombo(
Ticket ticket,
List<ItemPricingView> vistas,
DefinicionPromocion promo) {
var lista1 = seleccionarMasCaros(vistasDisponiblesPara(ticket, vistas, promo, ListaNumero.LISTA1),
cantidadRequerida(promo, ListaNumero.LISTA1));
var lista2 = seleccionarMasCaros(vistasDisponiblesPara(ticket, vistas, promo, ListaNumero.LISTA2),
cantidadRequerida(promo, ListaNumero.LISTA2));
if (!cumpleCantidades(promo, lista1, lista2)) {
return null;
}
BigDecimal base = sumarPreciosVigentes(lista1, lista2, promo);
BigDecimal beneficio = calcularBeneficioCombo(promo, base, lista1, lista2, ticket);
if (beneficio.compareTo(BigDecimal.ZERO) == 0) {
return null;
}
return new ComboUnitCandidate(promo, lista1, lista2, base, beneficio);
}
private ComboUnitCandidate seleccionarMejor(List<ComboUnitCandidate> candidatos) {
return candidatos.stream()
.min(this::compararPorConvenienciaEconomica)
.orElseThrow();
}
}
7.3 Criterio de comparación
private int compararPorConvenienciaEconomica(ComboUnitCandidate a, ComboUnitCandidate b) {
// Más negativo = mejor descuento
// Más positivo = peor resultado para el cliente
return a.montoBeneficio().compareTo(b.montoBeneficio());
}
7.4 Selección de los ítems más caros
private List<ItemPricingView> seleccionarMasCaros(List<ItemPricingView> candidatos, int cantidad) {
return candidatos.stream()
.filter(v -> !v.consumidoPorPromo())
.filter(v -> !v.reservadoTemporalmente())
.sorted(Comparator.comparing(ItemPricingView::precioVigente).reversed())
.limit(cantidad)
.toList();
}
8. Cálculo del beneficio COMBO
private BigDecimal calcularBeneficioCombo(
DefinicionPromocion promo,
BigDecimal base,
List<ItemPricingView> lista1,
List<ItemPricingView> lista2,
Ticket ticket) {
return switch (promo.tipobeneficio().id()) {
case "PORCENTAJE_DESCUENTO" -> base
.multiply(porcentaje(promo.valorbeneficio()))
.negate();
case "PORCENTAJE_RECARGO" -> base
.multiply(porcentaje(promo.valorbeneficio()));
case "MONTO_DESCUENTO" -> promo.valorbeneficio().negate();
case "MONTO_RECARGO" -> promo.valorbeneficio();
case "NUEVOPRECIOITEM", "EAN_COMBO" -> {
BigDecimal precioCombo = resolverPrecioCombo(promo, ticket);
yield precioCombo.subtract(base);
}
default -> throw new IllegalStateException("Tipo de beneficio no soportado para COMBO: " + promo.tipobeneficio().id());
};
}
private BigDecimal resolverPrecioCombo(DefinicionPromocion promo, Ticket ticket) {
ListaItems lista3 = obtenerLista3(promo);
return switch (lista3.tipoelemento().id()) {
case "MONTO" -> lista3.monto();
case "EAN" -> buscarArticuloPorEan(ticket, lista3.valores().getFirst()).preciolista();
default -> throw new IllegalStateException("LISTA3 solo admite MONTO o EAN");
};
}
9. CantidadEvaluator
CANTIDAD comparte buena parte de la estructura de COMBO, pero sin la semántica especial de LISTA3.
public final class CantidadEvaluator {
private final PromoApplicationService promoApplicationService;
public void evaluar(Ticket ticket, List<ItemPricingView> vistas, List<DefinicionPromocion> promocionesCantidad) {
List<DefinicionPromocion> acumulativas = promocionesCantidad.stream()
.filter(p -> p.condiciones().acumulativa())
.toList();
List<DefinicionPromocion> noAcumulativas = promocionesCantidad.stream()
.filter(p -> !p.condiciones().acumulativa())
.toList();
boolean aplicoAcumulativa = resolverGrupo(ticket, vistas, acumulativas);
if (!aplicoAcumulativa) {
resolverGrupo(ticket, vistas, noAcumulativas);
}
}
private boolean resolverGrupo(
Ticket ticket,
List<ItemPricingView> vistas,
List<DefinicionPromocion> promociones) {
boolean aplicoAlguna = false;
for (DefinicionPromocion promo : promociones) {
while (true) {
CantidadCandidate candidate = evaluarUnidadCantidad(ticket, vistas, promo);
if (candidate == null) {
break;
}
promoApplicationService.aplicarCantidad(ticket, vistas, candidate);
recalcularVistas(ticket, vistas);
aplicoAlguna = true;
}
}
return aplicoAlguna;
}
}
10. Aplicación de promociones al ledger
La aplicación concreta del beneficio debe centralizarse en un servicio común, para no duplicar reglas de signos, distribución y trazabilidad.
public final class PromoApplicationService {
public void aplicarComboUnitario(
Ticket ticket,
List<ItemPricingView> vistas,
ComboUnitCandidate candidate) {
Promocion promocion = crearPromocionAplicada(ticket, candidate.promocion(), TipoPromoAplicada.ITEM);
List<Movimiento> movimientosPromo = distribuirBeneficio(
ticket,
candidate.itemsAfectados(),
candidate.montoBeneficio(),
promocion.id());
ticket.promociones().add(promocion.withMonto(candidate.montoBeneficio()));
ticket.movimientos().addAll(movimientosPromo);
marcarConsumidos(vistas, candidate.itemsAfectados());
actualizarElementosPromocion(promocion, movimientosPromo);
}
public void aplicarCantidad(
Ticket ticket,
List<ItemPricingView> vistas,
CantidadCandidate candidate) {
// Misma idea: crear promocion[], generar movimientos[], marcar consumo
}
}
11. Distribución del beneficio
La distribución debe hacerse sobre los VENTA_ITEM afectados.
private List<Movimiento> distribuirBeneficio(
Ticket ticket,
List<ItemPricingView> itemsAfectados,
BigDecimal montoTotalBeneficio,
long promocionAplicadaId) {
BigDecimal base = itemsAfectados.stream()
.map(ItemPricingView::precioVigente)
.reduce(BigDecimal.ZERO, BigDecimal::add);
List<Movimiento> resultado = new ArrayList<>();
for (ItemPricingView item : itemsAfectados) {
BigDecimal proporcion = dividir(item.precioVigente(), base);
BigDecimal montoItem = montoTotalBeneficio.multiply(proporcion);
Movimiento promoMov = crearMovimientoPromocionDistribuido(
ticket,
item.movimientoVentaId(),
promocionAplicadaId,
montoItem,
item);
resultado.add(promoMov);
}
ajustarRedondeoFinal(resultado, montoTotalBeneficio);
return resultado;
}
12. ItemPricingViewBuilder
public final class ItemPricingViewBuilder {
public List<ItemPricingView> build(Ticket ticket) {
return ticket.movimientos().stream()
.filter(m -> m.concepto() == ConceptoMovimiento.VENTA_ITEM)
.map(m -> construirVista(ticket, m))
.toList();
}
private ItemPricingView construirVista(Ticket ticket, Movimiento movimientoVenta) {
Item item = buscarItemPorMovimiento(ticket, movimientoVenta);
Articulo articulo = buscarArticulo(ticket, item.articuloid());
return new ItemPricingView(
movimientoVenta.id(),
item.id(),
articulo.id(),
articulo.ean(),
BigDecimal.ONE,
articulo.preciolista(),
calcularPrecioVigente(ticket, movimientoVenta.id(), articulo),
false,
false
);
}
}
calcularPrecioVigente(...) debe reconstruir el saldo económico de la unidad a partir de:
VENTA_ITEM- promociones ya aplicadas sobre ese
movimientoid - pagos previos solo cuando la etapa lo requiera
En MODO ITEM, normalmente solo necesita ventas + promociones ITEM ya computadas.
13. Reglas comunes de validación
private boolean promoEsElegible(Ticket ticket, DefinicionPromocion promo) {
return vigenciaOk(ticket, promo)
&& sucursalOk(ticket, promo)
&& convenioOk(ticket, promo)
&& listasItemsNoVacias(promo)
&& maximoPromosTicketOk(ticket, promo);
}
private boolean itemEsElegibleParaPromo(
Ticket ticket,
ItemPricingView item,
DefinicionPromocion promo) {
if (item.consumidoPorPromo() || item.reservadoTemporalmente()) {
return false;
}
Articulo articulo = buscarArticulo(ticket, item.articuloId());
if (promo.condiciones().excluyearticulosoferta() && articulo.esarticuloconoferta()) {
return false;
}
return itemPerteneceAAlgunaLista(item, promo);
}
14. Redondeo
Dado que el modelo trabaja con distribución por unidad, se recomienda:
- precisión interna alta (
scale >= 8) - redondeo explícito al distribuir
- ajuste final del residuo en la última línea distribuida
private static final int SCALE_INTERNA = 8;
private static final RoundingMode ROUNDING = RoundingMode.HALF_UP;
15. Validación final
public final class ReconciliacionValidator {
public void validarCierre(Ticket ticket) {
BigDecimal total = ticket.movimientos().stream()
.flatMap(m -> m.nucleoimpositivo().stream())
.map(NucleoImpositivoLinea::monto)
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (total.compareTo(BigDecimal.ZERO) != 0) {
throw new IllegalStateException("El ticket no reconcilia: Σ movimientos != 0");
}
}
}
16. Observaciones de diseño
16.1 Qué se conserva del legacy
- competencia entre combos
- selección greedy
- consumo progresivo de ítems
- sensibilidad al estado vigente
16.2 Qué no se conserva literalmente
- 5 listas funcionales
- contadores agregados por EAN como fuente principal
- storage operativo intermedio tipo
dE_Promo - acumulados impositivos como única persistencia
16.3 Qué aporta el modelo nuevo
- ledger distribuido
- trazabilidad por unidad
- separación clara entre definición, aplicada y efecto económico
- compatibilidad natural con
POSTPAGOy reconciliación final
17. Próximos pasos sugeridos
- traducir este pseudocódigo a interfaces Java concretas
- definir
DefinicionPromocion,ListaItems,Condiciones,Movimiento,Promocioncomo tipos Java 21 - crear tests de regresión funcional:
- combo simple
- combo con dos listas
- combo + cantidad
- combo + exclusión por artículo en oferta
- combo + pago
- validar redondeo y reconciliación con ejemplos de ticket
FIN — pseudocódigo Java 21 alineado con model.md actualizado