TiFactura · Notificador AFIP — estado 2026-08-27

Seis sistemas. Un servicio. Cero código por cliente.

Lo que eran 6 proyectos casi idénticos mantenidos a mano es hoy un solo jar donde cada instalación se declara con configuración explícita: una base, cuatro flags de impuestos y dos quirks. Un cambio regulatorio se hace una vez. Un cliente nuevo no toca código. Activar un impuesto es una línea de YAML.

build v20260827 goldens byte-a-byte de los 6 clientes 4 revisores: Judgment Day ×2 rondas · review 8 ángulos · E2E vivo notificaciones reales contra Manager + ARCA homologación

01 — Así quedó hoy

El resumen picante

6 → 1
repos fiscales → un solo servicio/jar
−5.050
líneas de código por cliente eliminadas
2 × 4 × 2
bases × flags de impuestos × quirks = todos los clientes
1 flag
activar/desactivar un impuesto a una instalación
Y no es fe: es evidencia. Los 6 clientes reproducen su comportamiento original byte a byte (goldens de caracterización), el refactor pasó un Judgment Day adversarial (2 jueces ciegos × 2 rondas, APPROVE) y un code review de 8 ángulos, y el E2E disparó notificaciones reales que atravesaron Manager → WSAA → ARCA homologación, con los importes llegando EXACTOS al otro sistema (Anexo C).

02 — La configuración

El YAML, bien documentado

Un archivo por instalación, fuera del jar (config\application-prod.yml, al lado del jar — run.bat lo crea del template en el primer arranque). Esto es TODO lo que define una instalación:

# ============ QUÉ ES ESTA INSTALACIÓN (explícito — la única forma) ============
tifactura:
  label: "ALMACOR"          # nombre para el cockpit (opcional, recomendado)
  base: TipreRetail          # REQUERIDO: TipreRetail (tabla de_tresu) | TipreSucursales (detresu).
                             # Define nombres de columnas y el filtro flagcontrol de Sucursales.
  impuestos:                 # REQUERIDOS los 4, en SI/NO. Falta uno → NO ARRANCA.
    iibb: SI                 # Ingresos Brutos (código ARCA 2)
    comind: SI               # Comercio e Industria (código ARCA 3)
    percepcion-iva: NO       # Percepción IVA 21/10.5 (viaja como ARCA 99 + descripción)
    impuestos-internos: SI   # Impuesto Interno (código ARCA 4)
  quirks:                    # opcionales (default NO) — el "caso TIZIANO", explícito
    total-desde-fprecio-total: NO  # importeTotal desde la columna cruda fprecioTotal (solo Retail)
    registra-mensaje-error: NO     # escribe/limpia cMensajeError ante fallo/éxito (solo Retail)
  cuit: "30xxxxxxxxx"      # CUIT del ente (recomendado): el Manager resuelve el comercio por CUIT (v2)

# ============ INFRAESTRUCTURA ============
server:
  port: 8080                # puerto del cockpit/doc/status — cambialo si está ocupado
spring:
  datasource:                # la base del cliente (coherente con tifactura.base)
    url: jdbc:sqlserver://HOST:1433;databaseName=TipreRetail
    username: usuario
    password: ********       # SOLO acá — nunca dentro del jar ni en el repo (config\ está gitignored)

# ============ OPERACIÓN ============
notify.to.afip.cron.scheduler: "0 0/5 * * * ?"  # cuándo corre el job (acá: cada 5 min)
tifacturaonlinemanager.service.url: http://HOST:8087/TiFacturaOnlineManager/resource/rest/trxWS
process.max.results: 50     # tope de comprobantes por punto de venta por corrida (≥1 o no arranca)

