TiFacturaOnline · Gateway de facturación electrónica AFIP

Versionado y guía de actualización

Registro de releases QA/PROD desde qa-20260825 hasta qa-20260908, y el anexo con los pasos de actualización acumulados.

Releases: 8 Suite: 615 → 894 (0 fallas · 43 skipped) Última release: qa-20260908 · main 4df2e96 Repo: tipre/TIFacturaOnlineNext

Resumen — novedades de qa-20260908

Un review adversarial externo (Codex/Astra) encontró seis defectos en el money-path (F01–F06). Se corrigieron de a uno, cada uno con tests de caracterización sobre la transacción real, review fresco de dos jueces y suite completa, y se cerraron además los temas que aparecieron durante las correcciones y en dos tandas de review holístico. Orden de trabajo: F01 → paquete de tests de paranoia → F05 → F02+F06 → F03 → F04 → composición. Todo sale en una sola release: un cierre parcial dejaba el sistema en un estado intermedio peor que el anterior.

Filas 'R' históricas. Las escritas por el código viejo ante fallos de transporte no se distinguen en la base de un rechazo genuino: el único juez posible es ARCA. Esta release incluye el modo incluir-rechazadas del barrido (OFF por default) para adjudicarlas; es una herramienta de una sola pasada y se apaga al terminar. Runbook en docs/qa-pendientes-activar.md.

Resumen — novedades de qa-20260907c / d / e

Historial de releases

qa-20260908main 4df2e96894/0/432026-09-08

Cierre del review adversarial del money-path (F01–F06)

  • F01 — transporte ≠ rechazo. 'R' solo con veredicto de ARCA; transporte, faults, Errors de request, 10016 y 'A' sin CAE quedan null = estado desconocido y los adjudica el barrido. Cierra de paso la doble anulación de NC por parpadeo de red.
  • F02 + F06 — el reintento de ticket V2 no renumera sobre un número que ARCA consumió ni adopta un CAE ajeno (relectura fresca bajo lock + guardas de titularidad).
  • F03 — el barrido no pisa un 'A' concurrente: cada escritura en transacción propia, bajo el mismo lock, con relectura pesimista y re-verificación. Ninguna llamada a ARCA con un lock tomado.
  • F04 — las NC del POS entran en la reconciliación CAE↔CAEA (IN ('CAE','NC') AND trxoriginal IS NULL). Requiere el índice replace-IX-Trx-v2-cae-fecha-con-nc.sql.
  • F05 — una sola fila de NC por original: el reverso rechazado se re-emite sobre la misma fila, con tope de reintentos, tope por corrida y ventana de 92 días.
  • CAEA: el informe nunca marca 'R'; la reparación consulta FECompConsultar antes de marcar 'A'; el registro del POS commitea la corrección de CbteFchHsGen antes de llamar a ARCA (rompe el loop de obs 1441).
  • Composición: applock con @LockOwner='Transaction', consulta a ARCA no mutante, jobs Quartz sin ejecución concurrente, guardas de titularidad compartidas.
  • Operación: motivo del rechazo V2 al POS, TrxErrorLog.referencia, bloque de flags en StartupConfigLogger, DeployYmlParityTest y ci-realdb.bat. Gate de Notas de Débito OFF por default.

Aplicar: scripts del DBA en orden (add-TrxErrorLog-mensajeCurado.sql antes del jar) + deploy/application.yml de esta release + swap de jar + activar el barrido Fase A y B (precondición dura, no opcional). Sin cambio de WSDL. Rollback: flags OFF y reinicio ANTES de tocar el jar.

qa-20260907e657/0/242026-09-08 11:49

Cockpit: el snapshot periódico agrega en SQL

  • Menos carga periódica sobre SQL — el job @Scheduled (cada 5 min) que persiste el KPI en CockpitStat traía todas las filas CAE V2 crudas y agregaba en Java. Ahora agrega en SQL con GROUP BY (igual que posStats()); mismos números. Rate configurable: COCKPIT_SNAPSHOT_RATE_MS (default 300000).
  • Índice opcional para el DBAadd-Trx-cockpit-posstats-index.sql (índice filtrado, SQL Server 2008+); vuelve el agregado index-only. No lo corre la app.

