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
switchsobreEstadoTrabajoenTrabajoTapService. - 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=truepara 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:SyncServiceyTrabajoTapService.web/rest/— controllers REST, separados enapp/(móvil) yadmin/(backoffice), más los controllers del módulo Smiley enweb/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
refreshpor rotación de token. - Throttling: 5 fallos por usuario+device devuelven
429por 10 minutos. - Header
X-Device-UUIDobligatorio en login y refresh (si falta →400). logoutes 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/meincluyen elroldel 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(filtroSmileyApiKeyAuthFilter). - 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
sanitariolo 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.xmlcon N ≥ 002 (la001la 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 cleano borrartarget/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, tablajhi_user):admin/admin(solo dev). En prod, la migration 031 (context=prod) borra eluserscaffold y resetea el password deadminal bcrypt de la env varPROD_ADMIN_PASSWORD_HASH— el deploy DEBE exportarla, si noadminno autentica (fail-closed). - App móvil (
/api/v1/app/auth/*, tablaoperario), 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:
XxxDTOpara entrada,XxxResponseDTOpara salida custom. - Services: interface
XxxService+ implXxxServiceImpl. - 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¶
- Antes de tocar
/synco/tap, leerdocs/SYNC_PROTOCOL.mdentero. No es opcional. (Ver también Sincronización.) - Idempotencia siempre. Cualquier endpoint que mute datos desde la app móvil debe ser idempotente vía
client_uuido equivalente. - El timestamp del teléfono es la verdad para
inicio_ts/fin_ts. El server solo registraserver_received_ts. - Nunca borrar físicamente catálogos. Soft delete con
deleted_at. - Hashes con BCrypt cost 12 para password y PIN. Usar
BCryptPasswordEncoderde Spring Security. - No exponer hashes en responses.
@JsonIgnoreen los DTOs de salida. - Concurrencia de operarios permitida. Dos operarios pueden tener trabajos abiertos sobre el mismo sanitario.
- El backend confía en el orden cronológico de
trabajos_pendientes. El cliente garantiza orden ports_local_device ASC. - Docs antes del commit. Siempre actualizar los
.mdque 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*ITcorren con failsafe, no conmvn test. - Mínimo 1 test por endpoint custom (happy path + 2 edge cases).
- Los tests de
TrabajoTapServiceson 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.