# ============ IDS FISCALES (códigos ARCA — viajan tal cual al XML) ============
id:
  ente.facturador: 1        # id interno del comercio en el Manager (NO es código ARCA)
  tributo:
    ing.bruto: 2            # ARCA 2 = Impuestos provinciales — VERIFICADO EN VIVO (Anexo B)
    comind: 3               # ARCA 3 = Impuestos municipales — VERIFICADO EN VIVO
    impinterno: 4           # ARCA 4 = Impuestos Internos — VERIFICADO EN VIVO (no usar 10)
    perciva: 99             # ARCA 99 = "Otro" + Desc — así viaja la Percepción de IVA
  iva: { "0": 3, "105": 4, "21": 5 }   # alícuotas ARCA — comillas OBLIGATORIAS
logging.file.name: ./log/tifactura.log
Reglas (todas fail-fast: preferimos no arrancar a emitir mal)
SituaciónQué pasa
base ausente o inválidaNO ARRANCA — mensaje claro con los dos valores válidos.
Falta cualquiera de los 4 flags de impuestos, o un valor no es SI/NONO ARRANCA — ningún impuesto queda apagado por accidente.
Quirk de Retail sobre base SucursalesNO ARRANCA — combinación físicamente imposible (esas columnas no existen en detresu).
process.max.results ≤ 0NO ARRANCA la corrida — antes era un "procesa cero para siempre" silencioso.
Flag en NOEl impuesto jamás se emite y sus columnas ni se consultan — inmune a columnas ausentes por instalación.
Flag en SISe emite cuando el importe de la fila ≠ 0 — el dato decide caso a caso.

El template real (config-template\application-prod.yml, también en el Release) trae todo esto comentado + la tabla oficial ARCA completa + el sello de verificación en vivo. Referencia clave-por-clave en el manual /doc.

03 — La matriz de referencia

Cada cliente histórico, en una fila (para copiar)

No hay presets en el código — esta matriz es conocimiento, no mecanismo: al instalar un cliente, copiás su fila al YAML.

instalación → base + flags + quirks
ClienteBase · tablaiibbcomindpercIVAimpInternosQuirks / lógica particular
DINOTipreRetail · de_tresuSISISINOReferencia; único solo-percepción (alíc. 3% / 1.5%).
ALMACORTipreRetail · de_tresuSISINOSIImp. interno a monto fijo (base 0, alícuota 0).
BECERRATipreRetail · de_tresuSISINOSIIgual que ALMACOR.
TIZIANOTipreRetail · de_tresuSISINOSIAmbos quirks SI: total desde fprecioTotal + registra cMensajeError.
CARACOLTipreSucursales · detresuSISISISIEl único con ambos. Filtro flagcontrol>0. NC/ND sin fecha asociada (preservado).
CYRETipreSucursales · detresuSISINOSIComo CARACOL sin percepción.

04 — El sello

Por qué podés confiar en esto

Pendientes honestos (plan §9): verificar CARACOL contra una base propia (con columnas de percepción), columnas de reconciliación en las 4 bases de producción, fprecioTotal vs suma (TIZIANO), fecha de comprobante asociado en NC/ND (CARACOL/CYRE), y el sello final "A" de ARCA en el E2E (gated por datos maestros — Anexo C).

Anexo A — Lo que había

Problemas encontrados y corregidos en el camino

Todo detectado por las revisiones del día y resuelto (salvo lo marcado como deuda):

graveIDs fiscales inventados en el template

El template traía valores de ejemplo (comind: 4, iva: 6/7/8) que viajan tal cual al XML de ARCA — trampa de copy-paste que habría emitido códigos incorrectos. Quedó: códigos oficiales (2/3/4/99, IVA 3/4/5) verificados EN VIVO contra ARCA (Anexo B).

graveIIBB/ComInd sumados al total aunque su flag esté en NO

Tras el refactor, la estrategia respetaba los flags pero el SQL del total no — con iibb: NO el total incluía un impuesto no declarado. Quedó: los 4 impuestos gateados simétricamente en SELECT, suma de tributos y total.

riesgoArranques "silenciosamente mal"

Nombre mágico con typo booteaba con impuestos apagados · max.results=0 procesaba cero para siempre sin error · quirk imposible se degradaba en silencio. Quedó: flags explícitos obligatorios + fail-fast en todas esas rutas — y al final, los nombres mágicos se eliminaron del todo (decisión de hoy: el YML declara todo).