Aplicar: swap de jar. Sin DDL obligatorio.

qa-20260907d657/0/242026-09-07 22:33

Claridad de errores AFIP hacia el cliente / POS

  • Señal en vez de genérico — un faultstring inseguro (HTML, ORA-, XML inválido, HTTP 5xx) ya no colapsa a “ARCA no disponible”: se extrae la señal (AFIP error BD: ORA-01033, AFIP HTTP 503, …) más un ref: con el reqId del traffic.log.
  • Cap 180 → 80 caracteres (buffer del POS) con truncado exacto; los errores de mail también se devuelven curados.
  • Documentación — anexo “Respuestas de error al cliente” (catálogo de faults y resultado='R') y diagrama del flujo de email, en este mismo documento.

Aplicar: swap de jar. Sin DDL. Sin cambio de WSDL.

qa-20260907c650/0/242026-09-07 20:52

Barrido de numeración + KPI RG 5.616 + PDF dedup / 552

  • Barrido de numeración — recupera CAE/NC autorizados por ARCA con respuesta perdida (nunca emite NC); gateado OFF, corre antes del job de NC. (Plan 027)
  • KPI RG 5.616 — mide comprobantes sin condición IVA del receptor; solo medición.
  • Dedup PDF (−92 %/−95 %, sin pérdida) y guarda SMTP 5xx no reintentable.

Aplicar: swap de jar. KPI y PDF/email quedan activos solos; el barrido, inerte hasta cargar su fila en Tareas y prender los flags.

qa-20260907b625/0/242026-09-07 17:20

Libro de IVA completo + cockpit email / descarga

  • #119 Libro de IVA completoGET /api/libro-iva (X-Api-Key): todos los comprobantes autorizados del período (CAE + CAEA + NC/ND). Formatos arca (RG 4597), csv y json.
  • #120 Descarga desde el cockpit (filtros + botón) → /api/cockpit/libro-iva.
  • #118 Cockpit email: KPI “aprobados ARCA no enviados” y el botón Reenviar ahora pide el mail.

Aplicar: swap de jar. Sin DDL. Endpoints read-only.

qa-20260907623/0/242026-09-07 14:34

Email guard (solo aprobados ARCA) + BootUI 1.16

  • #117 Regla de oro del email — solo se envían comprobantes aprobados por ARCA (resultado='A' y cae != null); si no, ERROR terminal EMAIL_NO_APROBADO. Candado único de las 4 vías de envío.
  • #116 BootUI 1.14 → 1.16 (paneles Fault Tolerance y WebSockets). Trae OpenTelemetry al classpath, dejado inerte (management.tracing.enabled=false).

Aplicar: swap de jar. Sin DDL. Config: agrega management.tracing.enabled=false y logging.pattern.correlation="" (ver anexo).

qa-20260904622/0/242026-09-04 23:41

Ajustes de contingencia: detalle impositivo + formato Libro de IVA

  • #114 /api/ajustes-contingencia: detalle impositivo completo por comprobante (impuestos + alícuotas + tributos + datos del receptor). Aditivo.
  • #115 Parámetro ?formato=libro-iva — registros de ancho fijo del Libro de IVA Digital (RG 4597), verificados contra el PDF de AFIP.

Aplicar: swap de jar. Sin DDL. Endpoints read-only.

qa-20260825615/0/242026-08-25 19:47

Filtro cuit/nroSucursal + log limpio + flush AUTO

  • #112 /api/ajustes-contingencia acepta cuit y nroSucursal como filtros. Log más limpio (se vacía logging.pattern.correlation).
  • #111/#97 Se acepta flush AUTO como invariante real; se quita el setFlushMode(COMMIT) NO-OP. Sin cambio de comportamiento.

Aplicar: swap de jar. Sin DDL. Punto de partida de esta guía.

APIs de integración

Todas read-only, no tocan el money-path de emisión. Fechas en YYYYMMDD. Reemplazá HOST y <api-key> por los de tu entorno.

A. Ajustes de contingencia — tripla CAE + NC/ND + CAEA

