Saltar a contenido

Backend WorkDone Sanitarios

El backend es un servicio Spring Boot que concentra el dominio de WorkDone Sanitarios, el sistema de control de limpieza por NFC para sanitarios de aeropuertos y shoppings (producto de LubecaTech, cliente piloto Corpal en Córdoba, AR). Acá vive el corazón del negocio: el tap NFC, el endpoint de sincronización offline-first, la máquina de estados de los tags, la gestión de alertas y SLA, y el BackOffice React embebido.

Esta página es la referencia del backend como componente: stack, estructura, autenticación, base de datos y convenciones que un dev nuevo tiene que conocer antes de tocar nada.

Repos relacionados

Este repo es solo el backend Spring Boot. La app móvil Android y el kiosk Smiley viven en repos separados. Para entender el modelo de datos del dominio mirá el modelo de datos; para el vocabulario del proyecto, el glosario.

Stack

Tecnología Versión Para qué
Java 25 (LTS-track; mínimo Java 21) Lenguaje base — aprovecha pattern matching for switch, sealed classes, records con compact constructors y virtual threads
Spring Boot 4.0.6 Framework de aplicación
JHipster 9.1.0 Scaffolder inicial (jhipster jdl workdone.jdl)
Node 24+ (25 también funciona) Requerido para correr el JHipster CLI
Maven 3.9.16+ Build (no Gradle)
JWT Auth con refresh tokens
Liquibase Migraciones de schema
MapStruct Mapeo de DTOs
Caffeine Cache + Hibernate 2nd level
mssql-jdbc 12.6+ (classifier jre21+) Driver de SQL Server (prod)
H2 DB de desarrollo, file-based en ./target/h2db/
MSSQL Server 2019+ DB de producción, en EC2 Windows

Features de Java 25 que se aprovechan

  • Pattern matching for switch (final, ya no preview) — usado en el switch sobre EstadoTrabajo en TrabajoTapService.
  • Sealed classes — para los tipos cerrados de resultado del tap (Ingreso, Egreso, CierreAutomatico, Rechazo).
  • Records con compact constructors — para los DTOs del API.
  • Virtual threads — habilitados vía spring.threads.virtual.enabled=true para el endpoint /sync.

Spring Boot 4 dropea APIs deprecados de Spring 5

Si copiás snippets de blogs viejos pueden no compilar. Otros cambios a tener presentes: Testcontainers usa DynamicPropertySource; Hibernate 6.6+ cambió el API de Session.find() con tipos genéricos.

Estructura del proyecto

El proyecto se genera con JHipster y luego se extiende con paquetes custom (marcados con ⚠ en el árbol original). El paquete base es ar.com.lubeca.workdone.

src/main/java/ar/com/lubeca/workdone/
├── WorkdoneApp.java
├── config/                      # Spring config (security, cache, jpa, jackson)
├── domain/                      # Entities JPA (generadas)
├── repository/                  # Repositories Spring Data (generadas)
├── service/                     # Services + impls (generadas)
│   ├── dto/                     # DTOs (generadas + sync/ nfc/ admin/ auth/ custom)
│   ├── mapper/                  # Mappers MapStruct (generadas)
│   ├── sync/                    # ⚠ CUSTOM — SyncService + TrabajoTapService (corazón del negocio)
│   ├── nfc/                     # ⚠ CUSTOM — TagNfcLifecycleService (máquina de estados del tag)
│   ├── admin/                   # ⚠ CUSTOM — Dashboard, Reportes, TrabajoConsulta, Alertas
│   ├── auth/                    # ⚠ CUSTOM — AppAuthService (JWT móvil + throttling)
│   ├── scheduler/               # ⚠ CUSTOM — SlaSchedulerService (3 tareas)
│   └── util/                    # ⚠ CUSTOM — UuidTagNormalizer (todo uuid pasa por acá)
├── web/rest/                    # Controllers REST (generadas)
│   ├── app/                     # ⚠ CUSTOM — app móvil (Auth, Me, Sync, Trabajo, Nfc, EventoLimpieza)
│   └── admin/                   # ⚠ CUSTOM — backoffice (Dashboard, Reporte, Alerta, Operario, Trabajo, Nfc)
└── security/                    # JWT, filtros (modificada)

src/main/webapp/                 # ⚠ CUSTOM — BackOffice React (Vite+TS+shadcn), build → target/classes/static/

src/main/resources/
├── config/
│   ├── application.yml
│   ├── application-dev.yml
│   ├── application-prod.yml
│   └── liquibase/changelog/     # migraciones NNN_descripcion.xml
└── i18n/                        # JHipster genera, no se usa

Capas

  • domain/ + repository/ — entidades JPA y repositorios Spring Data, en su mayoría generados por JHipster.
  • service/ — lógica de negocio. Los subpaquetes custom (sync, nfc, admin, auth, scheduler, util) son donde vive el dominio real. service/sync/ es el corazón: SyncService y TrabajoTapService.
  • web/rest/ — controllers REST, separados en app/ (móvil) y admin/ (backoffice), más los controllers del módulo Smiley en web/rest/smiley/.
  • security/ — configuración JWT y filtros, incluido el filtro de api_key del device.
  • src/main/webapp/ — el BackOffice React, que en build prod se embebe en el JAR (target/classes/static/).

