API REST¶
Referencia de los endpoints REST (Spring MVC) de TiFacturaOnlineNext: versión, flujo CAEA para el POS y el contrato de errores. Los endpoints SOAP están en API SOAP; el ciclo de negocio, en El ciclo de facturación.
El Cockpit (ABM de SucPosPV, observabilidad) expone sus propios endpoints REST y se documenta aparte: ver Cockpit.
Endpoints¶
Todos son @RestController Spring MVC; el produces/consumes es application/json. La columna Acceso refleja el estado actual de seguridad (ver nota más abajo).
| Controller | Ruta | Método | Acceso | Qué hace |
|---|---|---|---|---|
VersionController |
/ |
GET |
abierto | Devuelve la versión (v20250328). |
CaeaWsRestController |
/caeaWS/getCaea y /TiFacturaOnlineManager/resource/rest/caeaWS/getCaea |
GET |
abierto | Lista los CAEAs vigentes/futuros del comercio (paso 1 del flujo POS). Ambos paths resuelven al mismo handler. |
CaeaNotificacionRestController |
/fecaeaSolicitar |
POST |
abierto | Registra un comprobante CAEA emitido offline por el POS (notificador DINO). Devuelve un DTO plano (TrxCaeaResponse), no la entidad JPA. |
CaeaNotificacionRestController |
/feCompUltimoAutorizado |
GET |
abierto | Último comprobante autorizado en ARCA para (ptoVta, cbteTipo). |
Seguridad: la facturación REST/SOAP queda abierta a propósito; el cockpit está protegido
SecurityConfig define un SecurityFilterChain que autentica el cockpit (/api/cockpit/** y la UI) con Spring Security (HTTP Basic + form login, user store in-memory por env), pero deja abiertos a propósito (permitAll) los endpoints de facturación: el SOAP del POS (/TiFacturaOnlineManagerWS/**, /ws/**) y estos REST (versión, flujo CAEA del POS, notificador DINO). Son máquina-a-máquina — POS legacy y notificador sin capacidad de auth. Por eso la columna Acceso sigue siendo "abierto" para todos los de esta tabla. CSRF está deshabilitado para /api/**, el SOAP y /fecaeaSolicitar (clientes-máquina sin token CSRF). El detalle del filter chain del cockpit (rutas, credenciales, CSRF) está en Cockpit.
VersionController¶
El más simple: GET / devuelve un string JSON con la versión.
@GetMapping(value = "/", produces = "application/json")
public String getVersion() { return VERSION; } // "v20250328"
En el sistema real la versión es ParentEntityManager.VERSION; acá la constante actúa de stand-in del contrato.
CaeaWsRestController — GET /caeaWS/getCaea¶
Paso 1 del flujo POS: lista los CAEAs vigentes/futuros del comercio en JSON. Es la reescritura del legacy JAX-RS (CaeaWSRest) a Spring MVC; delega en CaeaConsultaService.
Acepta query params opcionales para identificar el comercio (multi-comercio) y el POS, todos Integer/String (para distinguir "no vino" de 0/vacío):
| Param | Rol |
|---|---|
cuit |
Identifica el comercio emisor — v2, preferido. |
ente |
Identifica el comercio por EnteFacturador.id — v1, en transición (todavía aceptado). |
nrosuc / nropos |
Identifican el POS físico. Si llegan ambos, agregan nropvcae / nropvcaea a la respuesta. |
Sin cuit ni ente y con un solo comercio habilitado, funciona en modo single-comercio (paridad previa); con dos o más habilitados y sin identificador, la resolución falla (error claro, no 500 crudo).
GET /caeaWS/getCaea # single-comercio
GET /caeaWS/getCaea?cuit=20111111112 # v2: identifica el comercio por CUIT
GET /caeaWS/getCaea?ente=1 # v1: identifica el comercio por ente.id
GET /caeaWS/getCaea?cuit=20111111112&nrosuc=12&nropos=3
# Mismo handler también en el path legacy completo (compat POS V1):
GET /TiFacturaOnlineManager/resource/rest/caeaWS/getCaea
El cuit en la respuesta solo se devuelve al POS V1
Si el POS se identifica por ente (v1) NO conoce su propio cuit, así que el server lo agrega a la respuesta. Si se identifica por cuit (v2) ya lo tiene, y no se re-echa.
El lookup de PV ya NO es un handoff de una sola vez: nropvcae/nropvcaea se devuelven en cada getCaea que traiga nrosuc+nropos (el POS los necesita siempre), vía findSucPosPV — lookup read-only que no sella fechaUltObtencionPos. (El método viejo obtenerPVParaPOS, que sí sellaba una única vez, ya no lo usa este handler.)
La respuesta es GetCaeaResponse (@JsonInclude(NON_NULL) — los extras nulos no se serializan):
{
"caeas": [
{ "caea": "...", "periodo": 202606, "orden": 1,
"fchVigDesde": "...", "fchVigHasta": "...",
"fchTopeInf": "...", "fchProceso": "..." }
],
"nropvcae": "5",
"nropvcaea": "6",
"cuit": "..."
}
Dos paths para el mismo handler
El @GetMapping declara ambos paths: el nuevo /caeaWS/getCaea (misma convención que ParametroResource/VersionController) y el legacy completo /TiFacturaOnlineManager/resource/rest/caeaWS/getCaea. Así el POS V1 sigue funcionando sin cambios durante la migración a NextGen, sin depender del context-path de despliegue.
CaeaNotificacionRestController — notificador DINO¶
Endpoints consumidos por el notificador DINO (fase 2 anti-doble-factura). No usa @RequestMapping de clase para evitar colisión con /caeaWS: ambos cuelgan de la raíz del context-path.
POST /fecaeaSolicitar¶
Recibe el Trx JSON del notificador (TrxCaeaRequest, no la entidad JPA), lo registra en ARCA y persiste la fila CAEA. Es idempotente: si el comprobante ya existe (mismo ptoVta + comprobante + CAEA + V2) lo devuelve sin re-insertar ni re-reportar a ARCA. Delega en CaeaNotificacionService.registrarCaea. Devuelve ResponseEntity<TrxCaeaResponse> — un DTO plano, no la entidad JPA Trx.
La respuesta es un DTO plano (TrxCaeaResponse), no la entidad JPA
Antes devolvía la Trx managed directa. Con open-in-view=false, Jackson serializa la entidad fuera de la sesión Hibernate y revienta al tocar sus relaciones LAZY (tipoComprobante, alícuotas, tributos, cbtesAsoc) → LazyInitializationException → HTTP 500 en un POST que ya persistió e informó a ARCA (el notificador DINO lo lee como fallo y reintenta → riesgo de doble registro). TrxCaeaResponse.from(trx) copia solo campos escalares (seguros de leer sobre la entidad detached) y conserva los mismos nombres que Trx, para que el consumidor pueda deserializar en su propio Trx sin cambios. Campos del DTO: id, tipofacturacion, version, ptoVta, comprobante, comprobanteFecha, caea, resultado, fchProceso, cae, caeFchVto.
TrxCaeaRequest espeja los campos que envía DINO (TiFacturaOnlineManagerClient.fecaeaSolicitar): cabecera del comprobante, receptor, importes, fechas de servicio, caea / cbteFchHsGen, datos POS (nroTicketPos / nroSuc / nroPos) y las colecciones alicuotaIvas / tributos / comprobantesAsociados. Es un DTO explícito para aislar el contrato JSON del modelo de dominio.
Identificación del comercio: cuit (v2) o enteId (v1), ambos opcionales
TrxCaeaRequest acepta cuit (v2, preferido) o enteId (v1) para identificar el comercio emisor. Ambos son opcionales por back-compat con el notificador DINO legacy que no los envía: si vienen, el server resuelve el ente por CUIT o por id; si no, usa el único comercio habilitado (multi-comercio requiere identificarlo, si no la resolución falla).
GET /feCompUltimoAutorizado¶
Devuelve el último número de comprobante autorizado en ARCA para el (ptoVta, cbteTipo) dado. Contrato del notificador: devuelve Integer (0 si no hay ninguno).
Contrato de errores REST (DefaultExceptionHandler)¶
INVARIANTE (REFERENCE.md §5.8). Es la reescritura funcional del legacy (JAX-RS @Provider ExceptionMapper<Exception>) a un @RestControllerAdvice Spring MVC, con comportamiento observable idéntico.
Ante cualquier excepción (@ExceptionHandler(Exception.class)) devuelve:
| Componente | Valor |
|---|---|
| HTTP status | 500 INTERNAL_SERVER_ERROR |
Header exception-class |
nombre completo de la clase (e.getClass().getName()) |
Header exception-message |
String.valueOf(e.getMessage()) |
Content-Type |
application/json |
| Body | el stacktrace completo (de e.printStackTrace) |
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.header(ERROR_TYPE, e.getClass().getName()) // "exception-class"
.header(ERROR_MESSAGE, String.valueOf(e.getMessage())) // "exception-message"
.contentType(MediaType.APPLICATION_JSON)
.body(errors.toString()); // stacktrace
Mensaje null → header literal \"null\"
El legacy pasaba e.getMessage() crudo al header (puede ser null). Acá se usa String.valueOf(...) para evitar NPE al setear el header en Spring: si el mensaje es null, el header queda con el string "null". Si en paridad (SPEC-016) se detecta que el cliente espera el header ausente, se revisa.
Hardening de errores de mapeo (MappingErrorHardeningHandler)¶
Hay errores que Spring lanza antes de resolver el controller (en la fase de mapeo): path inexistente y verbo HTTP equivocado. Sin tratarlos aparte, los cazaba el DefaultExceptionHandler global como cualquier Exception → 500 + headers exception-class/exception-message + stacktrace en el body, filtrando estructura interna a cualquier scanner. Un @RestControllerAdvice con @Order(HIGHEST_PRECEDENCE) los endurece. Como se lanzan antes de resolver el controller, no pueden scopearse por paquete: el discriminado es por URI.
| Excepción | Caso | Respuesta |
|---|---|---|
NoResourceFoundException |
Path no mapeado | 404 global con body {"error":"not found"}, sin stacktrace (un path inexistente nunca es endpoint de negocio; además es más fiel al legacy, que devolvía 404). |
HttpRequestMethodNotSupportedException |
Verbo HTTP equivocado en /api/cockpit/** |
405 limpio con header Allow y body {ok:false, error:"Método HTTP no permitido"}, sin stacktrace. |
HttpRequestMethodNotSupportedException |
Verbo equivocado en cualquier otro path | Se delega en DefaultExceptionHandler → contrato legacy SPEC-009 intacto (byte-idéntico). |
Solo el 405 distingue por path
El 404 se endurece para todo el sistema (es siempre genérico). El 405, en cambio, solo se limpia para los paths nuevos del cockpit (/api/cockpit/**); fuera del cockpit el contrato de error legacy no cambia en absoluto.
Por dónde seguir¶
- API SOAP — los 3 endpoints SOAP (incluida la variante SOAP de
getCaea). - Cockpit — los endpoints REST de administración y observabilidad.
- El ciclo de facturación — dónde encaja el flujo CAEA del POS.