Arquitectura del ecosistema AutoCompraMod¶
AutoCompraMod no es una aplicación monolítica clásica ni un enjambre de microservicios: es un monolito modular que oficia de backend único, y una terminal cliente que lo consume. Esta página explica por qué está dividido así, cómo se comunican las piezas, cómo se autentica cada actor y dónde está la verdad. La idea no es que memorices endpoints — eso vive en la referencia — sino que entiendas el modelo mental: quién manda, qué frontera no se cruza y por qué.
La forma del sistema¶
El ecosistema tiene tres piezas: el backend, su panel de operación (Cockpit, que vive dentro del backend) y la terminal de autoservicio (TiprePOS, en su propio repo).
graph TD
subgraph Repo2["Repo: TiprePOS"]
POS["Terminal de autoservicio<br/>Flutter · BLoC · Isar<br/>(corre en desktop)"]
end
subgraph Repo1["Repo: AutoCompraMod"]
subgraph Back["Backend — monolito modular (Spring Modulith)"]
TICKETS["tickets<br/>(orquestador)"]
CAT["catalogo"]
PAG["pagos"]
PRO["promos"]
ENV["envases"]
SHARED["shared (kernel OPEN)"]
TICKETS --> CAT
TICKETS --> PAG
TICKETS --> PRO
TICKETS --> ENV
end
COCKPIT["Cockpit (monitoring)<br/>React · Vite · servido por el backend"]
end
POS <-->|"REST /autocompras/v1"| TICKETS
COCKPIT -->|"REST /monitoring/*"| TICKETS
PAG -.->|HTTPS| MP["MercadoPago"]
POS -.->|"daemon local"| AC["ApiCard"]
TICKETS -.->|"SOAP"| AFIP["AFIP<br/>(CAE / CAEA)"]
| Componente | Repo | Rol |
|---|---|---|
| Backend | AutoCompraMod |
El sistema único del autoservicio: orquesta tickets, catálogo, pagos, promos y envases. Fuente de verdad de los contratos. |
| Cockpit | AutoCompraMod/cockpit-ui |
Panel de operación (observabilidad): parque de terminales, salud, tickets, errores, pagos y fiscal. Servido por el propio backend. |
| POS | TiprePOS |
La terminal. Corre el flujo de autopago contra el backend. |
Por qué un monolito modular y no microservicios¶
Acá está la decisión arquitectónica más importante. El autoservicio eran cinco microservicios (TsTicket, TsArticulos, TsPagos, TsPromos, TsEnvases). AutoCompraMod los consolida en un solo deployable con Spring Modulith.
¿Por qué? Porque cinco servicios traían el costo de los microservicios (cinco deploys, cinco conexiones que la terminal tenía que orquestar, latencia de red entre servicios, fallos parciales) sin el beneficio: las cargas son chicas y los cinco servicios escalaban juntos de todos modos. Un monolito modular da lo mejor de los dos mundos:
- Un solo proceso, un solo deploy. La terminal habla con un backend, no con cinco.
- Fronteras internas reales, verificadas por el build. Cada módulo (
tickets,catalogo, …) tiene una API pública y todo lo demás es interno. Spring Modulith rompe el build si un módulo accede a los internals de otro o si aparece un ciclo de dependencias.
El detalle de cómo funciona ese guardrail está en El monolito modular. Cómo se llegó hasta acá desde los cinco servicios, en Migración strangler.
El backend manda sobre los contratos
La terminal no inventa endpoints ni DTOs: los consume. Si la terminal tiene una copia de un contrato y no coincide con el backend real, gana el backend: se reporta, no se "arregla" en el cliente.
Topología interna: estrella acíclica¶
Dentro del backend, los módulos no se llaman entre sí libremente. La topología es una estrella acíclica: sólo tickets (el orquestador, la API que ve el POS) llama a los demás; catalogo, pagos, promos y envases son hojas que no llaman a nadie. Cero ciclos.
graph LR
POS["TiprePOS"] --> TICKETS["tickets<br/>(orquestador)"]
TICKETS --> CAT["catalogo"]
TICKETS --> PAG["pagos"]
TICKETS --> PRO["promos"]
TICKETS --> ENV["envases"]
SHARED["shared (kernel OPEN)"]
shared es un kernel abierto (ResponseMessage, enums de pago, NucleoImpositivoDto) que cualquier módulo puede usar: es la lengua común, no un módulo de negocio.
Reglas no negociables
- Comunicación = sólo la API pública del otro módulo. Nunca los internals. Para desacoplar más → eventos de aplicación (
@ApplicationModuleListener). - Datos: schema-por-módulo, CERO foreign keys cruzadas. Si
pagosnecesita un ticket, lo pide por la API detickets, no con un JOIN. - Los bordes externos siguen siendo red.
pagos → MercadoPagoytickets → AFIP(CAE por SOAP) se mantienen async/durable porque son red de verdad: ahí viven la reconciliación de pagos y elVOUCHERPENDINGfiscal. - El guardrail manda.
ModularityTests.verify()rompe el build si se viola un boundary.
Cómo se comunican: la terminal y el backend¶
El backend expone una API REST bajo el context-path /autocompras/v1. La terminal hace mutaciones de ticket (openTicket, agregarArticulo, removeItem, closeTicket, changeStatus, validateTicket, payTicket) por POST, y consulta el estado de los servicios (/status) con un polling adaptativo: cada 60 s cuando el backend está sano, cada 15 s cuando está degradado.
sequenceDiagram
participant POS as Terminal (BLoC)
participant API as Backend (tickets)
participant CAT as catalogo / promos / envases / pagos
POS->>API: POST /openTicket
API-->>POS: TicketDto (estado OPEN)
POS->>API: POST /agregarArticulo (scan EAN)
API->>CAT: resuelve artículo + promos
API-->>POS: TicketDto recalculado
POS->>API: POST /payTicket (intent)
API->>CAT: pagos → MercadoPago
API-->>POS: TicketDto (PAGADO / VOUCHERPENDING)
POS->>API: POST /closeTicket
API-->>POS: TicketDto (CLOSE)
Antes era STOMP, hoy es REST
La terminal se comunicaba por STOMP/WebSocket y migró a REST puro (cutover de full-REST, documentado en Comunicación POS ↔ Backend). En el backend actual no hay @MessageMapping ni broker STOMP; quedan referencias muertas como TerminalSessionService que son sólo histórico. Si ves STOMP mencionado en el README viejo del POS, está desactualizado: la realidad del código es REST.
Autenticación a alto nivel¶
La seguridad es app-wide: un solo SecurityFilterChain y un JwtAuthConverter (paquete security), no uno por módulo. El modelo es deliberadamente liviano: se gatean sólo las mutaciones de administración, mientras que el runtime del POS queda accesible para no romper el checkout.
app.security.enabled(defaultfalse): confalseesanyRequest().permitAll(); contrueaplica la política de roles (resource server JWT / Keycloak).app.security.admin-role(defaultADMIN): rol exigido paraPOST /promociones/updateyPOST /cache/refresh(espera la authorityROLE_<admin-role>).
El riesgo del toggle
Con app.security.enabled=false, TODOS los endpoints quedan permitAll, incluidas las mutaciones admin (precios/promos, refresh de cache). Es sólo para dev / parallel-run, y al armar el filter chain con el toggle en false se loguea un WARN recordándolo. En el ambiente con Keycloak: enabled=true y admin-role al rol real del realm.
Identidad de la terminal¶
Cada terminal se identifica en cada request con tres headers que inyecta su cliente HTTP:
X-Cod-Terminal— el código de la terminal.X-Terminal-Uuid— un UUID por device.X-Device-Health— la última salud conocida de los dispositivos de pago (pinpad / Point), que alimenta la observabilidad por terminal del Cockpit.
El backend valida ese UUID contra la tabla pos en las mutaciones. Así el Cockpit puede mostrar el parque de terminales reconciliando las configuradas con las que efectivamente pingearon.
Deploy¶
El backend corre como un JAR de Spring Boot. El Cockpit (React/Vite) no es un deploy aparte: se construye con el frontend-maven-plugin durante el build de Maven y se copia a src/main/resources/static/cockpit, de modo que el propio backend lo sirve como SPA. Es una topología deliberadamente simple: las cargas del autoservicio son chicas y no justifican infraestructura distribuida.
Para entender las entidades que viajan en estos flujos — ticket/Trx, articulo, vale, envase, pos — pasá por el Glosario.