Saltar a contenido

Arquitectura del sistema

BeneficiosCenter es un único backend Spring Boot con consola embebida, no un enjambre de servicios. Esta página explica el modelo mental —dónde vive la verdad, qué garantiza la velocidad y por qué la auditoría nunca frena un cobro—, no los endpoints (eso vive en la Referencia de API). Si te llevás una sola idea de acá, que sea esta: la respuesta al gateway jamás espera a la base de datos (INV-O01), y de esa decisión cuelga casi toda la arquitectura.

El problema que resuelve

El GatewayQR consulta a BeneficiosCenter en medio de un cobro y después debe reenviar la intención al autorizador. El objetivo es responder en menos de 20 ms (p99). La configuración de beneficios cambia pocas veces al día; las consultas llegan miles de veces al día. Cada consulta debe quedar auditada igual, aplique o no un beneficio.

Resolver esto contra la base en cada consulta ata la latencia y la disponibilidad del gateway a SQL Server. Auditar de forma sincrónica (sin persistir la traza no hay respuesta) daría trazas garantizadas, pero una base lenta frenaría cobros. Ninguna de las dos alternativas es aceptable (ver ADR-002).

Ruta caliente + snapshot en memoria + auditoría asincrónica

La solución tiene tres piezas que trabajan juntas:

flowchart LR
    GW[GatewayQR] -->|"POST /resolver"| RC[Ruta caliente<br/>BenefitResolver]
    RC -->|lee| SNAP[("ConfiguracionActiva<br/>snapshot inmutable en memoria")]
    RC -->|responde benefits[]| GW
    RC -.->|"offer() no bloqueante"| COLA[("Cola acotada<br/>de auditoría")]
    COLA --> CONSUMER["Consumidor<br/>(virtual threads, en lotes)"]
    CONSUMER --> DB[(SQL Server<br/>tabla consulta)]
    CONSOLA[Consola web] -->|guarda beneficio/sucursal/tipo| EVENTO["Evento ConfiguracionCambiada"]
    EVENTO --> SNAP
    REFRESH["Refresco periódico<br/>(red de seguridad, cada 60s)"] --> SNAP

    style RC fill:#E0F848,color:#111
    style SNAP fill:#F3FBB9,color:#111
  1. Snapshot inmutable en memoria (ConfiguracionActiva). Cada request resuelve contra la referencia que tomó al entrar; nunca ve un estado intermedio. Guardar un beneficio, tipo de cliente o sucursal desde la consola publica un evento que reconstruye el snapshot y lo intercambia atómicamente (INV-O03: rige en la próxima consulta, sin reinicio). Un refresco periódico (por defecto cada 60 s, bc.resolver.refresh-seconds) actúa como red de seguridad ante cualquier fallo del evento.
  2. BenefitResolver — función pura, sin Spring, sin I/O. Toma un request y la ConfiguracionActiva, y devuelve el resultado. Es la pieza que se prueba al 100 % (ver Matriz de aceptación) porque no depende de nada externo.
  3. Auditoría asincrónica. Cada consulta se intenta encolar en una cola acotada (por defecto 10 000 elementos, bc.auditoria.queue-size). offer() nunca bloquea: si la cola está llena, el excedente se cuenta en auditoria.perdidas en vez de frenar la respuesta al gateway. Un consumidor en virtual threads escribe en lotes contra SQL Server.

Invariante que no se negocia

INV-O01 — la respuesta al gateway jamás espera a la base de datos. Con la base detenida, POST /resolver sigue respondiendo dentro del presupuesto (AC-18) y auditoria.perdidas incrementa en vez de ocultar la pérdida (INV-O02). Se acepta perder traza en un incidente prolongado porque el gateway conserva su propio registro de la transacción, y un cobro frenado es peor que una traza faltante (RISKS.md, A-01).

Componentes del sistema

flowchart TB
    subgraph Externos["Fuera de este sistema"]
        POS[POS de DINO]
        GW[GatewayQR — Tipre]
        AUT[Autorizador DINI/TECSO]
    end

    subgraph BC["BeneficiosCenter — un único jar"]
        API["API de resolución<br/>POST /api/v1/beneficios/resolver<br/>auth: API key"]
        ADMIN["API de administración<br/>ABM + tablero + export<br/>auth: Basic (ADMIN/OPERADOR)"]
        CONSOLA["Consola web (React)<br/>servida en /gestor/"]
        RESOLVER[BenefitResolver]
        AUDIT[ColaAuditoria + consumidor]
        DB[(SQL Server<br/>Flyway)]
    end

    POS --> GW
    GW -->|X-Api-Key| API
    API --> RESOLVER
    RESOLVER --> AUDIT
    AUDIT --> DB
    CONSOLA -->|Basic auth| ADMIN
    ADMIN --> DB
    ADMIN -->|ConfiguracionCambiada| RESOLVER
    GW --> AUT

    style API fill:#E0F848,color:#111
Pieza Rol
API de resolución Único punto de contacto con el GatewayQR. Autenticada por X-Api-Key (INV-S01), en su propia cadena de seguridad.
API de administración + consola ABM de beneficios, tipos de cliente, sucursales y usuarios; tablero; auditoría con filtros. Autenticada por HTTP Basic stateless (ver ADR-003), roles ADMIN/OPERADOR verificados en el servidor (INV-S04).
SQL Server + Flyway Persiste configuración y auditoría. Fuera de la ruta caliente. Migraciones aditivas (ver Base de datos).

El jar único

Backend y consola tienen builds independientes, pero se despliegan como un solo jar de Spring Boot (ver ADR-001): la consola compilada se empaqueta bajo static/gestor/ y el mismo proceso sirve la API de resolución, la API de administración y la consola.

La razón es el contexto de despliegue: un servidor del cliente, sin nube, con backup diario. Un artefacto único es lo más simple de instalar, versionar y respaldar — un jar corriendo como servicio de Windows detrás de un reverse proxy que termina HTTPS (ver Despliegue).

La configuración vive FUERA del jar

El artefacto no embebe ningún archivo de configuración. Cadena de conexión, API key del gateway, admin inicial, retención de auditoría, branding: todo vive en config/ al lado del jar. Si esa carpeta falta, el sistema no arranca en silencio con defaults invisibles: falla rápido con un mensaje que dice qué falta (INV-O04). Los secretos nunca van en un archivo versionado.

Seguridad de las dos superficies

BeneficiosCenter expone dos superficies con modelos de autenticación distintos, en cadenas de seguridad separadas:

  • La API de resolución (el gateway) usa X-Api-Key. Sin clave válida, 401 sin evaluar nada (INV-S01).
  • La consola y su API de administración (personas) usan HTTP Basic stateless sobre HTTPS, con usuarios en tabla y contraseñas hasheadas con BCrypt (INV-S02). El rol OPERADOR no puede ejecutar ninguna escritura, y esa verificación vive en el servidor, no solo en la consola (INV-S04, ver ADR-003).
  • TLS y exposición pública son responsabilidad del reverse proxy; el jar nunca escucha directamente en Internet (INV-S05).

Dónde sigue