TR
tipre ruleengine
model.md 202604161820 Java 21 COMBO integrado HTML navegable Descargar model.md Descargar jsonmodel.zip
Documentación integral del modelo

POS / Ticket / Ledger / Motor de Promociones

Versión extensa y navegable del modelo consolidado. Incluye dominio, reglas de cálculo, estrategia COMBO, flujo ITEM/PAGO/POSTPAGO, anexos de semántica legacy observada y pseudocódigo Java 21 alineado a la arquitectura objetivo.

Ticket agregado raíz Ledger distribuido COMBO global greedy POSTPAGO Reconciliación 0
Archivo fuente
model.md
Versión
202604161820
Lenguaje objetivo
Java 21
Semántica COMBO
Máximo 2 listas de ítems + LISTA3 como precio. Resolución global, iterativa, greedy y trazable.
Criterio de cierre
La suma algebraica de todos los montos de movimientos[] debe cerrar exactamente en 0.
Contenido
Modelo completo + anexos legacy + puente de implementación + pseudocódigo detallado.
Agregado raíz
Ticket
Catálogo + captura + promociones + pagos + ledger
Capas de promoción
3
Definición, aplicada y efecto distribuido
Cierre fuerte
Σ movimientos = 0
Reconciliación obligatoria al final del ticket
datosreferenciales metadatos + total + saldo núcleo impositivo ticket cliente tipo + convenio articulos[] items[] promociones[] pagos[] movimientos[] VENTA_ITEM + PROMOCION + PAGO fuente de verdad fiscal y contable reconciliación obligatoria
items MAYORISTA VENTAXBULTO GRUPOMAYORISTA COMBO CANTIDAD

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

  1. Objetivo y principios
  2. Agregado raíz: Ticket
  3. Granos del modelo
  4. Objetos del dominio
  5. Regla de itemización
  6. Catálogos y registros del ticket
  7. Estructura de Movimiento
  8. Reglas de pagos
  9. Reglas de ledger y convenciones de signos
  10. Regla de excedente y vuelto
  11. Reglas de reconciliación
  12. 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:

  1. este model.md tiene prioridad absoluta
  2. jsonmodel
  3. 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 pagoorigenid cuando 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_ITEMmovimientoid = null
  • PROMOCION → aplica sobre un VENTA_ITEM
  • PAGO distribuido → aplica sobre un VENTA_ITEM
  • PAGO excedente 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.vuelto conserva el resultado global y movimientos[] explica la distribución.
  • No sobrecargar Pago con campos adicionales. La versión base usa solo monto.

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[].monto no sustituye al detalle distribuido del ledger.
  • CUPON es excepción: solo queda en promociones[] con monto = 0, no genera movimiento.
  • Las promociones sintéticas de MODO_POSTPAGO también se registran en promociones[]; en ese caso promocionid = 0 y pagoorigenid referencia 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:

  1. La parte aplicada → movimientos PAGO negativos contra ítems.
  2. El excedente → movimiento PAGO negativo con movimientoid = null, origenid = id del pago ingresado.
  3. El vuelto → movimiento PAGO positivo con movimientoid = 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 ITEM
  • mediosdepago[] con entradas → promoción de MODO PAGO, se computa solo para esos medios y solo sobre los ítems de listasitems[]
  • 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 MAYORISTAno se evalúa en VENTAXBULTO ni GRUPOMAYORISTA.
  • Si el ítem ya fue alcanzado por una promoción del método VENTAXBULTO (sin haber sido alcanzado por MAYORISTA) → no se evalúa en GRUPOMAYORISTA.
  • Tras MAYORISTA → VENTAXBULTO → GRUPOMAYORISTA, el ítem pasa a COMBO y CANTIDAD con su último precio vigente.

14.5 Lógica acumulativa / no acumulativa

Aplica exclusivamente en los métodos COMBO y CANTIDAD.

