ivafix · saneamiento fiscal
tickets POS · JSON en SQL Server

Dos controles para dejar el ticket cerrado en cero.

IVAFix recorre los comprobantes de POS guardados como JSON, corrige el IVA 10,5% que quedó calculado al 11% y completa los pagos QR aprobados que nunca se asentaron — siempre sin tocar lo cobrado y respaldando el original antes de escribir.

artifactId com.tipre.tools:ivafix versión 1.1.0 stack Java 17 · Jackson · SQL Server · Arrow.js
la invariante que ambos controles preservan
Σ venta+29.049,00
+
promoción−3.980,00
+
Σ pago−25.069,00
= 0,00
El ledger de movimientos siempre vuelve a cero. Si una corrección lo movería, el portón la frena y no se escribe.
01 — qué resuelve

Un binario, dos saneamientos

El mismo ticket se guarda como un documento JSON en la columna jsonticket de una tabla de SQL Server (por defecto trx). IVAFix lo lee, diagnostica y —solo si se lo pedís— escribe la corrección. El escaneo es siempre dry-run.

control iva

Corregir el IVA 10,5%

Detecta tickets donde el IVA reducido se calculó al 11% y redistribuye la diferencia liberada dentro del mismo ticket, con una línea de ajuste, sin cambiar el total cobrado.

control pago

Completar el pago faltante

Encuentra tickets QR (ROUTERQR / Mercado Pago) aprobados que quedaron sin sección de PAGO y la arma: agrega los movimientos y deja el ledger balanceado.

Principio rector. El comprobante ya se emitió y se cobró: lo cobrado no se toca. En IVA, la diferencia se reabsorbe dentro del ticket; en PAGO, el asiento que se sintetiza es el espejo del neto a pagar. En los dos casos, el objetivo es que la suma de movimientos vuelva a cero.
02 — control iva

El 10,5% que se volvió 11%

La alícuota reducida en Argentina es 10,5%. En algún punto, 10.5 se truncó a 11 y el IVA se calculó como base × 0,11. El 21% no está afectado: ya es entero.

ConceptoCálculo (neto 1447,96)IVA
Correcto (10,5%)round(1447,96 × 0,105)152,04
Como quedó (11%)round(1447,96 × 0,11)159,28
Diferencia liberada159,28 − 152,047,24

Qué hace con esos 7,24

Como no se puede bajar el total, la diferencia se reinyecta como una línea de ajuste en el mismo ticket — del mismo producto (default) o un ítem exento — y se reconcilian los movimientos. El total queda idéntico; el IVA, bien discriminado.

ÍTEM · IVA 11% IVA 159,28 bruto 1607,24 recalcula ÍTEM · IVA 10,5% IVA 152,04 bruto 1600,00 Δ 7,24 LÍNEA DE AJUSTE + 7,24 total intacto ledger Δ ≈ 0
corrección del ítem → diferencia liberada → ajuste que mantiene el total y deja el ledger neutral

El portón: "no cuadra"

Después de corregir en memoria, el motor valida dos invariantes: que la suma de componentes dé el total y que la corrección no mueva el ledger. Los PAGO reales traen importes de alta precisión (≈8 decimales) y las VENTA van a 2, así que el ledger original casi nunca es 0 exacto — por eso se valida el cambio, no el cero absoluto (tolerancia 0,005). Si al reducir una VENTA su PAGO no se empareja 1:1 (pago consolidado, anulaciones, promos), el Δ salta y el ticket se marca error: validation y no se escribe.

03 — control pago

Cobró, pero el pago no se asentó

En tickets QR a veces el cobro se aprueba pero la sección de PAGO no se graba. Quedan VENTA sin PAGO, el ledger abierto, montoPagado y ordenDePago.monto en 0, pero con "estadoPago":"PAGO_APROBADO" y la orden en PAGADA.

La regla del pago

