Saltar a contenido

Ciclo de vida de los tags NFC

Si recién entrás al proyecto, esta es la página que explica cómo nace, se asigna, se reasigna y se da de baja una placa NFC en WorkDone Sanitarios, y qué transiciones de estado son válidas (y cuáles no). Una placa (tag_nfc) es el chip físico que el personal acerca con el teléfono para registrar un trabajo; antes de poder usarse, tiene que recorrer un ciclo de vida controlado.

La fuente de verdad de este documento es el backend (Spring Boot / JHipster), concretamente TagNfcLifecycleService: el servicio que concentra TODAS las transiciones de estado del tag. Ningún otro componente cambia el estado de un tag por su cuenta.

Punto de partida

Cada operación recibe un uuid crudo (leído del chip o recibido por API), un contexto de auditoría (AccionNfcContexto: operario, dispositivo, notas — todos nullable) y devuelve el TagNfc resultante. Las transiciones inválidas lanzan TransicionInvalidaException; los conflictos de asignación, ConflictoAsignacionException. Cada transición exitosa persiste un registro inmutable en tag_nfc_evento.

Para el contexto del modelo de tablas, ver Modelo de datos. Para cómo estas operaciones viajan offline desde la app, ver Sincronización.

Máquina de estados

Un tag tiene tres estados (EstadoTag): EN_STOCK, ASIGNADO y BAJA. El alta lo crea directamente en EN_STOCK. Las transiciones entre estados están todas mediadas por una acción concreta del TagNfcLifecycleService.

stateDiagram-v2
    [*] --> EN_STOCK: ALTA_STOCK

    EN_STOCK --> ASIGNADO: ASIGNACION
    ASIGNADO --> EN_STOCK: DESASIGNACION
    ASIGNADO --> ASIGNADO: REASIGNACION (otro sanitario)

    EN_STOCK --> BAJA: BAJA
    BAJA --> EN_STOCK: RE_ALTA (solo web)

    note right of ASIGNADO
        REASIGNACION es un solo
        paso atomico: ASIGNADO -> ASIGNADO'
        (decision 2026-06-10)
    end note

Lectura del diagrama

  • ALTA_STOCK es idempotente: si el tag ya existe, devuelve el existente sin generar evento.
  • REASIGNACION es un bucle sobre ASIGNADO: el tag no pasa por EN_STOCK; cambia de sanitario en una sola operación atómica.
  • BAJA sale solo desde EN_STOCK. Un tag ASIGNADO debe desasignarse antes de darse de baja.
  • RE_ALTA (BAJA → EN_STOCK) está disponible solo desde web (BackOffice), nunca por /sync.

Acciones

Cada acción valida el estado de origen esperado, aplica el cambio y persiste un TagNfcEvento con la AccionTagNfc correspondiente. La siguiente tabla resume qué hace cada una.

Acción De → A Quién puede ejecutarla Evento que genera
ALTA_STOCK [*]EN_STOCK Web (BackOffice) y mobile vía /sync ALTA_STOCKsalvo que el tag ya exista (idempotente, no genera evento)
ASIGNACION EN_STOCKASIGNADO Web y mobile vía /sync ASIGNACION (con sanitarioNuevo)
DESASIGNACION ASIGNADOEN_STOCK Web y mobile vía /sync DESASIGNACION (con sanitarioAnterior)
REASIGNACION ASIGNADOASIGNADO' Web y mobile vía /sync REASIGNACION (un solo evento con sanitarioAnterior + sanitarioNuevo)
BAJA EN_STOCKBAJA Web y mobile vía /sync BAJA
RE_ALTA BAJAEN_STOCK Solo web (BackOffice) RE_ALTA

Mobile no envía todas las acciones

El procesador de la cola NFC del sync (SyncService.procesarUnNfcEvento) sólo despacha ALTA_STOCK, ASIGNACION, DESASIGNACION y REASIGNACION. RE_ALTA y BAJA no están en ese switch: RE_ALTA es exclusiva de web por diseño. Cualquier otra acción recibida por sync cae en el default y se marca como ERROR.

