Reglas operativas (parámetros y SLA)¶
Estos son los números y reglas de negocio que gobiernan el comportamiento del sistema; cambiarlos cambia la operación. Todo lo que sigue está verificado contra el código del backend (no contra documentos previos): cada fila apunta al archivo y la línea que la define. Si mañana alguien edita un default, este documento queda desactualizado salvo que se actualice junto al código.
Cómo leer esta página
Los parámetros son configurables vía application.yml (prefijos app.tap, app.smiley, app.monitor, app.scheduler). Los valores de la tabla son los defaults de seguridad que aplica el constructor de cada record cuando el valor inyectado es <= 0 (o vacío). Es decir: aunque no configures nada, el sistema arranca con estos números.
1. Parámetros configurables de negocio¶
1.1. Algoritmo del tap — app.tap¶
Definidos en TapProperties.java (@ConfigurationProperties(prefix = "app.tap")). El constructor compacto del record fija el default si el valor es <= 0.
| Parámetro | Default | Qué controla |
|---|---|---|
cierreAutomaticoMaxMinutes |
30 min | Si el operario olvida marcar salida, la duración del trabajo cerrado automáticamente se trunca a este tope (ADR-003). |
clockDriftToleranceMinutes |
5 min | Tolerancia entre ts_local_device y server_received_ts. Si se excede, agrega un warning pero NO rechaza el trabajo. |
duracionMinimaSeg |
30 seg | Piso de validez de una limpieza (ANEXO DT, DT-D2). Un EGRESO con duración menor cierra el trabajo con duracion_anomala=1: no cuenta como limpieza válida y genera Alerta DURACION_ANOMALA. Es un detector de tap accidental, no una vara de calidad fina. |
reboteIngresoSeg |
8 seg | Ventana anti-rebote server-side (DT-D3 / DT-B3). Un INGRESO a menos de este margen de un EGRESO del mismo operario+sanitario+device se descarta como doble-tap accidental. |
driftMaxSeg |
300 seg (5 min) | Umbral de drift de reloj del device (DT-D5). Si \|device_now − server_now\| lo excede, se estampa drift_seg y se genera Alerta RELOJ_DESVIADO. |
silencioClienteSeg |
8 seg | Config remota que viaja al cliente en el sync (DT-D1): tras procesar un tap, la app ignora lecturas del mismo tag por estos segundos. |
confirmarEgresoMin |
2 min | Config remota que viaja al cliente (DT-D4): si un EGRESO resuelve con duración menor a estos minutos, la app pide confirmación al operario. |
Dos parámetros distintos para el reloj desviado
clockDriftToleranceMinutes (5 min) y driftMaxSeg (300 seg) coinciden en valor pero son cosas distintas: el primero decide el warning clock_drift_warning en el trabajo; el segundo decide la Alerta RELOJ_DESVIADO. No los unifiques pensando que es redundancia.
1.2. Anti-abuso del Smiley — app.smiley¶
Definidos en SmileyProperties.java (prefix = "app.smiley").
| Parámetro | Default | Qué controla |
|---|---|---|
debounceSeg |
3 seg | Opiniones del mismo device separadas por menos de este valor se descartan (se conserva la primera de cada ráfaga temporal). |
rafagaMax |
5 opiniones | Máximo de opiniones del mismo valor permitidas dentro de la ventana de ráfaga. Si hay más, se conserva 1 y el resto se marca descartada=true, motivoDescarte="rafaga". |
rafagaVentanaSeg |
30 seg | Ventana de tiempo para el colapso de ráfaga. |
pinTecnicoDefault |
2468 |
PIN técnico GLOBAL del menú oculto de la terminal (S13/B3a). Es un secreto de deploy: el backend lo hashea con BCrypt al arrancar y solo entrega el hash en /sync. El override por empresa (empresa.pin_tecnico_hash) lo pisa si existe. |
Colapso de ráfaga, en una frase
Hasta 5 opiniones del mismo valor en 30 segundos se cuentan como una sola. Es la regla que evita que alguien martille el botón "feliz" 50 veces.
1.3. Estado de conexión de dispositivos — app.monitor¶
Definidos en MonitorDispositivoProperties.java (prefix = "app.monitor"). Derivan el estado de conexión a partir de ultimoContactoTs.
| Parámetro | Default | Qué controla |
|---|---|---|
operarioEnLineaMin |
30 min | Minutos desde el último contacto por debajo de los cuales un APP_OPERARIO se considera EN_LINEA. |
operarioConRetrasoMin |
120 min | Umbral superior: entre 30 y 120 min un APP_OPERARIO está CON_RETRASO; por encima, offline. |
terminalEnLineaMin |
15 min | Minutos por debajo de los cuales una TERMINAL_SMILEY se considera EN_LINEA. |
terminalSinReportarMin |
30 min | Minutos a partir de los cuales una TERMINAL_SMILEY se considera SIN_REPORTAR (genera alerta G3). |
monitorCheckFixedRateMin |
5 min | Intervalo del scheduler G3 que detecta tablets caídas y auto-atiende las que reconectaron. |
Terminal vs app operario tienen umbrales distintos
Una terminal se considera caída a los 30 min (umbral único SIN_REPORTAR). Una app de operario tiene dos escalones: EN_LINEA (< 30 min), CON_RETRASO (30–120 min) y offline (> 120 min). No es el mismo número porque la terminal reporta de forma continua y el operario no.
2. "Limpieza válida" y cuentaParaSla — la regla más importante¶
Una limpieza resetea el SLA del sanitario (es decir, vuelve a poner el reloj de "última limpieza" en cero) solo si cumple TODO lo siguiente. Si falla cualquiera de los criterios, no cuenta y el sanitario sigue acumulando tiempo sin limpiar.
Evidencia: TrabajoRepository.findUltimaLimpiezaValida (TrabajoRepository.java:95-108) y su gemela existsLimpiezaValidaById (líneas 115-127), que aplican los mismos criterios.
| # | Criterio | Campo / condición |
|---|---|---|
| 1 | Es una limpieza (no una supervisión) | tipo = LIMPIEZA |
| 2 | Está cerrada | finTs IS NOT NULL |
| 3 | No es de duración anómala (egreso corto, < duracionMinimaSeg) |
duracionAnomala = false |
| 4 | Cuenta para el SLA | cuentaParaSla = true |
| 5 | Ningún evento la invalida | no existe TrabajoEvento con eventoLimpieza.invalidaLimpieza = true |
Los criterios son AND, no OR
Basta con que uno falle para que la limpieza no resetee el SLA. Una limpieza puede estar perfectamente cerrada y registrada (finTs != null, sin eventos) y aun así no contar si cuentaParaSla = false. Ese es exactamente el caso del trabajo colgado que se ve abajo.
2.1. El default y quién lo cambia¶
Trabajo.cuentaParaSla arranca en Boolean.TRUE por construcción de la entidad (Trabajo.java:78-79). Casi nadie lo baja a false. Solo dos caminos lo tocan:
| Camino | Efecto sobre cuentaParaSla |
Evidencia |
|---|---|---|
| Registro MANUAL (placa rota + motivo de contingencia) | true si NFC; si es MANUAL depende de contingenciaProps.cuentaComoValida() (RB-D3, default true) |
TrabajoTapService.java:523 |
| Cierre por scheduler de trabajo colgado | false — forzado |
SlaSchedulerService.java:150 |
2.2. Los dos cierres automáticos que se comportan distinto¶
Esta es la causa de la confusión clásica: "hay dos cierres automáticos y se comportan distinto". Y es cierto. Ambos dejan el trabajo en estado EGRESO_CIERRE_AUTOMATICO y truncan la duración a cierreAutomaticoMaxMinutes (30 min), pero solo uno toca cuentaParaSla.
| Cierre automático por tap | Cierre por scheduler (colgado) | |
|---|---|---|
| Qué lo dispara | El operario abre en sanitario A y tapea B sin cerrar A | El trabajo quedó abierto más de app.scheduler.trabajos-colgados-max-hours (horas) y nadie lo cerró |
| Quién lo ejecuta | TrabajoTapService (en el sync) |
SlaSchedulerService.cerrarTrabajosColgados() (job @Scheduled, cada 1 hora) |
| Estado resultante | EGRESO_CIERRE_AUTOMATICO |
EGRESO_CIERRE_AUTOMATICO |
| Duración | truncada a 30 min (o al tap de B si es antes) | truncada a 30 min (o a now si es antes) |
cuentaParaSla |
NO se toca → queda true |
se fuerza a false |
| ¿Resetea el SLA? | SÍ (es una limpieza real que el operario olvidó cerrar al instante) | NO (un colgado de horas no es una limpieza real medida) |
| Warning | cierre_automatico |
scheduler_cierre |
| Evidencia | TrabajoTapService.java:334-340 |
SlaSchedulerService.java:141-155 |
La distinción es deliberada (ADR-024)
El cierre por tap sí cuenta porque el operario efectivamente limpió A y se fue a B: la limpieza ocurrió, solo faltó el egreso explícito. El cierre por scheduler no cuenta porque un trabajo abierto durante horas no representa una limpieza medible — pudo haber sido un tap accidental al inicio del turno que quedó huérfano. El comentario en SlaSchedulerService.java:147-150 lo dice textual: "el cierre automático por siguiente ingreso (path del tap, ADR-024) sí cuenta: aquel no toca cuentaParaSla".
flowchart TD
A[Trabajo abierto sin egreso] --> B{¿Quién lo cierra?}
B -->|Operario tapea otro sanitario| C[Cierre por tap]
B -->|Scheduler tras N horas| D[Cierre por scheduler]
C --> E[estado = EGRESO_CIERRE_AUTOMATICO<br/>cuentaParaSla NO se toca = true]
D --> F[estado = EGRESO_CIERRE_AUTOMATICO<br/>cuentaParaSla = false]
E --> G[Resetea el SLA del sanitario]
F --> H[NO resetea el SLA]
3. Cierre automático y truncado de duración¶
Cuando el operario abre en el sanitario A y tapea B sin cerrar A, el sistema cierra A automáticamente. El fin_ts no es simplemente "ahora": se calcula como el mínimo entre el tope de 30 min y el momento del ingreso a B.
Evidencia: calcularCierreAutomatico (TrabajoTapService.java:672-675), invocado desde el flujo de cierre+ingreso (TrabajoTapService.java:334-356):
private Instant calcularCierreAutomatico(Instant inicioAbierto, Instant tapActual) {
Instant maxFin = inicioAbierto.plus(Duration.ofMinutes(props.cierreAutomaticoMaxMinutes()));
return tapActual.isBefore(maxFin) ? tapActual : maxFin;
}
Por qué el mínimo y no el tap directo
Si el operario abrió A a las 09:00 y recién tapea B a las 11:30, sería absurdo registrar 2h30 de limpieza en A. El tope de 30 min corta esa inflación. Pero si tapeó B a las 09:10, el cierre usa las 09:10 reales (10 min), no los 30 de tope. El tope es un techo, no un valor fijo.
4. Reglas de aceptación / rechazo del tap¶
El orden de validación importa: el rol se chequea antes de resolver el tag, y los operarios inactivos no se rechazan.
4.1. Operario con rol ADMIN → ROL_NO_PERMITIDO (no persiste nada)¶
Un operario con rol = ADMIN que tapea devuelve RolNoPermitido y no persiste absolutamente nada: ni el trabajo, ni un TAG_DESCONOCIDO, ni su alerta. El cliente ADMIN purga el tap sin reintentar.
Evidencia: TrabajoTapService.java:200-203.
RolOperario rol = operario.getRol() != null ? operario.getRol() : RolOperario.OPERADOR_LIMPIEZA;
if (rol == RolOperario.ADMIN) {
return new RolNoPermitido(rol);
}
Regla de seguridad de auditoría (2026-06-11)
El chequeo de rol va antes de resolver el tag justamente para que un ADMIN no deje rastro alguno. Si lo movieras después de la resolución del tag, un ADMIN tapeando una placa desconocida generaría un TAG_DESCONOCIDO persistido y su alerta — exactamente lo que esta regla evita. No reordenes este chequeo.
4.2. Operario inactivo → se ACEPTA (con warning + alerta)¶
Contraintuitivamente, un operario inactivo que tapea no se rechaza: el trabajo se crea igual, se le agrega el warning operario_inactivo_offline y se genera la Alerta OPERARIO_INACTIVO_OFFLINE.
Evidencia: el flag se calcula sin cortar el flujo en TrabajoTapService.java:190, se agrega el warning en la línea 194, y la alerta se emite tras el cierre/ingreso (líneas 257-261, 328, 355).
boolean operarioInactivo = !Boolean.TRUE.equals(operario.getActivo());
// ...
if (operarioInactivo) warnings.add(WARN_OPERARIO_INACTIVO);
Por qué no se rechaza
El operario ya está físicamente en el sanitario y ya limpió — rechazar el dato perdería trabajo real hecho en campo. El sistema prefiere registrar y alertar antes que descartar. La inactividad es un problema administrativo (alguien lo dio de baja en el sistema pero sigue trabajando), no una razón para tirar evidencia.
4.3. Resumen de resoluciones del tap¶
| Situación | Resultado (TapResultado) |
¿Persiste trabajo? |
|---|---|---|
Mismo client_uuid ya procesado |
Duplicado |
No (devuelve el existente) |
| Rol ADMIN | RolNoPermitido |
No (nada) |
| Operario inactivo | sigue el flujo normal (Ingreso/Egreso/...) |
Sí + warning + alerta |
Tag no ASIGNADO (stock/baja/inexistente) |
TagDesconocido |
Sí (con tag nulo) + alerta |
| Sanitario inactivo o borrado | SanitarioInactivo |
No |
| Rebote post-egreso (< 8 seg) | RechazadoRebote |
No (registra tap_descartado) |
| MANUAL sin sanitario/motivo válido | ManualInvalido |
No |
5. Acumulación de warnings¶
El trabajo guarda sus warnings en una sola columna warnings NVARCHAR(500), concatenados con |, y truncados a 500 caracteres. No es una tabla de eventos: es un string plano.
Constantes definidas en TrabajoTapService.java:78-86; concatenación y truncado en appendWarnings (TrabajoTapService.java:680-694):
private static void appendWarnings(Trabajo t, List<String> warnings) {
if (warnings == null || warnings.isEmpty()) return;
String joined = String.join("|", warnings);
if (t.getWarnings() == null || t.getWarnings().isBlank()) {
t.setWarnings(joined);
} else {
t.setWarnings(t.getWarnings() + "|" + joined);
}
// Mantener dentro del límite de 500 chars
if (t.getWarnings().length() > 500) t.setWarnings(t.getWarnings().substring(0, 500));
}
Warnings posibles:
| Constante | String almacenado | Cuándo se agrega |
|---|---|---|
WARN_OPERARIO_INACTIVO |
operario_inactivo_offline |
Operario dado de baja que tapea |
WARN_CLOCK_DRIFT |
clock_drift_warning |
Drift entre device y server > 5 min |
WARN_CIERRE_AUTOMATICO |
cierre_automatico |
Cierre por tap en otro sanitario |
WARN_TAG_DESCONOCIDO |
tag_desconocido |
Tag no ASIGNADO |
WARN_SIN_EVENTOS |
sin_eventos |
EGRESO sin eventos (B2) |
WARN_EVENTOS_EXCLUSIVO_COMBINADO |
eventos_exclusivo_combinado |
Evento exclusivo combinado con otros |
WARN_EVENTO_RECHAZADO_PREFIX |
evento_rechazado:<ref> |
Evento inexistente/inactivo/borrado |
WARN_EVENTO_NO_PERMITIDO_PREFIX |
evento_no_permitido:<codigo> |
Evento no permitido para el rol |
scheduler_cierre (WARN_SCHEDULER_CIERRE) |
scheduler_cierre |
Cierre por scheduler de colgado (SlaSchedulerService) |
El truncado a 500 puede perder warnings
Si un trabajo acumula muchos warnings (varios evento_rechazado: con refs largas, por ejemplo), el string se corta a 500 caracteres y los últimos warnings se pierden silenciosamente. No confíes en la columna warnings como auditoría completa cuando hay alta cardinalidad de eventos — para eso están las alertas y la tabla de eventos.
Ver también¶
- Sincronización — el protocolo de sync donde se procesan los taps y se aplican estas reglas.
- Jobs y alertas — los schedulers (
SlaSchedulerService, monitor G3) que cierran colgados y derivan estados de conexión. - Modelo de datos — los campos
cuenta_para_sla,duracion_anomala,warningsy la tabla de eventos.