LÍNEA CON 20% OFF — el pago espeja el NETO, no el bruto VENTA (SET) +19.900,00 PROMOCIÓN → SET −3.980,00 neto a pagar PAGO (SET) −15.920,00 = 19.900 − 3.980 LEDGER = 0 montoPagado = 25.069 (neto a pagar) · NO 29.049 (bruto) los demás ítems (sin promo) llevan un PAGO espejo 1:1
ticket de referencia con promoción: VENTA + PROMOCIÓN ⇒ PAGO del neto, ledger en cero
Lo que no se reconstruye. Los identificadores de Mercado Pago — paymentId, orderid, referencia — vienen de la respuesta del procesador y no están en el ticket, así que no se regeneran; el voucher impreso tampoco. Lo que sí queda correcto es el asiento de movimientos, el ledger en cero y los montos.
Verificado. Contra un ticket QR OK con promo, al quitar su PAGO y volver a completar, los movimientos quedan idénticos a los originales; un ticket que había quedado descuadrado queda balanceado; y un caso sin promo se completa 1:1.
04 — interfaz

El cockpit, en dos modos

Una interfaz web local (solo 127.0.0.1) para conectar, elegir el control, escanear en dry-run, inspeccionar cada ticket y aplicar selectivamente. Esta es una reconstrucción fiel de la pantalla real.

autoservicio · cockpitAutoServicio Ticket fixer tool
tipo de control
Control IVA
Control PAGO
conexión
172.17.11.210 : 1433
AutoCompra
Usuario SQL
Windows
estado del ticket
☑ CLOSE  ☑ ANULADO
Escanear (dry-run)
Aplicar (marcá filas)
17
Leídos
0
Con IVA mal
17
Falta pago
0
Completados
0
Errores
trx.idTrxResultadoÍtemsTotal
190
{ } ver JSON
CLOSE a completar 8 venta(s) → +8 pago(s)
origenId 2825472
18.071,72
772
{ } ver JSON
CLOSE a completar 10 venta(s) → +10 pago(s)
origenId 2842377
46.236,00
181
{ } ver JSON
ANULADO a completar 1 venta(s) → +1 pago(s)
origenId 2824788
50,00

Los gauges y los estados

Dos indicadores de problema fijos — Con IVA mal (modo IVA) y Falta pago (modo PAGO) — más Leídos, Completados y Errores. En la grilla, cada fila trae su estado y el botón { } ver JSON, que abre el documento completo con toggle original / resultado.

Control IVA
a corregir corregido error: validation
Control PAGO
a completar completado no aprobado no completable

En modo PAGO, los QR aprobados salen a completar (seleccionables); los no aprobados salen no aprobado (solo informan, no se tocan).

05 — operación

Compilar y correr

Fat-jar autocontenido (Jackson + mssql-jdbc vía shade). Requiere JDK 17+ y Maven.

# compilar
cd ivafix-tool
mvn clean package        # → target/ivafix.jar

# cockpit (recomendado; única vía para Control PAGO)
java -jar target/ivafix.jar --ui

# CLI del Control IVA — dry-run y luego aplicar
java -jar ivafix.jar --server SRV --db MiBase --user sa --pass *** 
java -jar ivafix.jar --server SRV --db MiBase --user sa --pass *** --apply

Al arrancar con --ui imprime una URL con token (http://127.0.0.1:8742/?t=…). Si refrescás, usá Ctrl+F5. Sin --apply en CLI, nunca escribe.

La API local

EndpointQué hace
/api/scanRecorre y reporta (dry-run). El campo control = iva | pago elige el motor; en pago pre-filtra en SQL los candidatos.
/api/applyAplica el control activo a los ids seleccionados, en transacción, con respaldo previo.
/api/ticketDevuelve el JSON completo (original + resultado) para el visor.
/api/profilesLista / guarda / borra perfiles de conexión (archivo local).
06 — resguardos

Antes de escribir, respalda

Al aplicar (y solo entonces), cada registro se respalda y se actualiza en transacción; si algo falla, rollback de ese registro.

  1. Crea (si no existe) trx_jsonticket_bkp con trx_id, jsonticket_original y un hash SHA-256.
  2. Por ticket: INSERT del original, UPDATE con el JSON resultante, commit.
  3. El motor revalida cada registro en el server antes de tocarlo (doble portón).

Volver atrás

UPDATE t SET t.jsonticket = b.jsonticket_original
FROM trx t JOIN trx_jsonticket_bkp b ON b.trx_id = t.id
WHERE t.id = <id>;
Comparar contra el JSON crudo. DBeaver/SSMS alfabetizan las claves y hacen parecer que la herramienta desordena. IVAFix no reordena: reescribe valores y agrega ítems/movimientos al final. Compará siempre contra el SELECT jsonticket crudo.
El servidor escucha solo en 127.0.0.1, con token aleatorio por sesión. El máximo por escaneo es 2000 filas.