Regla del motor:

  1. Primero se evalúan y computan todas las promociones con condiciones.acumulativa = true.
  2. 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 en articulo.nucleoimpositivo[]
    • si condiciones.usapreciolistaarticulo = false → base = último precio vigente del ítem en ese momento del cómputo, luego de promociones previas ya computadas
  • 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 (ahora sucursales[] en la promoción), CANTIDAD_MAX_PROMOS (ahora condiciones.maximacantidadpromosxticket), MEDIODEPAGO (ahora mediosdepago[] 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 Identificador único.
descripcion string Nombre descriptivo.
metodocomputo objeto {id} Método de cómputo.
tipobeneficio objeto {id} 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 Lista de { id } de convenios. Vacío = aplica a todos.
condiciones objeto Ver 16.3.
vigencia objeto Ver 16.4.
sucursales[] array Sucursales habilitadas. Vacío = todas. Sin exclusión.
mediosdepago[] array Vacío = MODO ITEM. Con entradas = MODO PAGO.
listasitems[] array 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 + cuotaidmdep + 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 LISTA1 y LISTA2, valores[] contiene uno o más valores candidatos válidos para satisfacer esa lista.
  • cantidad expresa la cantidad requerida para cumplir esa lista.
  • tipoelemento indica cómo interpretar todos los elementos de valores[] (por ejemplo EAN, PLU, RUBRO, etc.).

Semántica de LISTA3 en combos: define el precio de venta unitario del combo.

  • tipoelemento = MONTO → precio directo en campo monto, con valores[] vacío.
  • tipoelemento = EANvalores[] 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 Identificador del registro aplicado.
promocionid integer 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 Nombre heredado de la definición o descripción sintética generada por el sistema.
tipoPromo string ITEM o PAGO. Naming conservado por consistencia con JSON del modelo.
monto number 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:

  • LISTA1 y LISTA2 = listas funcionales de ítems candidatos
  • LISTA3 = precio del combo, directo (MONTO) o indirecto (EAN)
  • la trazabilidad definitiva del efecto económico vive en promociones[] y movimientos[], 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 = 2
  • LISTA2.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:

  1. primero se procesan promociones acumulativa = true
  2. 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:

  1. un registro maestro en promociones[]
  2. movimientos PROMOCION distribuidos en movimientos[] sobre los VENTA_ITEM alcanzados

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
  • record permitido y recomendado para value objects inmutables, referencias simples de catálogo y DTOs internos
  • enum Java para catálogos cerrados
  • Constructores, factorías y métodos explícitos cuando mejoren la legibilidad
  • switch expressions, 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:

  1. este model.md
  2. jsonmodel
  3. 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

  • Ticket es 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[] y pagos[] son registros maestros. Su efecto económico vive en movimientos[].
  • Pago.monto puede ser negativo cuando el medio actúa como vuelto.
  • nucleoimpositivo es la base común para importes e impuestos.
  • articulo.preciolista corresponde a la suma de los montos informados en articulo.nucleoimpositivo[].
  • movimientos[] no conserva niveles de precio. El efecto económico de promociones y pagos se explica exclusivamente por sus montos distribuidos.
  • cliente.convenio es un único { id, nombre }. Su id filtra conveniosclientes[].
  • articulo.esarticuloconoferta identifica artículos con oferta. Si una promoción tiene condiciones.excluyearticulosoferta = true, esos artículos se excluyen del cómputo.
  • tiposdepago tiene clave compuesta funcional idmdep + ididmdep + cuota.
  • El modo de la promoción se infiere de mediosdepago[] para ITEM/PAGO; POSTPAGO se 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 COMBO y CANTIDAD.
  • condiciones.usapreciolistaarticulo define la base de cálculo de promociones ITEM: true usa articulo.preciolista; false usa el último precio vigente tras promociones previas.
  • CUPON solo queda en promociones[] con monto = 0. No genera movimiento.
  • POSTPAGO genera una promoción sintética con promocionid = 0 y pagoorigenid informado.
  • 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:

  • LISTA1
  • LISTA2
  • LISTA3 como 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:

  1. determinar cuántos elementos requiere cada lista
  2. buscar candidatos disponibles por lista
  3. seleccionar primero los de mayor precio
  4. reservarlos temporalmente
  5. si se completa la cantidad requerida, calcular el beneficio
  6. 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 maestro
  • movimientos[] 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:

  • movimientoVentaId
  • itemId
  • articuloId
  • 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:

  • MotorPromociones
  • ModoItemProcessor
  • MayoristaEvaluator
  • VentaXBultoEvaluator
  • GrupoMayoristaEvaluator
  • ComboEvaluator
  • CantidadEvaluator
  • ModoPagoConsulta
  • ModoPagoAplicar
  • PostPagoProcessor
  • PromoApplicationService
  • ReconciliacionValidator

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:

  1. crear el registro en promociones[]
  2. distribuir el beneficio/recargo sobre movimientos[]
  3. actualizar el precio vigente de las vistas transitorias
  4. respetar signos y reglas de nucleoimpositivo[]
  5. 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.md actualizado. 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:

  • COMBO
  • CANTIDAD
  • interacción entre promociones ITEM
  • generación de promociones[] y movimientos[]

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 Movimiento VENTA_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 COMBO de modo ITEM
  • respetar acumulatividad
  • evaluar competencia global entre promociones
  • seleccionar una unidad por iteración
  • registrar promociones[] y movimientos[]

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 POSTPAGO y reconciliación final

17. Próximos pasos sugeridos

  1. traducir este pseudocódigo a interfaces Java concretas
  2. definir DefinicionPromocion, ListaItems, Condiciones, Movimiento, Promocion como tipos Java 21
  3. crear tests de regresión funcional:
    • combo simple
    • combo con dos listas
    • combo + cantidad
    • combo + exclusión por artículo en oferta
    • combo + pago
  4. validar redondeo y reconciliación con ejemplos de ticket

FIN — pseudocódigo Java 21 alineado con model.md actualizado