TsTicket · CAE/CAEA money path

Guía de revisión para los PRs encadenados #3 → #4 → #5 · generada 2026-09-08 · validado contra TIFacturaOnlineNext local (97707e4)

Resumen ejecutivo

Los tres PRs endurecen y simplifican la reconciliación CAE → CAEA de TsTicket contra el server de facturación, y llevan ese código al nivel de calidad de money path: tests directos, mutation testing y un chequeo de titularidad que antes no existía.

Antes
0 tests
directos sobre CaeService, CaeSoapClient y FacturacionService. Score de mutación 0% en las tres clases.
Viajes previos por ticket CAE
2 → 0
se eliminan getCaea por suc/pos y feCompUltimoAutorizado antes de emitir.
Recuperación de CAE
por ticket
consulta V2 con nroTicketPos; el CAE se reutiliza solo si es del ticket y está aprobado.
Regla de negocio que gobierna todo: en modo CAE,CAEA, cualquier falla del lado CAE termina en CAEA. La reconciliación existe solo para reutilizar un CAE que AFIP realmente otorgó y evitar notas de crédito después.

Orden de merge

Son PRs encadenados. Mergear en este orden, borrando la rama base en cada paso para que GitHub reapunte el siguiente a main:

  1. #3 feat/cae-reconciliation-ownership-testsmain
  2. #4 feat/cae-consult-by-ticket-v2 → rama del #3
  3. #5 test/cae-mutation-testing → rama del #4

Qué mejora cada PR

#3Titularidad del CAE recuperado + primeros tests directosfeat/cae-reconciliation-ownership-tests
  • Chequeo de titularidad. feCompConsultar devuelve nroTicketPos; el CAE recuperado se aplica solo si coincide con el del ticket (solo dígitos, ceros a la izquierda ignorados). Si falta o difiere: warn y CAEA.
  • CaeSoapClientTest (nuevo): forma del envelope de fecaeSolicitar (claves V2, sin comprobante/ptoVta), parseo con XML golden, rechazo por trxErrors, y las cuatro ramas de clasificación de fallos HTTP. Usa un WebClient con exchangeFunction stub, sin dependencias nuevas.
  • CaeServiceTest (nuevo): validaciones de consultarComprobante, propagación de nroTicketPos, casos fallback-safe de resolverPtoVtaCae, campos obligatorios de autorizarCae.
  • Fix de indentación en CaeService.validateInputs.

Qué mirar

  • TicketService.caeBelongsToTicket / normalizeTicketNumber: la política "ausente = ajeno" es intencional.
  • Los tests de TicketServiceTest modificados: el caso "own ticket" ahora setea nroTicketPos en ambos lados.
  • Los tests nuevos entraron con git add -f: .gitignore línea 3 (test/) también tapa src/test/. Pendiente anclarlo a /test/ en otro cambio.
#4Reconciliar por ticket (V2) y eliminar los viajes previosfeat/cae-consult-by-ticket-v2
  • Envelope V2 de feCompConsultar: enteFacturador, nroTicketPos, nroSuc, nroPos, tipoComprobante.id, comprobanteFecha. Misma clave que findByNextGenKey en el server. No manda ptoVta ni comprobante (el server rechaza las dos claves juntas).
  • Flujo: emitir → si falla, consultar por ticket una vez → si aprobado y propio, reutilizar el CAE; cualquier otra cosa, CAEA.
  • Eliminado: resolverPtoVtaCae, getCaea(cuit, suc, pos), consultarUltimoAutorizado y sus DTOs. El refresco diario de CAEA sigue usando getCaea(cuit) sin cambios.
  • Fix XPath: lecturas solo de hojas (and not(*)). La respuesta del server tiene <cae> como contenedor y como hoja; una respuesta no aprobada devolvía el texto concatenado del contenedor en vez de null.

