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 · Cómo queda marcada la fila de reverso (NC o ND) y por qué no se re-detecta

Cuando F2 emite el comprobante compensatorio sobre un CAE que también tuvo CAEA, la marca de la relación vive en la fila de reverso, NO en el CAE original. El CAE de venta queda intacto (resultado='A', su número) — no se le escribe nada. El vínculo es direccional: reverso → CAE (el hijo apunta al padre).

El reverso puede ser NC o ND — según el tipo de origen

El tipo que anula fiscalmente depende del comprobante original (MAPA_REVERSO, por tipoComprobante):

Factura → NC : 1→3 6→8 11→13 51→53 NC → ND : 3→2 8→7 13→12 53→52 ← el reverso es ND ND → NC : 2→3 7→8 12→13 52→53

Es decir: el reverso es una ND cuando el comprobante original es una NC (típicamente una NC que el POS emitió online, se quedó sin respuesta y se terminó facturando por CAEA). Anular esa NC de más es una ND — nunca una "NC de la NC" (eso sería re-cobrarle al cliente).

Clave para no confundirse: en la fila emitida, tipofacturacion='NC' es un MARCADOR de "esto es un reverso", no el tipo fiscal. El tipo fiscal real —NC (3/8/13/53) o ND (2/7/12/52)— va en tipoComprobante. Por eso hasta una ND queda tagueada tipofacturacion='NC', y un solo guard de dedup cubre las dos.

Las tres marcas que lleva la fila de reverso

Campo en la fila de reversoQué esPara qué sirve
trxoriginal = cae.idlink interno al id de la Trx del CAE originales lo que usa el dedup del job
tipofacturacion = 'NC'marca de "reverso de reconciliación" (el tipo fiscal real —NC o ND— va en tipoComprobante)un mismo guard cubre NC y ND
ComprobanteAsociado (CbtesAsoc)link fiscal que exige AFIP: tipo/pv/nro/fecha del CAE acreditadola relación del lado ARCA

Por qué el job no la vuelve a detectar

El finder (y la query de §5) cierra con un NOT EXISTS que descarta todo CAE que ya tenga un reverso apuntándolo. Recordá que el marcador tipofacturacion='NC' vale para NC y ND, así que este mismo guard cubre los dos casos:

AND NOT EXISTS ( SELECT 1 FROM Trx nc WHERE nc.tipofacturacion = 'NC' AND nc.trxoriginal = cae.id )

O sea: la existencia de la fila de reverso ES la señal de "ya reconciliado" — no hay un flag aparte en el CAE. En cuanto existe el reverso con trxoriginal = cae.id, el CAE deja de ser candidato.

A prueba de doble anulación: la fila de reverso (con trxoriginal ya seteado) se comitea ANTES de emitir a AFIP (persist-before-emit, tx REQUIRES_NEW). Si la emisión a ARCA o el commit posterior fallan, la fila ya quedó grabada → la corrida siguiente la ve por el NOT EXISTSnunca se emite un segundo reverso fiscal (NC o ND) contra el mismo CAE.
Consecuencia a tener en cuenta: como la marca vive solo en la fila de reverso, si alguien borra manualmente esa fila de la tabla Trx, el CAE vuelve a ser candidato y el sistema la re-emitiría. Para el job está perfecto; solo importa si hacés limpieza manual de Trx.

7 · 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.

8 · 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

9 · 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.

10 · 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 · §6 marca de relación NC↔CAE + dedup (sin cambio de código; comportamiento vigente de PR#76/#80) · anterior: reconciliacion-cae-caea-nc-nd.html (PRs #73–#77)