riesgoProceso zombie ante fallo de arranque

Un arranque fallido dejaba el JVM vivo con el puerto tomado respondiendo 404 (lo encontró el testing E2E de instalación; dos jueces lo habían pasado por alto). Quedó: salida dura del proceso — probado: proceso muerto, puerto libre.

riesgoTIZIANO había perdido cMensajeError en la unificación

El original escribía el error en la base ante fallos y lo limpiaba al notificar — se había caído sin documentar (CRITICAL del primer Judgment Day). Quedó: restaurado verbatim; hoy es el quirk registra-mensaje-error.

operativoMemoria/CPU, puerto, run.bat

~650 MB por heap sin tope (quedó -Xmx256m ≈ 400 MB medidos) · CPU en reposo = 0% medido (los picos eran arranque o el cron de dev cada 1 segundo) · puerto no configurable (quedó server.port) · run.bat no funcionaba en layout de instalación (quedó dual).

deudaRegistrado sin acción (a propósito)

null→0 en importes (el legacy propagaba null; cambiarlo a ciegas es riesgo fiscal — necesita test con fila NULL real) · alias de columnas duplicados SELECT/mapper · SQL rebuild en hot path · constructor posicional de 34 args · GET feCompUltimoAutorizado sin cuit (falla en Manager multi-comercio — hallado por el E2E).

Anexo B — IDs fiscales

Verificados EN VIVO contra ARCA

Llamada real a FEParamGetTiposTributos / FEParamGetTiposIva (WSAA con certificado, homologación, 2026-08-27):

Respuesta viva del WS · lo que usamos, confirmado
ConfigCódigoCatálogo ARCA (vivo)Estado
ing.bruto2Impuestos provinciales (desde 2010)✓ vigente
comind3Impuestos municipales (desde 2010)✓ vigente
impinterno4Impuestos Internos (desde 2010) — el 10 NO existe en WSFE✓ vigente
perciva99"Otro" + Desc (los específicos 5=IIBB y 6=Perc.IVA existen desde 2017 como alternativa — migrar sería cambio deliberado)✓ vigente
iva 0/10.5/213/4/5Alícuotas (+6=27%, 8=5%, 9=2.5%)✓ vigentes

Cadena de evidencia: producción legada (años facturando) → manuales oficiales → Excel oficial otros_Tributos.xlsx (archivado en docs/) → respuesta viva del WS. Re-verificable con tools\consulta-tributos-arca.ps1.

Anexo C — E2E vivo

Notificaciones reales: notificador → Manager → ARCA

Seis combinaciones (todas las filas de la matriz, quirk de TIZIANO incluido) dispararon notificaciones REALES contra el Manager local y ARCA homologación. Cada barrera superada enseñó algo:

Pasadas del E2E
PasadaBarreraQué quedó probado
v1ARCA obs 703 (numeración secuencial)El circuito llega a ARCA; el Manager persistió el intento con total=1280 y tributos=70 EXACTOS (1000 neto + 210 IVA + 15 comInd + 25 IIBB + 30 percIVA) + reconciliación + CbteFchHsGen.
v3Dedup del Manager (UQ_Trx_nextgen)ARCA aceptó la numeración; la defensa anti-duplicados del Manager funciona (ticket único por día — mis datos sintéticos lo violaban).
v4ARCA obs 1444 (PV no habilitado)4 comprobantes persistidos en el Manager con desglose de IVA y tributos hijos; solo faltan datos maestros de ARCA.
Veredicto: la cadena fila del POS → reader por flags → Trx → Manager → WSAA → ARCA → respuesta → persistencia está probada de punta a punta con evidencia en ambas bases. El sello final "A" de ARCA queda gated por datos maestros del ciclo del Manager (CAEA vigente: su job corre los días 11-25; PV habilitado: portal ARCA) — no por código. El harness quedó listo para repetir el E2E un día 11-25 y buscar el verde total. Higiene: todas las filas sintéticas eliminadas, entes restaurados, cero rastros.