# Model Alignment

## Purpose

This document does not replace [model.md](/C:/Work/AI/POS/docs/model.md).

- `model.md` remains the domain specification and primary source of truth.
- This file describes how the current backend implementation aligns with that model.
- If this file conflicts with `model.md`, `model.md` wins.

The goal is to make the current implementation state explicit:

- what is already implemented
- what is implemented conservatively
- what remains pending
- what implementation tradeoffs are currently in place

## Current Aggregate Shape

The codebase preserves the central model idea that `Ticket` is the aggregate root.

The aggregate currently keeps these concepts explicitly separated:

- `articulos[]`: in-ticket article catalog
- `items[]`: operational capture
- `pagos[]`: payment master records
- `promociones[]`: applied promotion master records
- `movimientos[]`: distributed economic and fiscal explanation layer

This separation is implemented in code and is not flattened into invoice-style tables.

## Ticket And Lifecycle

The backend currently supports:

- draft creation
- item mutation
- payment registration
- promotion registration
- payment-promotion consultation
- post-payment promotion application
- reconciliation summary
- finalization

There is explicit mutation audit support for the aggregate lifecycle, including consultation vs application flows for payment promotions.

## Ledger Model

The current implementation is aligned with the model in these points:

- `VENTA_ITEM` is generated server-side from `items[]`
- `PROMOCION` movements are generated server-side from applied promotion records
- `PAGO` movements are generated server-side from payment master records
- `movimientos[]` remains the economic source of truth for explaining ticket economics

### Payment Distribution

The current backend uses proportional payment distribution across all `VENTA_ITEM` movements that still have positive saldo.

This is a correction over the earlier FIFO-style approach.

The current behavior is:

- positive payments are distributed proportionally over net positive item saldo
- net positive item saldo is computed after item-linked promotions
- change / overpayment is resolved explicitly with:
  - excedente movement
  - vuelto movement
  - synthetic negative payment master record when needed

### Fiscal Distribution

Derived movement fiscal scaling currently works like this:

- promotion-derived movements scale against the original fiscal nucleus of the base `VENTA_ITEM`
- payment-derived movements also scale against the original fiscal nucleus of the referenced sale movement
- payment applicability, however, uses net economic saldo per movement after promotions
- derived movements preserve the component structure of the referenced `VENTA_ITEM.nucleoimpositivo[]`; they do not collapse fiscal impact into a single synthetic component

Promotion-derived movements also preserve the sign of the corresponding `promociones[].elementos[].monto`:

- discount stays negative
- recargo stays positive

That sign is propagated through fiscal scaling because the implementation computes a positive factor from `|elemento.monto| / totalBase` and passes an explicit `negate` flag into `scaleNucleoImpositivo(...)`.

That means the implementation distinguishes:

- economic saldo used for distribution
- original fiscal base used for derived fiscal scaling

This is the current implementation rule for fiscal truth:

- `PROMOCION` and `PAGO` movements derive their `nucleoimpositivo[]` proportionally from the referenced `VENTA_ITEM` fiscal components
- discounts propagate as negative scaled components
- recargos propagate as positive scaled components

The original `model.md` is less explicit on this point than the codebase; the current backend therefore makes this convention concrete.

### Monetary Arithmetic And Rounding

The backend uses `BigDecimal` for monetary fields and item economics.

Current implementation conventions are:

- final ticket-facing amounts are normalized at scale `2`
- proportional distributions use `RoundingMode.HALF_UP`
- sensitive intermediate divisions use higher intermediate precision before final rounding, typically:
  - `divide(..., 8, HALF_UP).setScale(2, HALF_UP)`

This applies to the current implementations of:

- payment distribution
- promotion movement proration
- combo payment proration
- postpago itemized distribution

This is an implementation-level clarification that reduces reconciliation drift relative to a looser reading of the original model.

## Promotion Engine State

### Implemented ITEM Methods

The current ITEM engine supports:

- `MAYORISTA`
- `VENTAXBULTO`
- `GRUPOMAYORISTA`
- `COMBO`
- `CANTIDAD`

Catalog source is currently split explicitly by method family:

- `MAYORISTA` and `GRUPOMAYORISTA` are loaded from `listapromociones_mayorista`
- `VENTAXBULTO`, `COMBO`, and `CANTIDAD` are loaded from `listapromociones`

For `MAYORISTA` and `GRUPOMAYORISTA`, the current implementation selects the highest-threshold tramo whose `cantidadminima` is satisfied by the current unit count and applies the resulting benefit to all candidate units.

`GRUPOMAYORISTA` counts units across all configured `eans[]` as a single group pool.

For these two mayorista-family methods, the current implementation treats one applied promotion record as one full-definition resolution over the currently eligible unit pool. In practice this means:

- a given `MAYORISTA` definition applies at most once per ticket
- a given `GRUPOMAYORISTA` definition applies at most once per ticket
- positive values of `condiciones.maximacantidadpromosxticket` do not multiply repeated applications of the same definition, because the selected tramo already consumes the full eligible unit pool in one shot
- different definitions may still compete as alternative candidates, and the engine keeps the economically best applicable result

`VENTAXBULTO` is currently implemented as a conservative ITEM-mode slice:

- it uses the generic `listapromociones` catalog
- it resolves the first inclusion list as the eligible unit pool
- `listasitems[].cantidad` is interpreted as the bundle size
- only units that fit into complete bundles are reached by the promotion
- leftover units outside complete bundles remain free for later methods
- reached units are excluded from `GRUPOMAYORISTA`
- `maximacantidadpromosxticket` limits the number of bundle applications
- accumulative and non-accumulative definitions are now split operationally:
  - accumulative `VENTAXBULTO` definitions apply first and update prices without marking units as reached
  - non-accumulative `VENTAXBULTO` definitions then compete over units that still remain unreached

This implementation is intended to stay aligned with the current model direction without claiming that every possible `VENTAXBULTO` variant is already covered.

The current implementation also treats the three early item-price families as mutually exclusive alternatives:

- `MAYORISTA`
- `VENTAXBULTO`
- `GRUPOMAYORISTA`

If more than one of those families could apply over the same ticket state, the engine compares them as competing early paths and keeps the path with the best total downstream economic result instead of stacking them together.

### Implemented Payment-Side Methods

The current payment-side logic supports:

- payment `COMBO`
- payment `CANTIDAD`
- `MODO PAGO CONSULTA`
- `POSTPAGO`

### Shared ITEM Pricing View

The backend has a shared transient pricing/orchestration layer for ITEM promotions.

This supports:

- method ordering
- current price vs list price decisions
- cumulative vs non-cumulative behavior
- unit-level reached/consumed semantics
- bounded global path comparison between ITEM methods

Current effective ITEM orchestration is:

- `MAYORISTA`
- `VENTAXBULTO`
- `GRUPOMAYORISTA`
- `COMBO`
- `CANTIDAD`

In the current orchestration, `MAYORISTA`, `VENTAXBULTO`, and `GRUPOMAYORISTA` are not combined with each other inside the same early path. They are evaluated as alternative early resolutions, and the winning path then continues into `COMBO` and `CANTIDAD`.

### Non-Integer Quantities And Weighted Items

The current implementation now makes the weighted-item strategy explicit:

- if an `item.unidades` value is an integer, the backend generates one `VENTA_ITEM` movement per unit
- if an `item.unidades` value has a fractional remainder, the backend generates:
  - one `VENTA_ITEM` per full unit
  - one additional scaled `VENTA_ITEM` for the fractional remainder

This preserves ledger traceability while avoiding hidden quantity loss.

The promotion engine then evaluates quantity-sensitive ITEM methods against the real summed quantity of those sale slices:

- `MAYORISTA`
- `VENTAXBULTO`
- `GRUPOMAYORISTA`
- `COMBO`
- `CANTIDAD`

That means a fractional remainder is not treated as an extra whole unit just because it is represented by its own `VENTA_ITEM` movement. At the same time, if a promotion definition explicitly requires a fractional quantity, that remainder can participate economically and is reflected in `promociones[].elementos[].unidadesimpactadas`.

### COMBO LISTA3 Semantics

The backend explicitly implements the current intended semantics for `LISTA3` in combo promotions:

- `tipoelemento = MONTO`
  - combo final price is taken directly from `monto`
  - `valores[]` must be empty
- `tipoelemento = EAN`
  - `valores[]` must contain exactly one EAN
  - combo final price is taken from the list price of that article in the in-ticket article catalog

This means `EAN_COMBO` is not treated as a separate engine family in the current implementation; it is handled as `COMBO` price indirection through `LISTA3 = EAN`.

### Accumulative vs Non-Accumulative

The engine now explicitly considers cumulative vs non-cumulative configuration:

- non-cumulative promotions do not reuse units already reached by non-cumulative price promotions
- cumulative promotions may coexist over already reached units when the method rules allow it
- internal competition inside `MAYORISTA`, `GRUPOMAYORISTA`, and non-cumulative `CANTIDAD` has been hardened so order-in-catalog is not the deciding factor
- when those internal competitions compare descuento and recargo candidates, the engines now rank them by customer outcome rather than by absolute amount:
  - descuento beats recargo
  - between descuentos, the larger discount wins
  - between recargos, the smaller recargo wins
- the same signed customer-benefit metric is also used when the ITEM orchestrator compares full early paths, so a path with net recargo no longer beats the no-promotion path just because its absolute magnitude is larger

### Current Heuristic Clarifications

Some engine behaviors are implemented conservatively and are important to state explicitly:

- `COMBO` uses deterministic heuristic resolution rather than an exhaustive global optimizer
- in the current `COMBO` implementation, candidates are compared first by higher immediate economic benefit, then by higher regular-price coverage, then by lower `promocionid`, and finally by a stable movement-id signature
- payment `COMBO` now follows the same deterministic competition idea across eligible payment promotions; it no longer depends on raw catalog order when multiple combo-payment definitions compete over the same payment
- `COMBO` item and payment both now support either descuento or recargo semantics from `LISTA3`; a combo with price above the regular sum is preserved as a positive promotion amount rather than being discarded
- in the current `ITEM` orchestration, `COMBO` still competes only with other `COMBO` candidates and `CANTIDAD` still competes only with other `CANTIDAD` candidates; the interaction between both methods is governed by method order plus `acumulativa` / `no acumulativa` rules
- within that ordered orchestration, the post-combo stage now compares not only `CANTIDAD` alone versus the full greedy `COMBO` result, but also bounded prefixes of the greedy `COMBO` sequence before entering `CANTIDAD`; this is treated as a cutoff decision inside the `COMBO -> CANTIDAD` pipeline, not as a free global competition between both methods
- the same customer-benefit ordering is now applied to internal candidate competition in `MAYORISTA`, `VENTAXBULTO`, `GRUPOMAYORISTA`, and non-cumulative `CANTIDAD`, so a larger recargo no displaces a smaller descuento just because its absolute magnitude is higher
- weighted / non-integer item quantities are now handled by explicit sale-slice quantity tracking, but the original `model.md` still remains less prescriptive than the implementation on some edge details
- `maximacantidadpromosxticket` semantics for `MAYORISTA` / `GRUPOMAYORISTA` are implemented conservatively as at-most-once-per-definition, because each application already resolves the full currently eligible pool for that definition

### Current Engine Limits

The engine is materially beyond the initial scaffold, but it is still not an exhaustive realization of every possible heuristic implied by the original model.

Important remaining refinement areas include:

- additional combo/payment heuristics beyond the current conservative engine
- deeper optimization heuristics for ambiguous competing paths

## Payment Promotion And Postpago State

The current implementation includes:

- payment catalog resolution through `idmdep + ididmdep + cuota`
- payment promotion consultation snapshots
- consistency checks between `CONSULTA` and `APLICAR`
- explicit `pagoorigenid` reconciliation
- stronger validation that payment promotions only affect movements actually covered by the origin payment
- `POSTPAGO` as explicit payment-driven promotion flow with itemized distribution rules

The backend now checks, conservatively:

- payment promotions cannot exceed what the origin payment covered
- postpago must target movements that still had saldo before the source payment
- postpago pre-payment saldo is currently computed per `VENTA_ITEM` as:
  - original sale amount
  - plus prior `PROMOCION` movements on that item
  - plus prior `PAGO` movements whose `origenid < pagoorigenid`