Los comprobantes que el gateway emitió por su cuenta y el BackOffice necesita para cerrar su Libro de IVA: el CAE original (duplicado), la NC/ND que lo anula, y el CAEA que es la venta real impresa. El neteo cierra (CAE + CAEA − NC = CAEA); el valor es de registro. Cada comprobante trae su detalle impositivo completo.

GET /api/ajustes-contingencia

Auth: header X-Api-Key: <api-key> · ventana máx. 92 días.

Parámetros

ParámetroReq.Descripción
desdeInicio del período (YYYYMMDD).
hastaFin del período (YYYYMMDD).
cuitFiltra por comercio (con o sin guiones; se normaliza a dígitos).
nroSucursalFiltra por sucursal.
enteFiltra por id interno de EnteFacturador (compatibilidad).
formatojson (default) · libro-iva (registros ancho fijo RG 4597).

Ejemplo — request

curl -H "X-Api-Key: <api-key>" \
  "https://HOST/api/ajustes-contingencia?desde=20260901&hasta=20260930&cuit=30712434763"

Ejemplo — respuesta (json, un ajuste)

[
  {
    "ticket": { "suc": 1, "pos": 2, "nroTicket": "T-0001" },
    "caeOriginal": {
      "ptoVta": 559, "nro": 301882, "fecha": "20260901",
      "cae": "75130212345601", "tipoComprobante": 6, "importeTotal": 1234.56,
      "docTipo": 99, "docNro": 0, "condicionIvaReceptorId": 5,
      "impuestos": {
        "netoGravado": 1020.30, "noGravado": 0.0, "exento": 0.0,
        "iva": 214.26, "tributos": 0.0, "total": 1234.56,
        "ivaAlicuotas": [ { "id": 5, "baseImponible": 1020.30, "importe": 214.26 } ],
        "tributosDetalle": []
      }
    },
    "ncAnulacion": { "ptoVta": 559, "nro": 45, "fecha": "20260902", "cae": "75130298765401",
                     "tipoComprobante": 8, "importeTotal": 1234.56, "docTipo": 99, "docNro": 0,
                     "condicionIvaReceptorId": 5, "impuestos": { "…": "idem estructura" } },
    "caeaImpreso": { "ptoVta": 560, "nro": 12000, "caea": "26123456789012",
                     "fchTope": "20260910", "estadoInforme": "INFORMADO",
                     "impuestos": { "…": "idem estructura" } },
    "motivo": "doble facturación CAE+CAEA de contingencia",
    "crossPeriodo": false,
    "estado": "CERRADO"
  }
]

Ids AFIP: tipoComprobante 6=Fac B, 8=NC B (1/3=A, 11/13=C, 51/53=M) · alícuota IVA 3=0%, 4=10,5%, 5=21%, 6=27%, 8=5%, 9=2,5% · docTipo 80=CUIT, 96=DNI, 99=Cons.Final.

Opción ?formato=libro-iva

Mismos filtros; devuelve los registros de ancho fijo del Libro de IVA Digital (RG 4597):

curl -H "X-Api-Key: <api-key>" \
  "https://HOST/api/ajustes-contingencia?desde=20260901&hasta=20260930&formato=libro-iva"

→ { "ventasCbte":      [ "…registro de 266 caracteres por comprobante…" ],
    "ventasAlicuotas": [ "…registro de 62 caracteres por alícuota…" ] }

B. Libro de IVA completo — todos los comprobantes del período

A diferencia de (A), devuelve todos los comprobantes autorizados por AFIP (CAE online + CAEA offline + NC/ND), no solo la tripla de contingencia. Fuente: la tabla Trx (canal único).

GET /api/libro-iva

Auth: header X-Api-Key: <api-key> · ventana máx. 366 días.

Parámetros

ParámetroReq.Descripción
desdeInicio del período (YYYYMMDD).
hastaFin del período (YYYYMMDD).
cuitFiltra por comercio (con o sin guiones).
nroSucursalFiltra por sucursal.
enteFiltra por id interno de EnteFacturador.
formatoVer opciones abajo.

