TiFactura · Release técnico — 20260808

Todo lo posterior al doc de reconciliación (PRs #73–#77): versión visible en cockpit y bot, /nc read-only, config de reconciliación en el yml, mensaje claro del 409 en el ABM, y el fix crítico de huérfanos R (pre-requisito para F1)
Suite 549 / 0 / 16 main 684af93 PRs #78 · #79 · #80 · #82 jar: usar qa-20260808c WSDL sin cambios

1 · Qué hay que aplicar

  1. Reemplazar el JARqa-20260808c (main 684af93).
    Incluye #78 + #80 + #82 y reemplaza a los "a" y "b" de hoy. Si tenés el "a" deployado, reemplazalo sí o sí: tiene el bug de huérfanos R (§4) — no prendas F1 con ese jar.
  2. Config — nada obligatorio. Recomendado: agregar al yml externo el bloque nc.reconciliacion (§3) y revisar el checklist §6.
  3. Migración — ninguna.

2 · Versión + fecha de compilación: cockpit y bot PR#78 · #82

Badge en el header del cockpit, junto a PRODUCCIÓN / PROD·monitoreo:

2.0.0-SNAPSHOT · build 08/08/2026 15:22

El jar registra su propia fecha de compilación (META-INF/build-info.properties, goal build-info del plugin de Spring Boot — sin dependencias nuevas). Sale también:

La hora se muestra en la zona del server (build UTC convertido). En dev sin package muestra dev. Se acabó el "¿qué versión corre acá?".

3 · Reconciliación manejable desde el yml externo PR#79

El bloque va en el application.yml del server (config externa) — sin NSSM ni rebuild:

nc: reconciliacion: identificar-enabled: ${NC_RECON_IDENTIFICAR:false} # F1: detecta + alerta Telegram emitir-enabled: ${NC_RECON_EMITIR:false} # F2: emite NC/ND (requiere F1) ventana-dias: ${NC_RECON_VENTANA_DIAS:35}

Precedencia: si la env var existe, gana; si no, vale el default del yml. Para prender F1 editás el default a ...:true} y reiniciás. Modo "solo avisar por Telegram" = F1 true + F2 false.

Además de los flags, el job necesita su fila en Tareas:
INSERT INTO Tareas (Id, Descripcion, Programacion, JobProcessor) VALUES ('NC_RECONCILIACION', 'Reconciliacion NC CAE-CAEA', '0 30 2 * * ?', 'NotaCreditoReconciliacionJob');
Insertar la fila es seguro: sin flags prendidos el job corre y no hace nada (fail-closed).

4 · FIX CRÍTICO: un CAE rechazado (R) NO es huérfano PR#80

Detectado con data real de prod (10 casos en 3 semanas): comparando el conteo de tickets duplicados (50) contra la detección de dobles (40), los 10 excluidos eran todos el mismo patrón: CAE rechazado por ARCA (R) + CAEA aprobada. Eso es la contingencia sana — ARCA rechazó el CAE, el POS facturó por CAEA, el único comprobante válido es el CAEA. No corresponde NC.

El bug que eso destapó

La rama de "huérfanos" del job (CRÍTICO-4) incluía los R además de los NULL. El problema: tras un rechazo, ARCA no consume el número → la venta siguiente lo reusa. Con F1 prendida el job habría consultado ARCA por ese número, recibido el comprobante aprobado de OTRA venta, pisado la fila R con un CAE ajeno (se persistía con F1 sola), y con F2 emitido una NC contra el comprobante equivocado — anulando fiscalmente una venta legítima.

El fix

Huérfano = resultado IS NULL únicamente (respuesta de ARCA perdida — la intención original de CRÍTICO-4). Una R persistida es respuesta definitiva: ni se consulta, ni se pisa, ni se emite. Test dedicado que fija la invariante con la fila R intacta.

Por esto el jar a usar es qa-20260808b: el "a" (y anteriores) tienen la rama vieja. No prender F1 sin este fix en ningún server con rechazos CAE en la ventana.

5 · Query de control (candidatos reales a NC)

Read-only, la misma lógica del job F1. Cada fila = CAE aprobado con doble CAEA, candidato a NC:

SELECT cae.id, cae.nroSuc, cae.nroPos, cae.nroTicketPos, cae.ptoVta AS pvcae, cae.comprobante, cae.comprobanteFecha FROM dbo.Trx cae WHERE cae.tipofacturacion='CAE' AND cae.version='V2' AND cae.resultado='A' AND cae.comprobanteFecha >= CONVERT(varchar(8), DATEADD(day,-35,GETDATE()), 112) AND EXISTS (SELECT 1 FROM dbo.Trx ca WHERE ca.tipofacturacion='CAEA' AND ca.nroTicketPos=cae.nroTicketPos AND ca.nroSuc=cae.nroSuc AND ca.nroPos=cae.nroPos AND ca.idTipoComprobante=cae.idTipoComprobante AND ca.comprobanteFecha=cae.comprobanteFecha) AND NOT EXISTS (SELECT 1 FROM dbo.Trx nc WHERE nc.tipofacturacion='NC' AND nc.trxoriginal=cae.id) ORDER BY cae.id;

