Saltar a contenido

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

  1. Comunicación = sólo la API pública del otro módulo. Nunca los internals. Para desacoplar más → eventos de aplicación (@ApplicationModuleListener).
  2. Datos: schema-por-módulo, CERO foreign keys cruzadas. Si pagos necesita un ticket, lo pide por la API de tickets, no con un JOIN.
  3. Los bordes externos siguen siendo red. pagos → MercadoPago y tickets → AFIP (CAE por SOAP) se mantienen async/durable porque son red de verdad: ahí viven la reconciliación de pagos y el VOUCHERPENDING fiscal.
  4. 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 (default false): con false es anyRequest().permitAll(); con true aplica la política de roles (resource server JWT / Keycloak).
  • app.security.admin-role (default ADMIN): rol exigido para POST /promociones/update y POST /cache/refresh (espera la authority ROLE_<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.