Saltar a contenido

Arquitectura del sistema

El Sistema de Premios no es una aplicación monolítica ni un enjambre de servicios: es un backend que oficia de fuente de verdad y dos frontends que lo consumen, todo empaquetado en un único artefacto que corre en un servidor del cliente. Esta página explica el modelo mental —dónde vive la verdad, quién le habla a quién y por qué está partido así—, no los endpoints (eso vive en la Referencia de API). Si te llevás una sola idea de acá, que sea esta: el QR del ticket es autocontenido, y de esa decisión cuelga casi toda la arquitectura.

La idea central: el QR autocontenido

Una promoción de sorteo tradicional obliga a sincronizar las cajas con un sistema central: cada ticket que se emite tiene que "avisarle" a la web que existe, para que después la web pueda validarlo. Ese acople es frágil y caro. Si se cae la red entre la caja y el servidor en el momento de la compra, la participación se pierde o el voucher nace inválido. Y montar esa sincronización con ~250 cajas de una cadena de supermercados es un proyecto en sí mismo.

El Sistema de Premios elimina esa dependencia de raíz. Todo lo que la web necesita saber del ticket —sucursal, caja, fecha de emisión, número de comprobante— viaja firmado dentro del token del QR. La caja imprime; la web valida la firma sola, sin preguntarle a nadie.

El contrato con las cajas es el token, y nada más

El sistema no conoce el POS de Caracol ni su backoffice de promociones. No hay una API entre las cajas y este sistema, no hay una tabla compartida, no hay un job de sincronización. La única superficie de contacto es el layout del token (Anexo A): 26 bytes que la caja firma con HMAC-SHA256 y el backend verifica. Ver El token del QR.

Esto tiene una consecuencia que conviene tener presente en toda la documentación: como el ticket puede guardarse y escanearse días después, toda decisión temporal se resuelve por la fecha de emisión firmada en el token, nunca por la fecha en que el cliente lo registró. Vigencia, día del tope diario y a qué sorteo pertenece una chance: todo sale del offset 8 del token. El porqué está en ADR-002, y es un invariante de la CONSTITUTION del proyecto.

Las tres piezas y cómo encajan

El sistema es un backend (la fuente de verdad) y dos frontends con propósitos —y presupuestos de peso— deliberadamente distintos. En el mapa de módulos del proyecto, estas tres piezas son M3, M4 y M5; conviven con piezas de Caracol que quedan fuera de alcance pero se integran.

Módulo Pieza Stack Rol
M3 Backend Java 17 · Spring Boot 4 · SQL Server · Flyway Fuente de verdad: valida vouchers (casos A–E), administra padrón, chances, premios y su ciclo, motor de sorteo + acta, antifraude y la API del gestor.
M4 Landing Preact + Vite (build mínimo) La web que abre el QR. Anónima, de un solo uso, carga instantánea en 3G de salón. Incluye el escaneo del DNI por cámara.
M5 Gestor React 18 · Vite · Tailwind (estilo shadcn) · PWA Panel de administración para Caracol: campañas, premios, sorteos, ganadores, clientes, reportes y modo prueba.
M1 Backoffice de promos (existente, LaEmpresa) Configura las promociones; emite el promoId que viaja en el token. Fuera de alcance.
M2 POS / emisión (existente, LaEmpresa) Imprime el voucher con QR firmado en la caja. Fuera de alcance.
M6 Puesta en marcha (operación) Instalación, HTTPS, ambiente de prueba, salida en vivo. Ver Instalar en el servidor.

Diagrama

Fijate en la línea punteada entre M1 y M2: es la única relación con el mundo de Caracol, y ocurre antes de que el sistema entre en juego. Desde el QR impreso para adelante, todo es de este sistema.

Por qué la landing y el gestor son piezas separadas

