Cockpit (panel de operación)¶
El Cockpit es la cara operativa del backend: un conjunto de endpoints REST de monitoreo y de operación, más una interfaz web que los consume para responder, de un vistazo, ¿está vivo el enlace con AFIP?, ¿cuánto se está emitiendo y con qué tasa de éxito? y ¿qué quedó pendiente de resolver?. Para el por qué del ciclo CAE/CAEA que estas métricas reflejan, mirá El ciclo de facturación; acá están los datos exactos.
La UI es una SPA estática (src/main/resources/static/cockpit.html, ~1180 líneas, con el micro-framework reactivo Arrow.js — sin build, sin React/Thymeleaf), servida directamente por Spring. Hace polling a /api/cockpit/* cada ~5 s. WebConfig la mapea a tres rutas que cambian el modo desde la misma página:
| Ruta | Modo | Runner de escenarios |
|---|---|---|
/cockpit y /cockpit-test |
Test (ambiente de pruebas) | Visible |
/cockpit-prod |
Prod (solo monitoreo) | Oculto |
Además del modo de la ruta, el runner se gatea por configuración del server: la SPA consulta GET /api/cockpit/config al arrancar y, si runnerEnabled=false (perfil prod, fail-closed), oculta el bloque del runner aunque la ruta sea de test. Ver Configuración y perfiles.
La interfaz¶
La UI v2 muestra toda la operación en una sola página, filtrable por comercio (multi-ente). Vista general, con datos reales del ambiente de prueba:

La fila birds-eye resume la salud de cada comercio de un vistazo: la sucursal con sus POS y su KPI, el gateway en el medio y, a la derecha, el estado del enlace con ARCA/AFIP (online/offline y vigencia del token de acceso):

El ABM de comercios se desbloquea con el secret (X-Cockpit-Secret) y permite dar de alta/baja EnteFacturador, cargar su certificado y administrar sus puntos de venta:

En modo test, el runner de escenarios ejecuta operaciones contra AFIP homologación (obtener token, dummy, consultar último comprobante, emitir un CAE de prueba, etc.) y muestra el XML SOAP del tráfico:

Estas capturas son reales
Se tomaron del cockpit corriendo en http://localhost:8080/cockpit (autenticado) contra AFIP homologación. Para regenerarlas, levantá el backend (ver Setup del entorno) y corré un script de captura Playwright desde e2e/: mandá el header de auth proactivamente (Authorization: Basic …, solo al host local para no romper las fuentes por CORS) y esperá el render de Arrow.js (waitForFunction sobre el contenido) antes de capturar.
Todo cuelga del paquete com.tipre.tifacturaonlinemanager.cockpit. Hay tres bloques bien diferenciados:
| Bloque | Controller | Base path | Naturaleza |
|---|---|---|---|
| Monitoreo | CockpitController |
/api/cockpit |
Solo lectura: métricas, estado AFIP, backlog, KPIs, historia. Ente-aware (filtro ?ente). |
| ABM fiscal de PVs | CockpitAdminController |
/api/cockpit/sucpospv |
Escritura de SucPosPV (con dueño idEnteFacturador), gateado por secret. |
| ABM de comercios | EnteFacturadorAdminController |
/api/cockpit/comercios |
CRUD de EnteFacturador + cert write-only + recarga de TA, gateado por secret. |
| Test runner | CockpitTestRunnerController |
/api/cockpit/test |
Acciones contra AFIP homologación (algunas emiten de verdad). |
El Cockpit ya está detrás de autenticación (cockpit-auth)
Todo /api/cockpit/** y la UI (/cockpit, /cockpit-test, /cockpit-prod, /cockpit.html) requieren autenticación vía Spring Security (SecurityConfig): un SecurityFilterChain con HTTP Basic (para la API/runner/scripts) y form login (para el browser). El user store es un InMemoryUserDetailsManager (un único usuario con rol COCKPIT, password BCrypt), con credenciales por env (COCKPIT_USER / COCKPIT_PASS; defaults de dev admin/changeme, que loguean un WARN de arranque). Ver Autenticación del cockpit más abajo.
Los endpoints de facturación SOAP (/TiFacturaOnlineManagerWS/**, /ws/**) y REST quedan abiertos a propósito (permitAll): son máquina-a-máquina (POS legacy sin capacidad de auth, notificador DINO). El ABM (PVs y comercios) suma, además del user/pass, un segundo candado: el header X-Cockpit-Secret. Aun así, no expongas el cockpit a una red no confiable sin haber seteado COCKPIT_USER/COCKPIT_PASS (y COCKPIT_ADMIN_SECRET para el ABM).
1. Monitoreo — CockpitController¶
@RestController en /api/cockpit. Pensado para polling cada pocos segundos. Combina dos fuentes de verdad:
- Métricas en memoria (
CockpitMetrics): contadores OK/error por operación AFIP, vivos, thread-safe, que se pierden al reiniciar. Es monitoreo operativo, no auditoría. - Datos de la DB (
CockpitPendientes,CockpitPorPos,CockpitAfipStatus, etc.): backlog, stats por POS y estado del TA.
Cockpit ente-aware — filtro ?ente
Los endpoints de stats/backlog aceptan el query param ?ente para filtrar por comercio (EnteFacturador). Es un CSV multi-select (?ente=1,2 → List<Integer>); sin el parámetro (o vacío) = Todos, que es el comportamiento previo (paridad single-ente). Lo aceptan: /pendientes, /por-pos, /ultimas-trx, /pos-stats, /server-stats, /kpi y /ranking. Internamente, la agregación (computePosStats) siembra solo los POS de los entes pedidos (SucPosPV.idEnteFacturador IN :entes) y filtra las Trx por t.enteFacturador.id IN :entes. El modelo CockpitStat incluye idEnteFacturador para que la historia también separe por comercio. El endpoint GET /api/cockpit/entes ([{id, cuit, razonSocial}], solo entes habilitado=true) alimenta el listbox de comercios del front, cuyo estado se sincroniza con la URI (?ente=...).
Endpoints¶
| Método | Ruta | Qué devuelve |
|---|---|---|
| GET | /api/cockpit/config |
Config que el frontend necesita al arrancar: {runnerEnabled} (refleja cockpit.runner-enabled del server). |
| GET | /api/cockpit/summary |
afip (modo/endpoints) + ops (contadores por operación con tasa de éxito) + recentErrors + jobsEnAlerta (jobs con racha de fallos ≥ umbral). El widget principal. |
| GET | /api/cockpit/errors |
Solo los últimos errores en memoria (buffer acotado a 50). |
| GET | /api/cockpit/entes |
Lista de comercios habilitados [{id, cuit, razonSocial}] (solo habilitado=true). Pobla el listbox de selección de comercio del front. |
| GET | /api/cockpit/pendientes?ente= |
Backlog DB: pendientesDeCae, conError, autorizados. ?ente opcional (CSV multi-select; sin él = Todos). |
| GET | /api/cockpit/por-pos?ente=&desde=&hasta= |
Comprobantes por PV × modalidad (CAE/CAEA) × período. desde/hasta en yyyyMMdd, opcionales. ?ente opcional. |
| GET | /api/cockpit/afip-status |
Estado del enlace: online (FEDummy), ta (token/expiración) y facturador (CUIT/razón social). |
| GET | /api/cockpit/ultimas-trx?ente=&n=5 |
Feed de las últimas N transacciones (operación, tipo, resultado, PV, fecha-hora). ?ente opcional. |
| GET | /api/cockpit/jobs |
Grilla de jobs: 1 fila por Tarea con su última y próxima ejecución, más fallosConsecutivos y enAlerta (fallos ≥ umbral → badge rojo). Ver Jobs Quartz. |
| GET | /api/cockpit/errores?tipo=&desde=&hasta=&max=200 |
Errores de transacción (POS→servicio y servicio→ARCA) con filtros. desde/hasta en yyyy-MM-dd. Cada fila incluye mensajeCurado (mensaje humano extraído del raw). |
| GET | /api/cockpit/pos-stats?ente= |
Stats por POS físico (suc, pos): caeOk, caeError, reprocesos. Cuenta las emisiones CAE de ambos protocolos (V1+V2) y siembra en 0 todos los POS configurados en SucPosPV (se ven aunque no hayan facturado). Filtra caea IS NULL (excluye CAEA) y tipofacturacion null/'CAE' (excluye las NC de reconciliación). ?ente opcional. |
| GET | /api/cockpit/server-stats?ente= |
Totales del server agregando todos los POS. ?ente opcional. |
| GET | /api/cockpit/kpi?ente= |
KPI global: kpiPct = caeOk·100 / (caeOk+caeError+reprocesos) + arcaOnline. ?ente opcional. |
| GET | /api/cockpit/ranking?ente= |
Top-10 peores POS por KPI (ascendente; a igualdad, más errores+reprocesos primero). ?ente opcional. |
| GET | /api/cockpit/pos-trx?suc=&pos=&kind= |
Drill-down (máx. 25, id desc) de un POS. kind ∈ ok | error | reproceso; cualquier otro valor → lista vacía. |
| POST | /api/cockpit/stats-snapshot |
Trigger manual de snapshot (inserta uno ahora sin esperar el job de los 5 min). Útil para testear la historia. |
| GET | /api/cockpit/stats-history?hours=24 |
Historia de KPI en la ventana: arcaUptimePct, serverTrend[], posCount. Límite ~288 puntos (24 h a 5 min). |
De dónde sale cada número
pos-stats, server-stats, kpi, ranking y pos-trx cuentan las emisiones CAE de ambos protocolos por POS físico (suc, pos):
- V2 (NextGen) trae
nroSuc/nroPos→ se agrupa por esos directamente. - V1 (clásico) solo trae
ptoVta→ se mapea a(suc, pos)víaSucPosPV(pvcae/pvcaea). - POS configurados sin movimientos: se siembran en 0 desde
SucPosPV, así aparecen enpos-statsaunque no hayan facturado todavía. (Enrankingse excluyen los de total 0: un POS sin actividad no es "el peor".) - Notas de Crédito de reconciliación (
tipofacturacion <> 'CAE') quedan fuera del conteo CAE, igual que las CAEA (caeano nulo).
La agregación V1+V2 ok/error/reprocesos es con GROUP BY en la DB (acotada por #POS, no por #filas, para escalar sobre el histórico V1); el resto de los KPIs derivados se calculan en Java a propósito, por máxima compatibilidad de dialecto entre H2 (tests) y SQL Server (runtime). Ver Persistencia.
Alerta roja de jobs (CockpitJobs) — sin infra de alerting nueva
Cada fila de /jobs lleva fallosConsecutivos (últimas N ejecuciones en ERROR desde la más reciente, capadas al umbral) y enAlerta (fallos ≥ umbral → badge rojo en el cockpit). El umbral es cockpit.jobs-alerta-umbral (default 3). La fuente es la bitácora JobEjecucion en DB, y la superficie es el JSON que la UI ya pollea — no hay canal de alerting nuevo. /summary reexpone los jobs en alerta bajo jobsEnAlerta ([{tareaId, jobNombre, fallosConsecutivos, error}]) para el badge del header. Al cruzar el umbral (no en cada poll) se emite un WARN a tifactura.log, que ops ya mira. Ver Jobs Quartz.
Métricas en memoria (CockpitMetrics)¶
Las operaciones que se cuentan están en el enum CockpitMetrics.Op:
| Op | Operación AFIP |
|---|---|
TOKEN |
WSAA LoginCms |
CAE |
WSFE FECAESolicitar |
CAEA |
CAEA (solicitud/consulta) |
ULTIMO_CBTE |
FECompUltimoAutorizado |
CONSULTA |
FECompConsultar |
NOTIFICACION |
Cada OpStat expone ok, error, total y successRate (0..100; 100 si no hubo intentos). El buffer de errores recientes está acotado a 50 entradas (recentErrors, más reciente primero). Los clientes AFIP (WSAA/WSFE) alimentan estos contadores al ejecutar cada operación; además, recordError persiste el error vía TrxErrorRegistro para que la grilla /errores lo vea aunque el contador en memoria se haya reseteado.
Mensaje curado del error (MensajeCurador) — legible para grilla y alarmas
Al persistir un TrxErrorLog, TrxErrorRegistro guarda —además del mensaje crudo (hasta 2000 chars)— un mensajeCurado (columna mensajeCurado, máx 500): el mensaje humano extraído del raw por MensajeCurador.curar(...). El raw llega en tres formas, y el curador las reduce por prioridad:
- JSON de error del servicio → el campo
"message"(des-escapado). - SOAP fault del POS → el texto del
Reason(<...:Text>). - Rechazo AFIP → la observación tras
observaciones:. - Fallback → el raw tal cual (truncado).
Lo consume la grilla /errores y el job de alarmas (mail/telegram/whatsapp) sin re-parsear XML.
Backfill del mensaje curado al arranque (CuradoBackfillRunner)
Las filas históricas de TrxErrorLog (grabadas antes de existir la columna) quedaron con mensajeCurado = null. Un runner one-time (@EventListener(ApplicationReadyEvent)) las cura al arranque (TrxErrorRegistro.backfillCurado()). Es idempotente: tras la primera corrida no quedan null, las siguientes son no-op. Su fallo no frena el arranque (loguea un WARN no crítico).
Estado AFIP (CockpitAfipStatus)¶
/afip-status arma tres bloques:
online— pingFEDummya WSFE (no requiere TA). Cacheado 60 s (DUMMY_TTL_MS) para no martillar a AFIP. Reportaappserver/dbserver/authserverycheckedAt. Agregaestado(online|verificando|caido) yfallosConsecutivoscon debounce (ver abajo).ta— si elEnteFacturadorhabilitado tiene token, cuándo expira (expira), si estávigenteyminutosRestantes.facturador—cuityrazonSocialdel primerEnteFacturadorhabilitado.
Debounce del estado ARCA — no pintar 'caído' por un blip
AFIP homologación es flaky: un solo FEDummy fallido no debe pintar "caído" en todos los comercios. El bloque online agrega estado con debounce, calculado por ArcaLinkHealth sobre fallos consecutivos:
online— el último ping fue OK (resetea el contador).verificando— 1 aUMBRAL_CAIDO−1 fallos seguidos (degradado transitorio).caido— se alcanzóUMBRAL_CAIDO= 3 fallos consecutivos.
Con el enlace degradado (algún fallo), el TTL del cache baja de 60 s a 15 s (DEGRADED_TTL_MS): converge a "caído" en ~1 min en vez de ~3, y detecta la recuperación más rápido. fallosConsecutivos viene en el JSON para diagnóstico. ArcaLinkHealth es thread-safe (el ping puede dispararse desde requests concurrentes). — CockpitAdminGuard (header X-Cockpit-Secret)
Los dos ABM del cockpit (PVs y comercios) son config fiscal sensible: un PV mal cargado factura contra el punto de venta equivocado; un cert mal seteado firma con la identidad equivocada. Por eso, además de la autenticación user/pass de cockpit-auth (sección 5), suman un segundo candado: el header X-Cockpit-Secret, centralizado en el componente compartido CockpitAdminGuard (requireSecret(...)), inyectado en ambos controllers (una sola fuente de verdad del candado).
- El secret se configura por la env var
COCKPIT_ADMIN_SECRET(propertycockpit.admin-secret). - El front lo manda en el header
X-Cockpit-Secreten cada request del ABM. - Comparación en tiempo constante (
MessageDigest.isEqual) para no filtrar el secret por timing. - Fail-closed: si no hay secret configurado, todo el ABM responde
503(deshabilitado). Si el secret no coincide,401.
Dos candados en serie sobre el ABM
Una request al ABM atraviesa primero Spring Security (user/pass de cockpit-auth) y después el CockpitAdminGuard (secret). No hay conflicto: la auth de plataforma valida quién entra al cockpit; el secret es un segundo factor que evita que la config fiscal quede a un click de cualquier sesión ya autenticada.
3. ABM de SucPosPV — CockpitAdminController¶
SucPosPV es el mapeo sucursal + POS físico → punto de venta CAE (y opcionalmente CAEA) de AFIP, con su comercio dueño (idEnteFacturador). Va detrás de cockpit-auth + el secret de la sección 2.
Ente-aware: el PV ahora lleva dueño (idEnteFacturador)
Tras multicomercio, cada SucPosPV pertenece a un comercio (EnteFacturador). El ABM lo refleja:
- Alta:
idEnteFacturadores obligatorio (ausente →400). El comercio debe existir y estar habilitado (inexistente o deshabilitado →422, víaEnteInvalidoException). Esto reemplaza el backfill manual en SQL que se hacía antes. - Edición:
idEnteFacturadores opcional (null → no cambia el dueño); si viene, se revalida existencia + habilitado (422si no). - Listado filtrable:
GET /api/cockpit/sucpospv?idEnteFacturador={id}filtra por comercio; sin el parámetro devuelve todos (comportamiento previo preservado). - El ABM no relaja la integridad fiscal de multicomercio: la unicidad GLOBAL del PV se sigue validando (
validarUnicidad), y un PV sin comercio asignado se rechaza al editar.
Endpoints¶
| Método | Ruta | Qué hace | Códigos |
|---|---|---|---|
| GET | /api/cockpit/sucpospv?idEnteFacturador= |
Lista los SucPosPV. Con idEnteFacturador filtra por comercio; sin él, todos. |
200 / 401 / 503 |
| GET | /api/cockpit/sucpospv/config |
Config informativa del ABM (revalidarArcaAlGuardar). |
200 / 401 / 503 |
| POST | /api/cockpit/sucpospv |
Alta. Body {suc, pos, pvcae, pvcaea?, idEnteFacturador}. |
201 / 400 (sin idEnteFacturador) / 409 (duplicado) / 422 (ente inexistente/deshabilitado) / 502 (AFIP no responde con revalidación ON) |
| PUT | /api/cockpit/sucpospv/{id} |
Edición (incluye reasignar idEnteFacturador). |
200 / 404 / 409 / 422 / 502 |
| DELETE | /api/cockpit/sucpospv/{id} |
Baja. | 200 / 404 |
| POST | /api/cockpit/sucpospv/{id}/validar-arca |
Valida los PV de la fila contra AFIP a demanda. | 200 / 404 / 409 (PV inexistente/bloqueado) / 502 |
El cuerpo de alta/edición es {suc, pos, pvcae, pvcaea, idEnteFacturador} donde pvcaea es opcional (nullable) — un POS puede facturar solo CAE — e idEnteFacturador es obligatorio en alta (en edición, null = no cambia el dueño).
Revalidación contra ARCA al guardar
Con cockpit.revalidar-arca-al-guardar=true (env COCKPIT_REVALIDAR_ARCA), el alta/edición valida los PV contra AFIP antes de persistir: si AFIP rechaza un PV, no se guarda (responde 502). Por default es false (no depende de AFIP para editar). Ver Configuración y perfiles.
Las ResponseStatusException se traducen a JSON {status, message} mediante un @ExceptionHandler propio, porque con la cadena de filtros de Security el resolver por defecto las dejaría salir como 500.
4. ABM de comercios — EnteFacturadorAdminController¶
CRUD de EnteFacturador (tabla EntesFacturadores): el comercio dueño de los PVs, con su CUIT, razón social, flag habilitado (el mismo que lee la resolución de ente en emisión), su certificado .p12 por comercio y los campos de PDF del comprobante (getPdf). Vive bajo /api/cockpit/comercios, detrás de cockpit-auth + el mismo secret X-Cockpit-Secret (sección 2). Reemplaza por UI lo que antes se administraba a mano en SQL. No toca emisión, money-path ni WSDL — es administración pura.
Campos de PDF del comprobante (getPdf)¶
El ABM administra los dos campos que configuran el PDF por comercio: backGroundFacturaPath (path de filesystem al PDF template del comprobante) y layout (copias: 1=ORIGINAL, 2=+DUPLICADO, 0=sin PDF). EnteFacturadorForm los expone y el service valida layout a 0..2 antes de persistir (rechaza con AbmComercioException); null/vacío = no tocar el valor actual (permite editar sin re-tipear). Ver Email y PDF.
El cert es write-only (la password nunca sale)¶
La protección es por whitelist, no blacklist: las respuestas del ABM nunca serializan la entidad EnteFacturador, solo el DTO de lectura EnteFacturadorView, que no tiene campo certP12Password (ni getter). Que el campo no exista en el DTO ⇒ Jackson no puede emitirlo bajo ninguna config futura.
EnteFacturadorView(lectura) publica:id, razonSocial, cuit, habilitado, certP12Path, certP12Alias, certConfigurado.certConfiguradoes un booleano derivado (certP12Password != null && !blank) que deja al front mostrar "Cert: configurado / sin configurar" sin revelar la clave.EnteFacturadorForm(escritura) sí incluyecertP12Password. Semántica de set: null/vacío → no toca el valor actual en DB; string no vacío → lo reemplaza. Así se puede editar razón social o el path sin re-tipear la clave.- El service tiene prohibido loguear la entidad (su
toString()arrastratoken/sign) y el valor de la password en cualquier nivel.
Password del cert en texto plano (decisión firme)
La certP12Password se guarda en texto plano en la DB (decisión de producto, "como antes"). Está mitigada —write-only estricto (nunca se devuelve, loguea ni muestra) y detrás de los dos candados del cockpit— pero no cifrada. Encriptarla es trabajo futuro.
Recarga de cert (evict del TA en 2 niveles)¶
POST /api/cockpit/comercios/{id}/cert/reload invalida el TA cacheado del comercio para que la próxima emisión re-logueé con el cert recién guardado, sin reiniciar la app. El evict limpia dos niveles (vía EnteFacturadorTokenProvider.evict):
- L1 (memoria): la entrada por CUIT del
WsaaTokenCache. - L2 (DB): nulea
token/sign/expirationTimede la fila delEnteFacturador.
Limpiar solo L1 dejaría el TA viejo VIGENTE en L2 (DB), y la próxima emisión seguiría firmando con el cert viejo — por eso se limpian ambos. Requiere cert configurado (path + password): si no, responde 422. El re-login es lazy (ocurre en la próxima emisión real), no inmediato. La respuesta avisa: "Cert recargado. El TA nuevo se emitirá en la próxima emisión (puede requerir esperar a que expire el TA anterior en AFIP)."
El gotcha de AFIP — 'el CEE ya posee un TA válido'
Al rotar el cert del mismo CUIT, AFIP puede rechazar un LoginCms inmediato si el TA anterior sigue vigente (~12 h). No es una regresión del ABM: es el comportamiento histórico del TA por CUIT. Por eso el evict es best-effort y diferido (no fuerza un LoginCms ya); el operador reintenta tras la expiración. El error de WSAA queda observable en las métricas TOKEN.
Verificación de PVs contra AFIP (F5)¶
GET /api/cockpit/comercios/{id}/pvs-afip compara los puntos de venta configurados localmente para el comercio contra los que AFIP reporta por FEParamGetPtosVenta (vía PtoVentaConsultaService). Es solo lectura (no toca money-path). Devuelve PvsAfipComparacion (solo números de PV, sin datos sensibles):
| Campo | Qué lista |
|---|---|
pvOkCae / pvOkCaea |
PV locales (CAE / CAEA) que SÍ existen en AFIP. |
pvFaltantesCae / pvFaltantesCaea |
PV locales (CAE / CAEA) que no están en AFIP. |
pvAfipNoConfigurados |
PV habilitados en AFIP que no están configurados localmente. |
Si AFIP falla (timeout, sin TA, cert inválido) responde 502 (AfipException).
Endpoints¶
| Método | Ruta | Qué hace | Códigos |
|---|---|---|---|
| GET | /api/cockpit/comercios |
Lista todos los comercios (incluso deshabilitados), como EnteFacturadorView. |
200 / 401 / 503 |
| GET | /api/cockpit/comercios/{id} |
Detalle de un comercio (sin certP12Password). |
200 / 404 / 401 / 503 |
| POST | /api/cockpit/comercios |
Alta. Body EnteFacturadorForm (razonSocial, cuit, habilitado, cert opcional, campos PDF opcionales). CUIT y razón social únicos. |
201 / 409 (CUIT/razón social duplicados) |
| PUT | /api/cockpit/comercios/{id} |
Edición de razón social, habilitado, cert y campos PDF (backGroundFacturaPath, layout). El CUIT NO es mutable (se ignora del form). |
200 / 404 / 409 / 422 (layout inválido) |
| PATCH | /api/cockpit/comercios/{id}/habilitar |
Baja lógica inversa: habilitado = true. |
200 / 404 |
| PATCH | /api/cockpit/comercios/{id}/deshabilitar |
Baja lógica (habilitado = false). No hay DELETE físico (preserva FKs de SucPosPV y Trx). |
200 / 404 |
| PUT | /api/cockpit/comercios/{id}/cert |
Set de cert (certP12Path, certP12Password, certP12Alias); campos null/vacíos no se tocan. La respuesta no trae la password. |
200 / 404 |
| POST | /api/cockpit/comercios/{id}/cert/reload |
Recarga de cert: evict del TA (L1+L2). | 200 / 404 / 422 (cert no configurado) |
| GET | /api/cockpit/comercios/{id}/pvs-afip |
Compara PVs locales vs AFIP (FEParamGetPtosVenta). |
200 / 404 / 502 (AFIP falló) |
Deshabilitar un comercio corta la emisión de sus POS
habilitado = false es el flag que EnteResolver exige true (fail-closed) en el path de emisión: deshabilitar un comercio hace que sus SucPosPV dejen de poder emitir (ENTE_DESHABILITADO), y lo saca del GET /api/cockpit/entes y del selector de emisión. Es el comportamiento correcto (cierre de comercio), pero impacta operación — la UI advierte antes de confirmar.
5. Autenticación del cockpit — cockpit-auth (SecurityConfig)¶
El cockpit ya no está abierto. SecurityConfig (com.tipre.tifacturaonlinemanager.config) define un SecurityFilterChain que protege el cockpit y deja abiertos los endpoints de facturación.
Qué está protegido y qué no¶
| Rutas | Acceso | Por qué |
|---|---|---|
/api/cockpit/** (API + runner) |
Autenticado | API del cockpit y runner: solo operadores. |
/cockpit, /cockpit-test, /cockpit-prod (+ subpaths) y /cockpit.html |
Autenticado | La UI y sus tres forwards de WebConfig + el static crudo. |
/TiFacturaOnlineManagerWS/** (SOAP) |
Abierto (permitAll) |
POS legacy sin capacidad de auth (máquina-a-máquina). |
/ws/** |
Abierto | Cualquier otro endpoint WS. |
| Resto (REST de facturación, health, actuator, estáticos) | Abierto | anyRequest().permitAll(). |
Los matchers van de más específico a más general: los de cockpit primero, después los permitAll del SOAP/WS y el anyRequest. (Sin listar /cockpit-test, /cockpit-prod y /cockpit.html explícitos, esos caían al anyRequest().permitAll() y quedaban SIN auth — por eso están enumerados.)
Mecanismo, user store y credenciales¶
- HTTP Basic (
httpBasic) para la API/runner/scripts/tests de integración + form login (formLogin) para el browser. Ambos sobre el mismo filter chain. - User store:
InMemoryUserDetailsManagercon un único usuario, rolCOCKPIT, password codificada con BCrypt. No usa la tablaUsuarioslegacy. - Credenciales por entorno:
cockpit.auth.user/cockpit.auth.password(envCOCKPIT_USER/COCKPIT_PASS). En dev, defaultsadmin/changeme(application-dev.yml). No hay credenciales hardcodeadas en el código. - Aviso de arranque: si las credenciales están vacías o son las de dev por defecto,
SecurityConfigloguea un WARN ("usando credenciales por defecto de dev… configuráCOCKPIT_USER/COCKPIT_PASSantes de exponer en producción").
CSRF¶
CSRF queda deshabilitado para todo lo máquina-a-máquina (ignoringRequestMatchers): /api/** (API del cockpit), /TiFacturaOnlineManagerWS/** y /ws/** (SOAP del POS) y /fecaeaSolicitar (REST del notificador DINO). Esos clientes son máquinas y no mandan token CSRF — con CSRF activo recibirían 403 y se rompería la emisión. CSRF sigue activo solo para el form login del cockpit (browser).
El secret del ABM convive con la auth
El candado X-Cockpit-Secret del ABM (sección 2) no se reemplaza por cockpit-auth: opera como segundo factor dentro de la ruta ya autenticada /api/cockpit/{sucpospv,comercios}. Spring Security verifica user/pass primero; luego el controller verifica el secret.
6. Test runner — CockpitTestRunnerService / CockpitTestRunnerController¶
@RestController en /api/cockpit/test. Son acciones de validación técnica contra AFIP homologación. Cada método mide durationMs, captura excepciones y devuelve siempre {ok, durationMs, error, …campos} — nunca tira la excepción al cliente. Además el controller envuelve cada llamada en withTraffic(...), que captura el XML SOAP request/response hacia ARCA y lo agrega en la clave traffic: [{dir, action, xml}] para inspeccionarlo desde el panel.
Fail-closed: el runner está apagado por defecto
Todos los endpoints del runner están gateados por la property cockpit.runner-enabled (@Value, default false). Si está en false (perfil prod), cada endpoint responde 403 (requireRunner() tira ResponseStatusException(FORBIDDEN)) — así la página de monitoreo no puede emitir nada aunque alguien pegue a /api/cockpit/test/* por URL. El perfil dev lo activa. Ver Configuración y perfiles.
Algunos endpoints EMITEN comprobantes REALES
POST /test/cae, POST /test/cae-v1, POST /test/cae-tributos, POST /test/cae-factura-a, POST /test/caea-solicitar y POST /test/caea-notificar generan/registran comprobantes reales en homologación. El código loguea un WARN ruidoso ([COCKPIT-CAE-PRUEBA] EMITIENDO CAE REAL…). Solo usar en el ambiente de prueba, nunca apuntando a producción.
Endpoints¶
Los endpoints que emiten o consultan contra AFIP aceptan, además de sus params propios, la pareja opcional enteId + idModo para elegir por qué identificador se resuelve el comercio (ver "Identificación del comercio" abajo).
| Método | Ruta | Qué hace | ¿Modifica? |
|---|---|---|---|
| GET | /api/cockpit/test/pos |
Lista los POS virtuales configurados (SucPosPV): [{suc, pos, pvcae, pvcaea}]. |
No |
| POST | /api/cockpit/test/token |
Obtiene/refresca el TA y devuelve vigencia (vigente, expira, minutosRestantes, tokenLen). |
No (cache-hit si vigente) |
| GET | /api/cockpit/test/ultimo?ptoVta=&tipo=&enteId=&idModo= |
Último comprobante autorizado (FECompUltimoAutorizado). |
No |
| GET | /api/cockpit/test/consultar?ptoVta=&tipo=&comprobante=&enteId=&idModo= |
Consulta un comprobante (cae, resultado, caeFchVto). |
No |
| POST | /api/cockpit/test/cae |
Emite un CAE de prueba (Factura B, Consumidor Final, $121) por el protocolo V2 (NextGen): el server numera por ticket + suc/pos (nroTicketPos). Body {suc, pos, nroTicketPos?, enteId?, idModo?}. |
Sí |
| POST | /api/cockpit/test/cae-v1 |
Emite un CAE de prueba por el protocolo V1 (clásico): el runner consulta último+1 en el PV y numera él. Body {suc, pos, ptoVta, enteId?, idModo?}; devuelve además ptoVta. |
Sí |
| POST | /api/cockpit/test/cae-tributos |
Emite una Factura B con Impuestos Internos (V1) para validar el bloque Tributos contra homologación (reproduce el caso que fallaba con AFIP 10024). Body {suc, pos, ptoVta, enteId?, idModo?}. |
Sí |
| POST | /api/cockpit/test/cae-factura-a |
Emite una Factura A completa (3 alícuotas de IVA + 3 tributos, V1) a Responsable Inscripto. El receptor es distinto del emisor (evita AFIP 10069). Body {suc, pos, ptoVta, enteId?, idModo?}. |
Sí |
| GET | /api/cockpit/test/dummy |
FEDummy: aliveness de los 3 subsistemas AFIP. |
No |
| GET | /api/cockpit/test/validar-pv?suc=&pos=&enteId=&idModo= |
Valida que el pvcae del POS esté activo en AFIP (FEParamGetPtosVenta): encontrado, bloqueado. |
No |
| GET | /api/cockpit/test/getcaea?enteId=&idModo=&suc=&pos= |
Espeja el entry point del POS (GET /caeaWS/getCaea): resuelve el comercio por cuit (v2) o ente.id (v1) y devuelve cantCaeasVigentes + cuit?/nropvcae?/nropvcaea?. Valida la resolución "aceptar ambos". |
No |
| GET | /api/cockpit/test/caea-consultar?periodo=&orden= |
Consulta el CAEA de la quincena (lectura). | No |
| POST | /api/cockpit/test/caea-solicitar?periodo=&orden= |
Solicita un CAEA a AFIP (idempotente por quincena). | Sí |
| POST | /api/cockpit/test/caea-notificar |
Registra un comprobante CAEA de prueba (Factura B, CF, $121). Body {suc, pos, enteId?, idModo?}. |
Sí |
El CAE de prueba es idempotente por ticket
POST /test/cae acepta un nroTicketPos explícito. Re-enviar el mismo ticket ejercita el anti-doble-facturación del flujo real V2: devuelve el CAE existente en vez de emitir de nuevo. Si no se pasa, usa TEST-<timestamp> (siempre nuevo). El campo de ticket en la UI solo aparece en V2; en V1 el server numera último+1 y no hay ticket.
Identificación del comercio en el runner — enteId + idModo (valida 'aceptar ambos')
Cada escenario declara qué comercio usa y por qué identificador, para validar desde el cockpit que la resolución del flujo real acepta ambos:
idModo = "cuit"→ v2: el runner declara SOLO elcuitdel comercio (resuelto desdeenteId).idModocualquier otro (o"ente") → v1: declara SOLO elente.id.enteIdausente → back-compat: primer comercio habilitado (single-comercio).
El TA que usa cada escenario es el del ente resuelto (EnteFacturadorTokenProvider.get(ente)), no un TA genérico: así el TA coincide con el CUIT y se evita el mismatch TA/CUIT en multi-comercio. Los escenarios de emisión V1 (cae-tributos, cae-factura-a) ahora llevan tributos y, la Factura A, receptor RI distinto del emisor (AFIP rechaza 10069 si DocNro = CUIT emisor).
En la UI del runner¶
El escenario "Obtener CAE" ofrece un selector de Protocolo POS que decide a qué endpoint pega el front: V2 · NextGen (/test/cae, default) o V1 · clásico (/test/cae-v1). El panel de resultado muestra el protocolo usado (V1 · fecaeSolicitar / V2 · fecaeSolicitar (nroTicketPos)) — ambos por la misma operación SOAP fecaeSolicitar, distinguidos por nroTicketPos.
En los escenarios que piden tipo de comprobante (Último comprobante y Consultar comprobante), el tipo dejó de ser un campo libre: ahora es un listbox restringido a los tipos permitidos [1, 3, 6, 8, 11, 13] (Facturas y Notas de Crédito A/B/C; sin Notas de Débito).
Manejo de errores (CockpitExceptionHandler)¶
Los endpoints del cockpit son nuevos: no forman parte del contrato legacy SPEC-009 (que replica el DefaultExceptionHandler global con 500 + headers exception-class/exception-message + stacktrace en el body — ver API REST). Para no filtrar estructura interna, un @RestControllerAdvice scopeado al paquete com.tipre.tifacturaonlinemanager.cockpit (@Order(HIGHEST_PRECEDENCE)) los maneja distinto:
ResponseStatusException(status intencionales: 403 runner off, 401/503 del ABM, etc.) → ese mismo status con body{ok:false, error:<reason>}.- Cualquier otra excepción →
500con{ok:false, error:"Error interno del cockpit"}; el detalle completo (con stacktrace) queda solo en el log del server, nunca en la respuesta.
Como está scopeado por basePackages, no toca los controllers de negocio (web.*) ni el SOAP; y los @ExceptionHandler propios de cada controller (p.ej. el {status, message} del ABM) siguen teniendo prioridad. El hardening de errores de mapeo (404/405, que se lanzan antes de resolver el controller) vive aparte en MappingErrorHardeningHandler — ver API REST.
Para emitir un CAE de prueba paso a paso, mirá la guía de tareas.
Por dónde seguir¶
- Jobs Quartz — la grilla
/jobsdel cockpit muestra estos jobs. - API REST — el resto de la superficie REST del backend.
- El ciclo de facturación — qué significan CAE, CAEA y los estados que el cockpit reporta.
- Setup del entorno — para levantar el backend y pegarle al cockpit en local.