Opciones de formato

  • arca (default) — JSON con las líneas de ancho fijo RG 4597 (ventasCbte 266 / ventasAlicuotas 62).
  • json — un objeto estructurado por comprobante (ver ejemplo).
  • csv — adjunto .zip con ventas_cbte.csv + ventas_alicuotas.csv.

Ejemplo — formato=json

curl -H "X-Api-Key: <api-key>" \
  "https://HOST/api/libro-iva?desde=20260901&hasta=20260930&formato=json"

→ [
    {
      "fecha": "20260901", "tipoComprobante": 6, "ptoVta": 559, "nroComprobante": 301882,
      "tipofacturacion": "CAE", "cae": "75130212345601", "caea": null,
      "docTipo": 99, "docNro": 0, "razonSocial": "Consumidor Final",
      "importeNeto": 1020.30, "importeNoGravado": 0.0, "importeExento": 0.0,
      "importeIva": 214.26, "importeTributos": 0.0, "importeTotal": 1234.56,
      "iva": [ { "id": 5, "baseImponible": 1020.30, "importe": 214.26 } ]
    }
  ]

Ejemplo — formato=csv (baja un .zip)

curl -H "X-Api-Key: <api-key>" -OJ \
  "https://HOST/api/libro-iva?desde=20260901&hasta=20260930&formato=csv"
# → libro-iva-20260901-20260930.zip  (ventas_cbte.csv + ventas_alicuotas.csv)

C. Descarga del Libro de IVA desde el cockpit

Mismo contenido que (B) pero con la auth normal del cockpit (sesión), sin la X-Api-Key. Siempre devuelve un archivo descargable. Es lo que usa el botón “Descargar” de la sección Libro de IVA (ventas).

GET /api/cockpit/libro-iva

Auth: sesión del cockpit (no requiere X-Api-Key ni el secret del ABM).

Parámetros

Iguales a (B): desde, hasta (req.), cuit, nroSucursal, ente, formato.

  • arca / csv → descarga .zip.
  • json → descarga .json.

Anexo — Pasos de actualización desde qa-20260825