Qué mirar

  • Que los campos del envelope V2 salgan de los mismos getters que el envelope de emisión (verificado en revisión: resolveComprobanteFecha, buildTipoComprobanteRef).
  • La consulta ahora también corre tras errores de validación previos a emitir. Es de solo lectura y termina en CAEA igual; el #5 lo cubre con test.
  • El server responde la consulta V2 SOLO desde su base, nunca va a AFIP. Una fila con resultado=null se lee como no aprobada y el POS va a CAEA, igual que antes; el job diario de notas de crédito del server limpia ese caso.
#5Mutation testing sobre el money pathtest/cae-mutation-testing
  • Perfil de mutación. Se agrega un perfil Maven mutation (PIT 1.30.0 + plugin JUnit 5) no ligado al ciclo de vida, y JaCoCo como reporte sin gate. Mismo criterio que el server TIFacturaOnlineNext.
  • Tests que matan sobrevivientes en CaeService, CaeSoapClient, el nuevo FacturacionServiceTest, y los métodos fiscales de TicketService. Todos afirman comportamiento: valores parseados, tipos de excepción, contenido del JSON de auditoría, forma del envelope XML.
  • Bug de producción encontrado y corregido. El catch de facturarConCAEA armaba el JSON de auditoría con Map.of("error", e.getMessage()). Map.of lanza NullPointerException con un valor null, así que un fallo de CAEA con excepción sin mensaje reventaba con un NPE ajeno en vez de salir como TicketException. Corregido con Collections.singletonMap y sustitución de null por cadena vacía, con test que falla primero.

Qué mirar

  • El fix 37a0caf: que el mismo patrón Map.of con valor posiblemente null no quede en otro lado del camino fiscal.
  • Que el perfil mutation NO corra en mvn test ni mvn verify, y que JaCoCo no tenga goal check que rompa CI.
  • Los 8 mutantes sobrevivientes documentados como equivalentes o código muerto: varios verificados empíricamente forzando la condición y confirmando que la suite sigue en verde.

Antes: reconciliación por contadores

Estado en main antes de los PRs. Dos viajes antes de cada emisión, y recuperación por el último número autorizado del punto de venta, sin verificar de quién es.

01 / PRECHECK (2 round trips per ticket) 02 / EMIT 03 / RECONCILE BY COUNTER GET getCaea?cuit,nrosuc,nropos nropvcae resolves fiscal point of sale feCompUltimoAutorizado(ptoVta) FECompUltimoAutorizado cbteNro cbteNro = N baseline fecaeSolicitar(nroTicketPos, V2) FECAESolicitar response lost / timeout row stays resultado=null SOAP fault 500 feCompUltimoAutorizado(ptoVta) retry FECompUltimoAutorizado cbteNro cbteNro = N+1 feCompConsultar(ptoVta, cbte=N+1) V1 key FECompConsultar cae, caeFchVto cae, caeFchVto CAE applied WITHOUT checking nroTicketPos ownership POS TsTicket TFO Server TIFacturaOnlineNext AFIP/ARCA fiscal authority Legend request return security async trace
Secuencia POS → TIFacturaOnlineNext → AFIP con el flujo previo.

Después de #3 y #4: consulta por ticket

Cero viajes previos. Ante una falla, una única consulta por la identidad del ticket. El server responde desde su base; el POS reutiliza el CAE solo si es propio y aprobado.

01 / EMIT 02 / RECONCILE BY TICKET 03 / OUTCOME 04 / DAILY CLEANUP fecaeSolicitar(nroTicketPos, nroSuc, nroPos, V2) FECAESolicitar response lost / timeout row reserved, resultado=null SOAP fault feCompConsultar(nroTicketPos, nroSuc, nroPos, tipo, fecha) V2 key, no ptoVta/comprobante -- DB lookup only, never AFIP resultado=A, cae, nroTicketPos owned + approved: reuse CAE trx without cae fallback CAEA (local code); informed later by notifier FECompConsultar (orphans) credit note (CAE + CAEA) POS TsTicket TFO Server TIFacturaOnlineNext AFIP/ARCA fiscal authority Legend request return security async trace
Secuencia con el flujo nuevo y la limpieza diaria del server.