Cómo leerla: NO incluye los CAE rechazados (R) — contingencia sana, sin NC. Los huérfanos (resultado IS NULL) los confirma solo el job contra ARCA (CRÍTICO-4). Necesita que las filas CAEA tengan el trío suc/pos/ticket (notificador nuevo).

La misma consulta, desde el bot: /nc PR#82

🧾 Candidatos a NC (dobles CAE+CAEA) · ventana 35d: 40 · s55/p1 tk1356 → CAE 140-126 (20260720) · s55/p2 tk1446 → CAE 141-447 (20260720) … (hasta 15) … y 25 más. ⚠️ Huérfanos por confirmar con ARCA (resultado NULL): 2 Consulta read-only: no se emitió nada. La emisión la controla el job (F1/F2).

Read-only puro: no ejecuta CRÍTICO-4, no consulta ARCA, no persiste — y funciona con el job deshabilitado o sin la fila en Tareas. Ideal para mirar antes de prender F1/F2.

6 · Checklist del yml externo de prod (revisión 08/08)

ÍtemQuéPrioridad
telegram.alert-check-msSon milisegundos: con 60 el chequeo corre cada 60 ms (~16 veces/seg). Debe ser 60000.🔴 corregir ya
cockpit.stats-window-daysSi falta, el default es 0 = sin límite → full scan del histórico en cada apertura. Poner 90.🟠
nc.reconciliacionBloque de §3 (apagado por default, visible y manejable).🟡
archivado-trxBloque del archivado (enabled/retencion/batch/max-filas), apagado por default.🟡
afip.tls-insecureEl valor true = trust-all (NO verifica); que el comentario no diga lo contrario. Con certs válidos de ARCA prod, false es lo deseable.🟡 decidir
email: + spring.mail:Solo si van a usar el envío de facturas (default del código: apagado).⚪ opcional
SecretosTodos por env var sin default (${DB_USER}, ${TELEGRAM_BOT_TOKEN}, …), nunca literales.🟡

Tras editar: reiniciar y verificar con el bloque CONFIG EFECTIVA AL ARRANQUE del log.

7 · Mapa de jobs en Tareas

JobProcessorQué hace¿Gated?Nota
TokenManagerJobTA de WSAA (renueva cada 5')legacy, ya presente
CaeaManagerJob (6 filas)obtención de CAEA por quincenalegacy, ya presente
ComprobantesEmitidosJobinforme batch de CAEA sin informar (resultado=null)noopcional si TODO el CAEA entra por el notificador REST (verificar: findstr saveTrx traffic.log sin resultados)
PvSinMovimientosJobinforma PV CAEA sin movimientos por quincenanosí va — nadie más lo hace (obligación del régimen)
NotaCreditoReconciliacionJobF1 identifica dobles + Telegram / F2 emite NC-NDsí (OFF)prender F1 recién con qa-20260808b
ArchivadoTrxJobarchiva Trx viejas a bak_sí (OFF)requiere add-bak-tables-Trx.sql antes de habilitar

8 · Runbook: prender el modo "avisar cuáles están para NC"

  1. Deployar qa-20260808c (pre-requisito §4). Desde ya podés espiar los candidatos con /nc en el bot (§5), sin activar nada.
  2. Verificar que las CAEA nuevas traen el trío (notificador nuevo): SELECT COUNT(*) FROM Trx WHERE tipofacturacion='CAEA' AND nroTicketPos IS NOT NULL > 0.
  3. Fila NC_RECONCILIACION en Tareas (§3).
  4. En el yml: identificar-enabled: ${NC_RECON_IDENTIFICAR:true} (F2 queda false) + reiniciar.
  5. Cada corrida: llega el ⚠️ por Telegram con la lista (suc/pos/ticket + CAE). Nada se emite.
  6. Cuando confíes en lo que ves: emitir-enabled → true y F2 emite el reverso (NC/ND) solo.

9 · ABM de PV: mensaje claro en el 409 PR#82

Al guardar/crear un PV que colisiona, el cockpit mostraba Error al guardar PV: HTTP 409 pelado. El backend ya mandaba el motivo en el body — ahora el front lo muestra:

Error: Ya existe una fila para sucursal 55 / POS 2 Error: El PV 33 (CAE) no existe en AFIP para este CUIT Error: El PV 40 (CAEA) está BLOQUEADO en AFIP

TiFactura · release técnico 20260808 · main 684af93 · suite 549/0/16 · PRs #78 (build-info) · #79 (yml) · #80 (fix huérfanos R) · #82 (bot versión + /nc + ABM 409) · jar qa-20260808c · anterior: reconciliacion-cae-caea-nc-nd.html (PRs #73–#77)