No es cosmético. Los dos frontends resuelven problemas opuestos:

  • La landing la abre el cliente en el salón, desde su celular, con la señal 3G que haya. El 99 % de la audiencia entra desde el teléfono, y —en palabras del operador— "de eso depende que lo usen". Su presupuesto es el peso: tiene que pintar la primera pantalla en menos de 1,5 segundos en 3G lento. Por eso corre sobre Preact en lugar de React: mismo código escrito contra la API de React, pero un runtime de ~4 KB en vez de ~45 KB. El bundle inicial bajó de ~51 KB a ~14 KB gzip. El porqué completo está en ADR-019.
  • El gestor lo usa un operador de Caracol desde una oficina, sin presión de red ni de audiencia masiva. Ahí sí conviene el stack completo (React + Tailwind + shadcn parcial + TanStack Query), porque el valor está en la riqueza de la interfaz, no en el peso. Se queda en React 18 sin cambios.

La topología de repos y frontends está decidida en ADR-015: un monorepo con tres módulos, un solo pipeline, versionado atómico de los contratos API ↔ frontends.

El backend manda sobre los contratos

Los frontends no inventan endpoints ni formatos: los consumen. El backend define el contrato JSON (snake_case) y los tipos de la landing y el gestor lo mapean. Si documento y realidad no coinciden, gana el backend. Este criterio es tan fuerte que el arranque del backend valida que la property snake_case esté presente: si falta, toda la API cambiaría de formato y la landing moriría, así que el sistema prefiere no arrancar antes que arrancar roto (ver ADR-029).

El jar único: tres piezas, un artefacto

Acá hay una decisión que sorprende viniendo de la separación anterior: aunque backend, landing y gestor son módulos distintos con builds independientes, se despliegan como un solo jar de Spring Boot.

En la puesta en marcha (M6), el build empaqueta landing/dist bajo static/ y gestor/dist bajo static/gestor/ dentro del mismo jar (perfil Maven full). El backend sirve los tres:

  • / → la landing (lo que abre el QR).
  • /gestor/ → el gestor (autenticado).
  • /api/participacion/* y /api/gestor/* → la API.

Diagrama

La razón es el contexto de despliegue: un solo servidor del cliente, sin nube, con backup diario. Un artefacto único es lo más simple de instalar, versionar y respaldar. No hay contenedores que orquestar ni servicios que coordinar; hay un jar corriendo como servicio de Windows detrás de un reverse proxy que termina HTTPS. La topología es deliberadamente chata porque las cargas son chicas y no justifican nada más.

La configuración vive FUERA del jar

Un matiz importante del jar único: el artefacto no embebe ningún archivo de configuración. Puertos, TLS, rutas de bases y branding, flags por cliente, URL de la base: todo vive en una carpeta 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é archivo falta y dónde ponerlo. Los secretos (clave HMAC, password de la DB) van solo en variables de entorno, nunca en un archivo versionado. El porqué está en ADR-029.

Una participación de punta a punta

Veamos el camino feliz completo: un cliente que compra, escanea y participa por primera vez (caso D, cliente nuevo). El detalle de cada bifurcación —firma inválida, ya usado, fuera de vigencia, tope diario— vive en Los casos de participación; acá seguimos el hilo de una participación exitosa para ver cómo colaboran las piezas.

Diagrama

Los momentos que hacen que esto sea robusto:

  • resolver no consume nada. Es una consulta: dice en qué caso cae el voucher sin registrar la participación. Recién confirmar (caso E) o alta (caso D) consumen la chance. Esto permite que la landing pida el DNI en el momento justo sin quemar el voucher si el cliente abandona.
  • El consumo es atómico a nivel base de datos. La chance se inserta con el hash del token como constraint UNIQUE. Dos envíos simultáneos del mismo voucher —cosa común con 3G inestable y clientes que tocan dos veces— resuelven a exactamente una chance; el segundo choca contra el índice único y se traduce al caso B ("ya participó"), nunca a doble chance. Ver ADR-011 y ADR-024.
  • En la base nunca vive el token en claro, solo su hash. Una filtración de la DB no permite reconstruir direcciones de participación válidas. Es un invariante de seguridad (ver Antifraude y seguridad).
  • El alta es una sola transacción. Cliente + chance + aceptación de bases se insertan juntos o no se inserta nada. Un retry de un formulario cortado resuelve a caso B, no a un cliente a medio crear.

Dónde sigue