Flujo de decisión en modo CAE,CAEA

01 / POS Fiscal Flow 02 / TFO Server EX / Fallback Close Ticket Mode CAE,CAEA? Apply CAE tipoCAE=CAE, nroPVFiscal Validation error before emit read-only, ends in CAEA Emit CAE fecaeSolicitar Success? Consult (V2) by ticket; owned + approved? CAEA local code, tipoCAE=CAEA CAE,CAEA mode success failure read-only owned + approved not owned / not approved Legend User UI Agent logic Policy Tool action Context / trace
Camino feliz, rama de recuperación y caída a CAEA.

Mutation testing

PIT 1.30.0 con mutadores por defecto, perfil Maven mutation no ligado al ciclo de vida (igual que en TIFacturaOnlineNext). Corrida: ./mvnw -Pmutation test-compile org.pitest:pitest-maven:mutationCoverage, reporte en target/pit-reports/index.html.

ClaseMutantesScore antesScore después
CaeService11287.5%93.8%
CaeSoapClient11083.6%93.6%
FacturacionService1994.7%94.7%
TicketService (solo métodos fiscales)6166.7%86.9%

Los 8 mutantes que quedan vivos están documentados como equivalentes o código inalcanzable (guardas de null duplicadas aguas arriba, ternarios que su callee ya neutraliza, o artefactos de bytecode duplicado de PIT sobre condiciones compuestas ya cubiertas). Varios se verificaron forzando la condición en producción y confirmando que la suite completa sigue pasando. Suite completa: 228 tests, 0 fallos, 1 skip preexistente. El perfil mutation no corre en el build normal; la corrida de PIT toma ~170 s.

Hallazgo del proceso: el trabajo de mutación destapó un bug de producción real, un NullPointerException que enmascaraba un fallo de CAEA sin mensaje. Fue corregido en este PR (fix(cae): 37a0caf). Justamente lo que el mutation testing busca: código que la cobertura de línea daba por probado pero ningún assert miraba.

Hechos del contrato del server en los que se apoya el cliente

HechoDónde está en TIFacturaOnlineNext
El ruteo V1/V2 es por datos: nroTicketPos presente → V2; comprobante ≠ 0 → V1; ambos → fault.ws/endpoint/TfomWsEndpoint.java L132-153
La consulta V2 es solo base de datos, nunca AFIP. No encontrado → trx sin cae, bloque <cae> omitido.action/ws/FacturacionService.feCompomprobanteFacturadoNextGen
caeFromTrx no copia resultado al bloque <cae>; la aprobación se lee de <trx><resultado>.ParentEntityManager#caeFromTrx
Un punto de venta por POS: índice único UQ_SucPosPV_pvcae.db/migration/add-unique-indexes-SucPosPV.sql
La emisión V2 persiste el número reservado ANTES de llamar a AFIP; respuesta perdida → resultado=null. Reintento idempotente por nroTicketPos (ramas 2 y 3).action/ws/FacturacionService.java ~L392-460
Job diario CAE↔CAEA: detecta doble facturación por identidad de ticket y emite la NC del CAE; consulta ARCA para huérfanos.action/ws/NotaCreditoReconciliacionService.java
Desde b7beec3 (2026-09-08) los rechazos V2 llegan con la observación real de ARCA; el texto genérico "Error al emitir CAE en AFIP" queda solo sin observación.git show b7beec3
nroTicketPos es xs:string en el WSDL: el server devuelve lo que se envió.wsdl/TiFacturaOnlineManagerWS.wsdl L120

Documento de handoff del server para exactamente esta revisión: docs/handoff-clientes-gateway.md.

Decisiones abiertas (no bloquean el merge)