ALTA_STOCK — alta idempotente

Crea el tag en EN_STOCK con activo = false. Es idempotente por uuid: si el uuid normalizado ya existe en tag_nfc, devuelve el tag existente sin crear evento ni modificar nada. Esto es clave para la idempotencia best-effort del sync (ver más abajo).

ASIGNACION — vincular a un sanitario

Requiere que el tag esté EN_STOCK y que el sanitario destino no tenga ya un tag ASIGNADO. Pasa el tag a ASIGNADO, le setea el sanitario, asignadoEn y activo = true.

Conflicto: el primero en sincronizar gana

Si el tag no está EN_STOCK, o si el sanitario ya tiene un tag ASIGNADO, se lanza ConflictoAsignacionException. En el sync esto se traduce a CONFLICTO_ASIGNACION. Cuando dos dispositivos asignan offline al mismo sanitario, el primero en el orden de sincronización gana y el segundo recibe el conflicto. El orden lo define ts_local_device ASC.

DESASIGNACION — devolver a stock

Sólo válida desde ASIGNADO. Limpia el sanitario, asignadoEn, pone activo = false y devuelve el tag a EN_STOCK. Guarda el sanitarioAnterior en el evento.

REASIGNACION — mover entre sanitarios (atómica)

Mueve un tag de un sanitario a otro en un solo paso atómico, sin pasar por EN_STOCK. El tag permanece ASIGNADO; sólo cambian sanitario y asignadoEn.

Decisión 2026-06-10: un solo evento

REASIGNACION no es "desasignar + asignar". Genera un único TagNfcEvento con sanitarioAnterior y sanitarioNuevo poblados. Esto preserva la trazabilidad del movimiento como una sola operación en la auditoría.

Caso borde: si el sanitario nuevo es el mismo que el anterior, no se valida el conflicto (no tendría sentido chequear que el sanitario "ya tiene un tag" cuando ese tag es justamente el que estamos moviendo). En cualquier otro caso, el destino no puede tener un tag ASIGNADO, o se lanza ConflictoAsignacionException.

BAJA — desactivar definitivamente

Sólo válida desde EN_STOCK. Pone el tag en BAJA, setea dadoBajaEn y activo = false. Un tag ASIGNADO no puede darse de baja directamente: hay que desasignarlo primero.

RE_ALTA — reactivar desde baja (solo web)

Devuelve un tag desde BAJA a EN_STOCK (limpia dadoBajaEn, activo = false). Disponible únicamente desde web (BackOffice); no se procesa por /sync.

Transiciones inválidas

Cada método valida el estado de origen esperado antes de aplicar el cambio. Si el tag está en un estado distinto del esperado, se lanza TransicionInvalidaException, que en el sync se reporta como TRANSICION_INVALIDA. Además, si el uuid no existe en tag_nfc, resolverTag lanza la misma excepción (tag no registrado).

Estas son las combinaciones que rechazan la operación:

Acción intentada Estado del tag Resultado
ASIGNACION ASIGNADO o BAJA ConflictoAsignacionException (debe estar EN_STOCK)
DESASIGNACION EN_STOCK o BAJA TransicionInvalidaException (solo desde ASIGNADO)
REASIGNACION EN_STOCK o BAJA TransicionInvalidaException (solo desde ASIGNADO)
BAJA ASIGNADO TransicionInvalidaException (desasignar primero)
BAJA BAJA TransicionInvalidaException (ya está en BAJA)
RE_ALTA EN_STOCK o ASIGNADO TransicionInvalidaException (solo desde BAJA)
Cualquiera uuid inexistente TransicionInvalidaException (tag no registrado)

Conflicto vs. transición inválida

