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](#1-objetivo-y-principios)
2. [Agregado raíz: Ticket](#2-agregado-raíz-ticket)
3. [Granos del modelo](#3-granos-del-modelo)
4. [Objetos del dominio](#4-objetos-del-dominio)
5. [Regla de itemización](#5-regla-de-itemización)
6. [Catálogos y registros del ticket](#6-catálogos-y-registros-del-ticket)
7. [Estructura de Movimiento](#7-estructura-de-movimiento)
8. [Reglas de pagos](#8-reglas-de-pagos)
9. [Reglas de ledger y convenciones de signos](#9-reglas-de-ledger-y-convenciones-de-signos)
10. [Regla de excedente y vuelto](#10-regla-de-excedente-y-vuelto)
11. [Reglas de reconciliación](#11-reglas-de-reconciliación)
12. [Orden base de cálculo](#12-orden-base-de-cálculo)

**PARTE II — MOTOR DE PROMOCIONES**
13. [Modelo de promociones: tres capas](#13-modelo-de-promociones-tres-capas)
14. [Modos de cómputo e inferencia de alcance](#14-modos-de-cómputo-e-inferencia-de-alcance)
15. [Catálogos auxiliares de promociones](#15-catálogos-auxiliares-de-promociones)
16. [Definición de promoción: listapromociones](#16-definición-de-promoción-listapromociones)
17. [Promoción aplicada en el ticket: promociones\[\]](#17-promoción-aplicada-en-el-ticket-promocionesl)
18. [Flujo del motor MODO ITEM](#18-flujo-del-motor-modo-item)
19. [Flujo específico — método COMBO (estrategia de resolución)](#19-flujo-específico--método-combo-estrategia-de-resolución)
20. [Flujo del motor MODO PAGO y MODO_POSTPAGO](#20-flujo-del-motor-modo-pago-y-modo_postpago)

**PARTE III — IMPLEMENTACIÓN**
21. [Convenciones Java 21](#21-convenciones-java-21)
22. [Convenciones de naming](#22-convenciones-de-naming)
23. [Relación con los JSON](#23-relación-con-los-json)
24. [Criterios de revisión](#24-criterios-de-revisión)
25. [Resumen ejecutivo](#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.

```json
{
  "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_ITEM` → `movimientoid = 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 `MAYORISTA` → **no** 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

```json
{
  "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[]`

```json
"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[]`

```json
"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 `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 = 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

```json
{
  "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:

- `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

```json
{
  "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:

```text
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.