Actualización acumulada qa-20260825 → qa-20260908. Hasta qa-20260907e todas las releases eran swap de jar sin DDL. Con qa-20260908 el orden cambia: hay un script del DBA que bloquea al jar y hay que correrlo antes, el resto son índices aditivos, y la activación del barrido de numeración pasa a ser precondición dura. Sigue sin haber ningún cambio de WSDL y la app no ejecuta DDL.

  1. SQL a correr (DBA, en este orden)

    Todos los scripts están en src/main/resources/db/migration/. El primero bloquea al jar; los otros cuatro son aditivos y pueden correrse con el jar ya arriba (conviene igual una ventana de bajo tráfico por el costo de construir cada índice).

    • 1. add-TrxErrorLog-mensajeCurado.sql — ANTES de desplegar el jar (bloqueante). El jar escribe esa columna al curar el error que ve el POS; sin ella el INSERT del log de errores falla.
    • 2. add-EntesFacturadores-cuit-unique.sql — sostiene el contrato POS-por-cuit. Requisito: sin cuits duplicados (el script trae la query de chequeo).
    • 3. add-Trx-hot-query-indexes.sql.
    • 4. replace-IX-Trx-v2-cae-fecha-con-nc.sqlobligatorio con este jar (F04): supersede a IX_Trx_v2_cae_fecha y sin él la consulta de reconciliación pasa a escanear el clustered de Trx.
    • 5. add-Trx-cockpit-posstats-index.sql — apoyo del snapshot del cockpit y de posStats. Ya no es opcional.

    Los cuatro índices los verifica por nombre RealDbIndexParityTest (-Drealdb=true): mientras falte alguno esa clase queda roja nombrándolo, así que el checklist es ejecutable. Los INSERT en Tareas (barrido y NC) van recién al activar los jobs, no acá.

  2. Configuración (application.yml / variables de entorno)

    Usar el deploy/application.yml de esta release. En producción el arranque usa --spring.config.location, que reemplaza al yml embebido en el jar: una clave que falte ahí no toma el default del jar, y su variable de entorno queda inerte. Claves nuevas de esta release (todas OFF o conservadoras por default):

    BARRIDO_NUM_INCLUIR_RECHAZADAS       # backfill de las 'R' históricas (una pasada)
    BARRIDO_NUM_RECHAZADAS_HASTA_DIAS    # cota superior de la rebanada de 'R'
    BARRIDO_NUM_MAX_RECUPERACIONES_R     # tope de escrituras R -> 'A' por corrida
    BARRIDO_NUM_AVISO_HUERFANOS_MS       # aviso mientras el barrido esté apagado
    NC_RECON_EMITIR_ND                   # gate de Notas de Débito (false)
    NC_RECON_VENTANA_DIAS_REVERSOS       # ventana de re-emisión de reversos 'R' (92)
    ARCHIVADO_TRX_*                      # bloque de archivado recurrente de Trx

    Si mantenés tu YAML externo anterior (de qa-20260825), agregá además estas dos claves que entraron con BootUI 1.16 (qa-20260907):

    management:
      tracing:
        enabled: false          # BootUI 1.16 trae OpenTelemetry; lo dejamos inerte
    logging:
      pattern:
        correlation: ""         # evita el bloque [ ] vacío en cada línea de log

    DeployYmlParityTest exige que toda clave operativa del yml embebido exista en deploy/application.yml con la misma variable y el mismo default, así que este desfasaje no se repite.

  3. Swap del jar

    Descargar el asset tifactura-spring-20260908.jar de la release y renombrarlo a tifactura-spring.jar.

    nssm stop TipreTiFactura
    copy /y D:\...\tifactura-spring.jar D:\...\backup\tifactura-spring.PREV-<fecha>.jar
    :: reemplazar el jar por el nuevo
    nssm start TipreTiFactura

    Al arrancar, leer el bloque “CONFIG EFECTIVA AL ARRANQUE” del log — incluye ahora la sección “JOBS / FLAGS OPERATIVOS” (barrido, reconciliación, reparación CAEA, archivado). Confirmar que los flags leídos son los esperados antes de seguir.

  4. Activar el barrido de numeración (precondición dura de qa-20260908, no opcional)

    Hasta qa-20260907e era opcional. Con F01, el barrido es el único mecanismo que resuelve las filas que quedan en resultado=null (CAE y NC) cuando ARCA no dio veredicto; sin él se acumulan y las NC afectadas quedan trabadas. Mientras esté apagado y haya huérfanos, el jar avisa (WARN + Telegram, máximo 1 por día). Se activa en orden:

    a. Correr el INSERT en Tareas (una vez). El script está en el repo o dentro del jar:

    jar xf tifactura-spring.jar BOOT-INF/classes/db/migration/schedule-barrido-numeracion-job.sql

    b. La sección de config es numeracion.barrido en application.yml — así viene por default (cada clave enlazada a una variable de entorno con su valor por default):

    numeracion:
      barrido:
        detectar-enabled:  ${BARRIDO_NUM_DETECTAR:false}     # Fase A: detecta + alerta (solo lectura)
        recuperar-enabled: ${BARRIDO_NUM_RECUPERAR:false}    # Fase B: recupera (escribe) — requiere Fase A
        ventana-dias:      ${BARRIDO_NUM_VENTANA_DIAS:35}    # días hacia atrás por comprobanteFecha
        max-por-corrida:   ${BARRIDO_NUM_MAX:200}            # tope de consultas a ARCA por corrida

    Cada clave se puede prender de dos formas equivalentes (elegí una):

    • Por variable de entorno (recomendado, sin editar el yml): BARRIDO_NUM_DETECTAR=true, luego BARRIDO_NUM_RECUPERAR=true.
    • Editando el yml: poné detectar-enabled: true y luego recuperar-enabled: true en ese bloque.

    c. Secuencia: primero Fase A (detectar-enabled=true, recuperar en false) → reiniciar → mirar la tarjeta “Barrido de numeración” del cockpit y las alertas unos días. Después Fase B (recuperar-enabled=true) → reiniciar.

    d. Backfill de las 'R' históricas (aparte y posterior): BARRIDO_NUM_INCLUIR_RECHAZADAS=true adjudica contra ARCA las 'R' que el código viejo escribió ante fallos de transporte. Es una herramienta de una sola pasada —prender, leer el resumen, recuperar una rebanada con BARRIDO_NUM_RECHAZADAS_HASTA_DIAS, apagar—, con su propio tope de escrituras (BARRIDO_NUM_MAX_RECUPERACIONES_R, default 20) y un WARN por corrida mientras esté prendida. Runbook completo en docs/qa-pendientes-activar.md.

  5. Activar la reconciliación de notas de crédito (opcional, después del barrido)

    a. INSERT en Tareas con schedule-nc-job.sql (02:30, después del barrido).

    b. F1 primero (NC_RECON_IDENTIFICAR=true): detecta y alerta, no emite. Con F04 hay que esperar más candidatos que antes — las NC del POS doble-facturadas ya existían y no se estaban contando.

    c. F2 después (NC_RECON_EMITIR=true), cuando los números de F1 cierren. NC_RECON_EMITIR_ND queda en false (decisión del titular: la reconciliación no emite Notas de Débito sola). Antes de prender F2, contar los reversos 'R' más viejos que NC_RECON_VENTANA_DIAS_REVERSOS (92 días): esos no se re-emiten solos y hay que mirarlos a mano.

    d. Antes de confiar en la reparación CAEA: smoke contra homologación de FECompConsultar sobre un comprobante informado con CAEA, confirmando que CodAutorizacion sea el CAEA y EmisionTipo diga CAEA. No se puede validar contra el mock.

  6. Verificación

    • La app levanta sin errores de configuración y el bloque “CONFIG EFECTIVA AL ARRANQUE” muestra los flags esperados en “JOBS / FLAGS OPERATIVOS”.
    • Cambio de KPI esperado el día 1 (no es un bug): baja el conteo de rechazados / conError y sube el de pendientes. Los fallos de transporte pasaron a contarse como estado desconocido, que es donde corresponden.
    • En el cockpit aparecen las tarjetas “Barrido de numeración” y “Condición IVA del receptor (RG 5.616)”.
    • GET /api/cockpit/barrido-numeracion y GET /api/cockpit/condicion-iva-receptor devuelven JSON.
    • Un rechazo de ARCA en V2 llega al POS con el código y el motivo (no el genérico “Error al emitir CAE en AFIP”).
    • ci-realdb.bat contra la base real: se esperan 5 rojos ambientales conocidos (4 del cockpit por falta de certificado y el gate de paridad de índices mientras falte el índice de F04).
  7. Rollback (el orden no es el intuitivo)

    1. Primero los flags, y reiniciar. BARRIDO_NUM_DETECTAR, BARRIDO_NUM_RECUPERAR, BARRIDO_NUM_INCLUIR_RECHAZADAS, NC_RECON_IDENTIFICAR, NC_RECON_EMITIR, NC_RECON_EMITIR_ND en false. Los jobs quedan agendados pero no escriben ni llaman a ARCA; en la mayoría de los casos el rollback termina acá y el jar se queda.

    2. Recién después, si de verdad hace falta, revertir el jar. El jar viejo no tiene F02/F03 y su barrido escribe sin re-verificar la clasificación contra el estado fresco de la fila: ponerlo a correr contra la población de huérfanos —más grande— que deja esta release es la peor combinación posible. Con los flags ya apagados entra inerte y el orden deja de importar.

    3. El DDL no se revierte. Las migraciones son aditivas y el jar viejo convive con ellas: el filtro del índice nuevo es un superconjunto del anterior. Los comprobantes ya recuperados quedan (son válidos).

La app nunca ejecuta DDL. Regla del proyecto: hbm2ddl=none, el esquema lo administra el cliente. Los .sql de db/migration/ los aplica el DBA. Novedad de qa-20260908: uno de ellos (add-TrxErrorLog-mensajeCurado.sql) bloquea al jar y va antes del deploy; el resto son índices aditivos.

Anexo — Respuestas de error al cliente

Catálogo de las respuestas con error que recibe el cliente (SOAP faults + Response con resultado=R), el faultstring de cada caso (CAE/AFIP + mail, con el fix de claridad aplicado) y el diagrama del flujo de envío de email. Documento self-contained; también suelto en docs/errores-respuesta-cliente.html.