El agregado Ticket y su cómputo¶
El ticket es el agregado raíz del sistema: todo —itemización, promociones, envases, impuestos, pago y fiscalización— gira alrededor de él. Esta página describe su estructura, su ciclo de vida y, sobre todo, el orden exacto en que se computa. Es la página más importante de la referencia: si vas a tocar la lógica de negocio, leela entera.
El código es la fuente de verdad
Reproducimos fórmulas, nombres de clase y fixes tal como están en el código a la fecha. Algunas firmas exactas de método pueden cambiar; ante cualquier discrepancia, mandan las clases en src/main/java/com/tipre/autocompras/tickets/.
El agregado: Trx¶
El ticket se persiste como la entidad Trx (tickets/domain/Trx.java) en la base TsAutoCompra. Campos principales:
| Campo | Tipo | Rol |
|---|---|---|
id |
Long |
PK autogenerada. |
fechaCreacion / fechaModificacion |
LocalDateTime |
Apertura y última modificación. |
codComercio, nroSucursal, nroPos, codTerminal |
— | Identidad de origen. |
nroTicketPos |
int |
Número secuencial del ticket en la terminal. |
total |
BigDecimal |
Monto total. |
estado |
TrxEstado |
Estado actual (ver abajo). |
jsonticket |
TEXT |
El TicketDTO completo serializado a JSON. Acá vive el detalle (ítems, movimientos, promos, núcleo impositivo). |
idempotencyKey |
String(64) |
Clave de deduplicación (fix TSTK-006). |
errorMessage |
String |
Mensaje si terminó en error. |
El detalle vive en el JSON, no en columnas
Trx guarda los campos de cabecera en columnas y el cuerpo completo del ticket en jsonticket como un TicketDTO serializado. Esto mantiene el modelo relacional simple y deja el agregado rico en el JSON. Cuando el POS pide un ticket, recibe ese TicketDTO.
Estados del ticket: TrxEstado¶
stateDiagram-v2
[*] --> OPEN
OPEN --> PAGADO: pago aprobado
OPEN --> CANCELED_USER: cancela el cliente
OPEN --> CANCELED_INACTIVITY: timeout de inactividad
PAGADO --> CLOSE: CAE/CAEA inmediato
PAGADO --> VOUCHERPENDING: falla/timeout fiscal
VOUCHERPENDING --> CLOSE: reconciliador reintenta y autoriza
CLOSE --> ANULADO: anulación post-cierre
OPEN --> ERROR
| Estado | Significa |
|---|---|
OPEN |
Abierto; se agregan/quitan ítems. |
PAGADO |
Pago aprobado, en tránsito a cierre fiscal. |
VOUCHERPENDING |
Pagado pero el comprobante aún no fue autorizado por AFIP. Durable; lo resuelve el reconciliador. Ver Facturación fiscal. |
CLOSE |
Cerrado y autorizado fiscalmente (comprobante emitido). |
CANCELED_USER |
Cancelado por el cliente (desde OPEN). |
CANCELED_INACTIVITY |
Cancelado automáticamente por inactividad (desde OPEN). |
ERROR |
Terminó en error. |
ANULADO |
Anulado después del cierre. |
TEST |
Ticket de prueba. |
Regla de oro: el ticket no se cierra hasta que el pago esté APROBADO
Trx.estado nunca avanza a PAGADO/CLOSE mientras el PaymentAttempt no esté APROBADO. El dominio de pago manda sobre el de ticket. El detalle está en El ciclo de pago y fiscalización.
Itemización¶
El TicketDTO no es una lista plana de líneas: separa ítems (lo que el cliente ve) de movimientos (las anotaciones contables que componen el total). Esta separación es la que permite que una misma línea reciba una venta, un descuento de promo y un crédito de envase sin perder trazabilidad.
- Ítem (
ItemDTO): una línea del ticket. Tiene su artículo, cantidad, tipo (ItemTicketTipo: venta normal,ENVASE, etc.) y estado (ItemTicketEstado:VENTA,SALDADO,ANULADO). - Movimiento (
MovimientoDTO): una anotación sobre un ítem. Una venta es un movimiento; un descuento de promo es otro movimiento ligado al mismo ítem; un crédito de envase, otro. El total del ticket es la suma de los movimientos.
Operaciones (todas mutaciones REST, ver API REST):
| Operación | Qué hace |
|---|---|
openTicket |
Crea el Trx en OPEN. |
agregarArticulo |
Resuelve el EAN en catalogo, arma el ítem y su movimiento de venta, recalcula promos y total. |
removeItem |
Quita un ítem (o lo marca ANULADO) y recalcula. |
changeStatus |
Cambia el estado del ticket. |
validateTicket |
Valida el ticket antes de pagar. |
closeTicket |
Cierra (dispara fiscalización). |
Solo los ítems en estado VENTA se facturan
Al armar la solicitud fiscal, CaeService.resolveFacturableItems toma solo los ítems en VENTA. Los SALDADO (saldados por envase/crédito) y ANULADO no van al comprobante.
El orden de cálculo¶
Este es el corazón. Cuando se agrega o quita un artículo, el total se recompone siguiendo un orden fijo. Saltearse un paso o cambiarlo de lugar produce totales mal calculados.
graph TD
A["1. Resolver artículo (EAN → catalogo)"] --> B["2. Decodificar EAN especial<br/>(peso / precio embebido)"]
B --> C["3. Armar la línea: precio vigente × cantidad<br/>→ NucleoImpositivoDto por ítem"]
C --> D["4. Calcular promociones<br/>(MAYORISTA, BULTO, COMBO, CANTIDAD)"]
D --> E["5. Computar envases<br/>(consumo de vales, envases a cobrar)"]
E --> F["6. Sumar movimientos → total del ticket"]
F --> G["7. Al pagar/cerrar: promos MODO PAGO + fiscalización"]
- Resolución del artículo: el EAN escaneado se busca en
catalogo(cache Caffeine ~100k). Devuelve precio, alícuota (iTax), flags (bPeso,bRandomPrice,bEnvase), impuesto interno (fImpinterno). - Decode de EAN especial: si el EAN codifica peso o precio (productos pesables, random price, DUN de bulto), se extrae. Ver Decodificación de EAN.
- Armado de la línea: se calcula el
NucleoImpositivoDtodel ítem (neto + IVA + impuestos internos) a partir del precio vigente y la cantidad. Ver Núcleo impositivo. - Promociones: se invoca el módulo
promos. Primero precios MAYORISTA y BULTO (por ítem), luego COMBO/CANTIDAD/CUPONES. El beneficio se distribuye proporcionalmente entre las líneas. Ver Promociones. - Envases: se computa el consumo de vales contra los ítems de tipo
ENVASEy se determina cuántos envases hay que cobrar (cantEnvasesCobrar = max(0, cantEnvases − consumidos)). Ver Envases y vales. - Total: la suma de todos los movimientos da el
totaldel ticket. - Al pagar/cerrar: se recalculan las promos que dependen del medio de pago (MODO PAGO, ver Promociones) y se dispara la fiscalización.
Redondeo y escala¶
Todo el dinero usa BigDecimal con reglas consistentes (definidas en NucleoImpositivoDto):
- Escala de moneda: 2 decimales,
RoundingMode.HALF_UP. - Escala de cálculo intermedio: mayor (≈10 decimales) para no acumular error antes del redondeo final.
- En la distribución proporcional de descuentos, la última línea absorbe el residuo para que la suma de las porciones sea exactamente el total (evita el centavo perdido por truncamiento).
Nunca redondees a mano en pasos intermedios
El error clásico es redondear a 2 decimales en cada paso. Eso descuadra el ticket. Se calcula con escala alta y se redondea una sola vez al final. La aritmética vive en NucleoImpositivoDto justamente para centralizar esto.
El comprobante (voucher)¶
Cuando el ticket cierra y tiene CAE/CAEA, VoucherService genera el comprobante imprimible:
- Impresión térmica: ESC/POS vía
escpos-coffee. - PDF: vía OpenPDF (reemplazó iText 5 AGPL — plan 011).
- QR fiscal de AFIP: generado con ZXing a partir de la URL fiscal (
AfipUtil.getQrAfip). Ver Facturación fiscal. - Templates: gestionados por
TemplateController.
VoucherType distingue: TICKET, TICKET_PDF, ENVASE, ENVASE_TOMRA, CUPON, ANULACION_PAGO.
Dos issues conocidos en VoucherService
- Thread-safety (plan 007): un campo de config mutable compartido entre requests puede hacer que un thread pise la config de otro mientras imprime.
getFirst()sin guarda (plan 008): un.getFirst()sobre lista vacía tiraNoSuchElementExceptionopaca si falta config.
Idempotencia y casos especiales¶
idempotencyKey(TSTK-006): deduplica mutaciones reenviadas. La misma operación repetida no duplica el efecto.- Cierre por inactividad: la terminal tiene timers (
InactividadBloc); un ticket abandonado pasa aCANCELED_INACTIVITY. - Zona horaria: las fechas con efecto fiscal se calculan en
America/Argentina/Buenos_Aires, no en la zona de la JVM (fix TSTK-024). - Logging por ítem (plan 010):
calcularTotalTicketlogueaba 2× INFO por ítem (1800 líneas en un carrito de 30); se baja a DEBUG.
Por dónde seguir¶
- Decodificación de EAN — el paso 2.
- Núcleo impositivo — el paso 3, el modelo fiscal.
- Promociones — el paso 4.
- Envases y vales — el paso 5.
- Facturación fiscal — el paso 7.