Servicios que un dev nuevo debe ubicar primero

TrabajoTapService (toggle ingreso/egreso del tap), SyncService (orquesta el endpoint /sync), TagNfcLifecycleService (máquina de estados del tag + auditoría) y UuidTagNormalizer (toda normalización de UUID de tag pasa por acá, sin excepción).

Autenticación y autorización

El sistema tiene dos mundos de autenticación distintos: personas (JWT) y dispositivos (api_key). No se mezclan.

Personas — JWT con refresh tokens

Actor Endpoint de login Tabla Authority
BackOffice web (admin) POST /api/authenticate (JHipster estándar) jhi_user ROLE_ADMIN
Operario móvil POST /api/v1/app/auth/login-password · login-pin · refresh · logout operario ROLE_OPERARIO
Cliente (portal de transparencia) flujo de activación por mail jhi_user ROLE_CLIENTE

Detalles del login móvil:

  • JWT de 15 minutos, con refresh por rotación de token.
  • Throttling: 5 fallos por usuario+device devuelven 429 por 10 minutos.
  • Header X-Device-UUID obligatorio en login y refresh (si falta → 400).
  • logout es público: valida por el hash del refresh token, así que el access token puede haber expirado.
  • Las responses de login y GET /api/v1/app/me incluyen el rol del operario: ADMIN | SUPERVISOR | OPERADOR_LIMPIEZA.

El operario móvil NO es lo mismo que el usuario web