- the triggering source pago is intentionally excluded from that pre-payment saldo computation
- postpago distribution is rejected if no item has positive residual saldo before the source pago
- applied payment promotions must remain consistent with the last compatible consultation snapshot
- `APPLY_PAYMENT_PROMOTION` must also use a consultation snapshot that is still temporally current for that medio/cuota: if another positive `REGISTER_PAYMENT` of the same resolved `idmdep + ididmdep + cuota` occurs after the consultation and before the apply, the earlier consultation is treated as stale and finalization fails until a new compatible consultation exists
- if multiple `APPLY_PAYMENT_PROMOTION` operations reuse the same compatible consultation snapshot, finalization now also validates aggregate snapshot consumption:
  - the distinct positive pagos backed by that consultation cannot exceed the quoted `saldoNeto`
  - the combined applied promotion amounts cannot exceed the quoted `promotionAmounts`
  - the combined applied per-movement distribution cannot exceed the quoted `promotionMovementAmounts`
- synthetic negative pagos created only to resolve vuelto do not participate in that temporal check because they are derived bookkeeping outputs, not user-registered payment intents
- when a `POSTPAGO` discount causes the ticket to become economically overpaid, the rebuilt aggregate re-runs payment resolution and resolves the excess through the same returned-change mechanism already used for ordinary payment overage
- in other words, the backend does not cap the `POSTPAGO` distribution by post-source-payment residual per item; instead it treats the postpago-adjusted ticket total as the new economic truth and lets payment redistribution / vuelto resolution absorb the difference

## CUPON Semantics

`CUPON` is now treated explicitly as a non-financial benefit:

- it remains only in `promociones[]`
- it uses `monto = 0`
- it carries no `elementos[]`
- it generates no `PROMOCION` ledger movements
- it does not affect `total`, `saldo`, payment distribution, fiscal scaling, or final reconciliation

Operationally, the current backend treats it as a coupon/print-output marker that downstream consumers can resolve from the applied promotion record and its catalog definition. It is not part of the economic ledger.

## Validation And Reconciliation

The current implementation has two distinct layers:

### Structural Validators

The structural validator pipeline includes:

- `TicketStructureValidator`
- `TicketReferenceValidator`
- `TicketLedgerValidator`

These validators currently cover:

- aggregate structure
- article reference consistency
- movement reference rules
- payment method structural validity
- payment-origin consistency
- promotion-origin consistency
- movement-level distribution consistency such as:
  - `Σ pagos = Σ movimientos PAGO`
  - `Σ promociones ledger-bearing = Σ movimientos PROMOCION`
- no-ledger promotion restrictions

These structural validators do **not** enforce the strongest ledger invariant `Σ movimientos = 0`.

### Reconciliation And Finalization

The strong economic/final-close invariants are enforced through reconciliation, currently built in `buildReconciliationSummary(...)` and used by finalization.

Finalization currently depends on reconciliation checks that include:

- item presence
- ledger-bearing movement presence
- promotion and payment distribution coherence
- `total = ventas + promociones`
- `saldo = total - pagos`
- `saldo = 0`
- ledger balance `= 0`
- audit consistency for payment consultation and application flows

So, in the current codebase, `Σ movimientos = 0` is enforced at the reconciliation/finalization layer rather than inside `TicketLedgerValidator`.

## Persistence State

The persistence model is currently hybrid but intentionally structured.

### Normalized Child Persistence Already In Place

The backend persists normalized relational rows for:

- ticket header
- datosreferenciales support fields not already present in the header row (`comercio`, `nroSucursal`, `nroPv`, `vuelto`, `nucleoimpositivo[]`)
- articulos
- cliente
- items
- movements
- payments
- applied promotions
- mutation audit

### Snapshot Use

The aggregate snapshot still exists as a conservative compatibility layer.

This remains a Phase 1 / early Phase 3 tradeoff and is not the intended final persistence end state.

The current trajectory is to keep moving stable substructures away from snapshot fallback and into explicit relational persistence and read models.