ASIGNACION sobre un tag que no está EN_STOCK lanza ConflictoAsignacionException (no TransicionInvalidaException), porque el caso típico es una carrera de asignación, no un estado imposible. La distinción importa: en el sync producen estados distintos (CONFLICTO_ASIGNACION vs TRANSICION_INVALIDA).

Normalización de UUID

Todo punto de entrada de uuid pasa por UuidTagNormalizer.normalize() antes de cualquier búsqueda o persistencia. La forma canónica es: null-safe, sin espacios (internos y de borde) y en mayúsculas (raw.replaceAll("\\s+", "").toUpperCase()).

Cada método del TagNfcLifecycleService normaliza el uuid en su primera línea. TrabajoTapService también normaliza para resolver el tag al procesar un tap (el uuid_tag_leido del Trabajo se persiste crudo, como snapshot del tap).

El backend es defensivo a propósito

Aunque la app móvil ya normalice el uuid antes de enviarlo, el backend vuelve a normalizar en cada entrada. No se confía en que el cliente haya hecho el trabajo: dos lecturas del mismo chip con distinto formato (con/sin guiones, mayúsculas/minúsculas) deben resolver al mismo registro tag_nfc. Esto sostiene la whitelist por uuid normalizado del ADR de sync.

Auditoría

Cada transición exitosa persiste un TagNfcEvento (tabla inmutable tag_nfc_evento). El evento captura: el tag, el uuidTag (ya normalizado), la AccionTagNfc, sanitarioAnterior, sanitarioNuevo, operario, dispositivo, fecha y notas.

Operario y dispositivo son nullable

Los campos operario y dispositivo del contexto de auditoría son nullable. AccionNfcContexto.vacio() los deja en null para operaciones web o de sistema que no tienen un actor humano / dispositivo asociado (por ejemplo, backfill o tareas programáticas). No asumas que todo evento NFC tiene operario.

Campo Poblado en
sanitarioAnterior DESASIGNACION, REASIGNACION
sanitarioNuevo ASIGNACION, REASIGNACION
operario / dispositivo Operaciones con actor humano (nullable en web/sistema)

Estados de procesamiento NFC en el sync

Cuando los eventos NFC viajan en la cola del sync, cada ítem se procesa de forma aislada (un ítem que falla no aborta el sync completo) y se devuelve con un EstadoNfcEventoProcesado. La autorización es estricta: solo el operario con rol ADMIN puede enviar ítems NFC; si no lo es y la cola no está vacía, todos los ítems se rechazan con ROL_INSUFICIENTE sin abortar.

Estado Significado
OK Transición ejecutada con éxito.
CONFLICTO_ASIGNACION El sanitario ya tiene un tag ASIGNADO, o el tag no está EN_STOCK (ConflictoAsignacionException).
DUPLICADO_IGNORADO client_uuid ya visto (best-effort, según el estado del tag).
TRANSICION_INVALIDA La máquina de estados rechaza la operación (TransicionInvalidaException).
ROL_INSUFICIENTE El operario del sync no es ADMIN — ítem ignorado sin abortar.
ERROR Excepción inesperada o acción desconocida — no aborta el sync completo.

Más sobre el protocolo de sincronización, orden de procesamiento y casos offline en Sincronización.

Limitación real: idempotencia best-effort (riesgo a evaluar)

La idempotencia de los eventos NFC en el sync es best-effort, no estricta. No existe una tabla nfc_evento_procesado que persista los client_uuid ya vistos. La idempotencia depende enteramente del comportamiento idempotente de cada acción — principalmente que ALTA_STOCK devuelve el tag existente sin tocar nada.

Consecuencia: el sistema no puede distinguir un reintento genuino de una operación nueva con el mismo uuid. Para idempotencia estricta haría falta persistir el client_uuid en una tabla auxiliar. El código lo marca explícitamente:

// TODO v2.1: tabla nfc_evento_procesado para idempotencia estricta por client_uuid.

Tratá esto como un riesgo a evaluar antes de cualquier escenario con reintentos agresivos o redelivery de la cola NFC.