El operario móvil usa la authority ROLE_OPERARIO (no ROLE_USER, que es el rol web) y accede exclusivamente a /api/v1/app/*. El admin web usa ROLE_ADMIN y accede al CRUD scaffold (/api/*) y a /api/v1/admin/*. Un operario que pegue a /api/<entidad> recibe 403, y un cliente que pegue a /api/v1/app/* también recibe 403. La separación de authorities está hecha a propósito (ADR-040) y está validada en el código: SecurityConfiguration.java:106 (/api/v1/app/**hasAuthority(OPERARIO)) y AuthoritiesConstants.java:18 (OPERARIO = "ROLE_OPERARIO").

Dispositivos — api_key (kiosk Smiley)

La terminal Smiley no tiene operario, así que no usa JWT. Se autentica por la api_key del dispositivo:

  • Headers X-Device-Uuid + X-Api-Key (filtro SmileyApiKeyAuthFilter).
  • El backend valida que el dispositivo exista, esté activo, no esté borrado y sea tipo=TERMINAL_SMILEY, y verifica la api_key con BCrypt.
  • Headers faltantes o api_key inválida → 401.
  • La authority resultante es ROLE_TERMINAL.
  • El sanitario lo determina el server por el vínculo del dispositivo — la terminal no puede opinar por otro sanitario.

La api_key se entrega en texto plano una sola vez, al vincular la terminal (POST /api/v1/smiley/vincular); el backend solo persiste su hash BCrypt.

Matriz de authorities

Prefijo de ruta Authority requerida Quién
/api/v1/app/* ROLE_OPERARIO operario móvil
/api/v1/admin/* ROLE_ADMIN BackOffice web
/api/v1/cliente/* ROLE_CLIENTE portal de transparencia (solo lectura)
/api/v1/smiley/* (salvo vincular/ping) ROLE_TERMINAL terminal Smiley (api_key)
/api/v1/publico/* sin auth (anónimo, rate-limited por IP) QR público
/api/* (CRUD scaffold) ROLE_ADMIN BackOffice web

Revocación de dispositivos

Un dispositivo con activo=false que llama a POST /api/v1/app/sync recibe 401 con cuerpo DEVICE_REVOCADO (validado en cada sync, sin cache). El cliente móvil ante ese 401 debe detener el sync, borrar credenciales del device y bloquear la app.

Base de datos por ambiente

Ambiente Motor Detalle
Desarrollo H2 File-based en ./target/h2db/ (incluye DevTools)
Producción MSSQL Server 2019+ En EC2 Windows, compartida con otros productos de LubecaTech

También se puede levantar dev contra un MSSQL local nativo en Windows (sin Docker) con el perfil dev,mssql. La suite completa con Testcontainers MSSQL sí requiere Docker.

Migraciones — Liquibase

El schema y los seeds se manejan con Liquibase, una migration por feature.

Reglas (en convenciones):

  • Numerar NNN_descripcion.xml con N ≥ 002 (la 001 la genera JHipster).
  • Nunca editar una migration anterior.
  • Para SQL específico de MSSQL (ROWVERSION, índices filtrados), usar <sql dbms="mssql">.
  • Si excepcionalmente se edita una migration ya aplicada (cambia su checksum), hay que recrear el H2 local antes de levantar (mvn clean o borrar target/h2db). CI y prod no se ven afectados.

Seeds por contexto

Los seeds de catálogo usan context="dev,prod"; los seeds de datos de prueba usan context="dev". Los seeds de dev incluyen los operarios de prueba (ver más abajo).

Credenciales de desarrollo

Hay dos mundos de auth con credenciales distintas en dev:

  • BackOffice web (/api/authenticate, tabla jhi_user): admin / admin (solo dev). En prod, la migration 031 (context=prod) borra el user scaffold y resetea el password de admin al bcrypt de la env var PROD_ADMIN_PASSWORD_HASH — el deploy DEBE exportarla, si no admin no autentica (fail-closed).
  • App móvil (/api/v1/app/auth/*, tabla operario), seedeados en dev:
usuario PIN password rol
jperez 1234 Pa$$w0rd! ADMIN
msuarez 5678 Pa$$w0rd! OPERADOR_LIMPIEZA
rgomez 1234 Pa$$w0rd! SUPERVISOR

admin no sirve para la app móvil

admin no es un operario. En prod, el supervisor/admin operario se crean por ABM o por UPDATE operario SET rol=....

Convenciones clave

Estas son las convenciones que un dev nuevo tiene que conocer antes de escribir código. Vienen del CLAUDE.md del backend.

Nombres

  • Entidades: PascalCase singular (Sanitario, TagNfc, Trabajo).
  • Tablas: snake_case singular (sanitario, tag_nfc, trabajo).
  • DTOs: XxxDTO para entrada, XxxResponseDTO para salida custom.
  • Services: interface XxxService + impl XxxServiceImpl.
  • Endpoints custom: /api/v1/app/* (móvil), /api/v1/admin/* (backoffice), /api/v1/cliente/* (portal), /api/v1/publico/* (QR público). Los endpoints generados por JHipster quedan en /api/* (sin /v1) por compatibilidad.

Reglas de oro del negocio

  1. Antes de tocar /sync o /tap, leer docs/SYNC_PROTOCOL.md entero. No es opcional. (Ver también Sincronización.)
  2. Idempotencia siempre. Cualquier endpoint que mute datos desde la app móvil debe ser idempotente vía client_uuid o equivalente.
  3. El timestamp del teléfono es la verdad para inicio_ts / fin_ts. El server solo registra server_received_ts.
  4. Nunca borrar físicamente catálogos. Soft delete con deleted_at.
  5. Hashes con BCrypt cost 12 para password y PIN. Usar BCryptPasswordEncoder de Spring Security.
  6. No exponer hashes en responses. @JsonIgnore en los DTOs de salida.
  7. Concurrencia de operarios permitida. Dos operarios pueden tener trabajos abiertos sobre el mismo sanitario.
  8. El backend confía en el orden cronológico de trabajos_pendientes. El cliente garantiza orden por ts_local_device ASC.
  9. Docs antes del commit. Siempre actualizar los .md que correspondan al cambio (API, modelo, decisiones, sync, estado) y commitear código y docs juntos.

Soft delete

Las entidades de catálogo tienen deleted_at con manejo manual (campo deletedAt en la entity; los services/queries filtran donde corresponde). No se usan @SQLDelete/@Where: el sync necesita ver los eliminados para propagar las bajas, y esas anotaciones lo harían imposible.

El soft-delete es frágil ante regeneración con JHipster

El delete(id) de los 7 service impls de catálogos sincronizados (Sanitario, TagNfc, Operario, Sucursal, Sector, TipoSanitario, EventoLimpieza) hace soft-delete (deletedAt + updatedAt + activo=false, nunca deleteById). Sus tests generados usan assertSameRepositoryCount, no assertDecrementedRepositoryCount. Si regenerás esas entidades con JHipster, hay que re-aplicar ambos cambios.

Auditoría

Todas las entidades de catálogo extienden AbstractAuditingEntity de JHipster, que auto-popula created_by / last_modified_by desde el SecurityContext.

Tests

  • Unit tests con JUnit 5 + Mockito.
  • Integration tests con @SpringBootTest + Testcontainers para MSSQL. Los *IT corren con failsafe, no con mvn test.
  • Mínimo 1 test por endpoint custom (happy path + 2 edge cases).
  • Los tests de TrabajoTapService son obligatorios: deben cubrir los casos especiales del protocolo de sync.
  • Property-based tests con jqwik para invariantes de funciones puras críticas (ej. UuidTagNormalizerPropertyTest).

Comandos comunes

# Levantar en dev (H2 + DevTools)
./mvnw

# Tests unitarios (surefire — excluye los *IT)
./mvnw test

# Tests de integración (failsafe — los *IT corren acá)
./mvnw test-compile failsafe:integration-test failsafe:verify -Dit.test='*IT' -DfailIfNoTests=false

# Suite completa con Testcontainers MSSQL (requiere Docker)
./mvnw verify

# Build prod (JAR con el BackOffice embebido)
./mvnw -Pprod clean package -DskipTests

Swagger UI en dev: http://localhost:8080/swagger-ui/index.html · OpenAPI JSON: http://localhost:8080/v3/api-docs.