`datosreferenciales` are now rehydrated from normalized persistence rather than from the snapshot fallback path.
`cliente` is also now rehydrated only from normalized persistence on the normal `getById(...)` path, rather than falling back to any snapshot-derived value.
The core persistence mapper now also reconstructs the aggregate header/status and child collections from `TicketEntity` plus normalized child tables for normal reads, instead of deserializing the full `snapshotJson`.
`snapshotJson` therefore remains stored mainly as a conservative backup/audit artifact rather than as the primary read source.

## Read Models

The API already exposes stronger operational reads than the initial scaffold:

- paginated ticket list with operational summary fields
- per-ticket operational detail endpoint with economic header data, customer identity, structural counts, movement count, and mutation count
- reconciliation summary endpoint
- ticket aggregate retrieval

The list endpoint already draws on normalized persistence to provide summary-level operational data rather than relying only on the snapshot.
The per-ticket operational detail endpoint also reads directly from normalized persistence and root header state rather than rebuilding the full aggregate.
The paginated list now includes normalized `datosreferenciales` support fields as well (`comercio`, `nroSucursal`, `nroPv`, `vuelto`) plus `movimientoCount` and `mutationCount`, so operators can inspect ticket state economically and operationally without hydrating the full aggregate.
Both operational read models now also expose movement-derived economic breakdown from normalized persistence:

- `totalVentas`
- `totalPromociones`
- `totalPagos`
- `ventaMovimientoCount`
- `promocionMovimientoCount`
- `pagoMovimientoCount`

Test coverage now also includes balanced cross-flow scenarios that combine:

- `ITEM` promotions
- `PAGO` promotions
- `POSTPAGO`
- returned change / synthetic refund pagos

There is now also service-level end-to-end coverage for `create(...)` using real promotion catalogs and real promotion engines, including:

- catalog-driven `COMBO` item resolution
- catalog-driven `COMBO` payment resolution
- catalog-driven `CANTIDAD` item resolution
- catalog-driven `CANTIDAD` payment resolution
- catalog-driven `MAYORISTA` item resolution
- realistic cashier flow with overpayment and returned change
- recomputed totals after both promotion layers
- proportional payment distribution against the rebuilt net ticket state

This is intended to reduce regression risk in the most coupled reconciliation paths.
There is now also a direct mapper-level regression proving that a `Ticket` can be reconstructed from normalized persistence even when `snapshotJson` is stale or irrelevant.
There is now also SQL Server integration coverage for ordered movement loading by ticket and operational breakdown reconstruction from normalized movement rows.
The reconciliation suite now also includes explicit final edge cases for:

- valid reuse of a shared `CONSULTA` snapshot when aggregate quoted usage stays within limits
- valid `APPLY_PAYMENT_PROMOTION` after a synthetic negative `Pago` used only for returned change, without incorrectly treating that bookkeeping payment as a stale-consultation trigger

Build hygiene has also been improved so test execution no longer relies on Mockito self-attaching dynamically under JDK 25; the Maven test plugins now run with Mockito configured as a `-javaagent`.

## Intentionally Deferred Or Out Of Scope

These areas are still intentionally incomplete or deferred:

- full final normalized relational design for every nested concept
- security/authentication/authorization
- external integrations
- reporting / BI
- event-driven architecture

## Current Remaining Refinements

The most relevant remaining work against `model.md` is no longer missing core flows; it is refinement and hardening.

Priority areas are:

- deeper promotion heuristics and edge-case optimization, especially around `COMBO` and other ambiguous candidate sets
- further reconciliation hardening on extreme tickets with many payments, payment promotions, and `POSTPAGO` interactions
- continued reduction of snapshot usage where stable normalized persistence can replace it cleanly
- richer operational read models from normalized persistence when operations need more domain-focused summaries
- continued end-to-end and SQL Server coverage for domain-heavy scenarios rather than structural persistence only

## Status Summary

The current backend should be understood as:

- structurally aligned with the aggregate and ledger concepts of `model.md`
- materially implemented for several real promotion and payment flows
- conservative but serious in reconciliation and audit handling
- still conservative in some heuristics and persistence tradeoffs compared with the full theoretical model

In short:

- this is no longer a scaffold-only backend
- the remaining work is final refinement, not missing foundational architecture
