Traducción de
docs/en/architecture/overview.md, que es el original. Si discrepan, manda el inglés.
#Resumen
Payment Emulator Lab es un sistema modular que delega en un runtime, con tres aplicaciones:
apps/api: un solo código NestJS con un proceso HTTP y un worker aparte.apps/console: una aplicación Nuxt para el operador, con su propio BFF Nitro.apps/checkout: una aplicación Nuxt para el comprador, con otro BFF Nitro y kits visuales por proveedor.
El backend se despliega como monolito modular. No es DDD y no usa abstracciones de repositorio sobre MikroORM. La arquitectura busca que cada proveedor sea pequeño: un plugin describe y traduce su contrato, mientras el runtime compartido hace una sola vez la idempotencia, el control de concurrencia, los cambios de estado canónicos, la contabilidad, las trazas y la programación de webhooks.
Las reglas vinculantes son las de .agents/README.md
y .agents/architectures/modular-arch/.
#Vista del sistema
Código del diagrama
flowchart TB
subgraph Navegadores
OP[Operador]
BUY[Comprador]
end
AUT[Aplicación en pruebas]
subgraph Interfaces
CON[Consola: Nuxt + PrimeVue]
CHK[Checkout: Nuxt + kits de proveedor]
CN[BFF Nitro de la consola]
KN[BFF Nitro del checkout]
CON --> CN
CHK --> KN
end
subgraph Backend
HTTP[Proceso HTTP NestJS]
GW[GatewayService]
PL[Plugins de proveedor]
PAY[PaymentsService]
LED[Servicios del libro mayor]
OUT[Outbox webhook_events]
WORK[Proceso worker]
end
OP --> CON
BUY --> CHK
AUT -->|petición con forma de proveedor| HTTP
CN -->|sesión de persona| HTTP
KN -->|enlace público + sesión de cliente opcional| HTTP
HTTP --> GW
GW --> PL
GW --> PAY
PAY --> LED
PAY --> OUT
HTTP --> PG[(PostgreSQL)]
WORK --> PG
WORK --> TR[(Transporte: PostgreSQL, MongoDB o Redis)]
TR --> WH[Endpoint de webhook permitido]
PostgreSQL es la fuente durable de la verdad. Un transporte lleva los trabajos de
webhook —el propio PostgreSQL, MongoDB o Redis, según WEBHOOK_QUEUE_DRIVER—
pero la fila del outbox en PostgreSQL es el compromiso de entrega; el worker
puede recrear desde ella un trabajo perdido. Ver
ADR 011.
#Fronteras entre aplicaciones
Las tres aplicaciones no comparten código. Un dato compartido —el nombre visible de un proveedor, sus métodos de checkout, su vocabulario de estados— lo sirve la API y se lee en ejecución. Eso protege dos fronteras que importan:
- Las credenciales del backend o del operador nunca entran en un bundle de navegador.
- El checkout del comprador no hereda las dependencias, la sesión ni la postura de seguridad de la consola.
Los dos navegadores llaman a sus propias rutas de Nitro.
apps/console/server/utils/api.ts reenvía la sesión de API de quien entró;
apps/checkout/server/utils/api.ts reenvía una sesión de cliente sólo si existe.
El checkout sigue siendo usable de forma anónima para métodos externos como
tarjeta, ticket o transferencia.
#Fronteras del backend
#Los controladores declaran, los servicios hacen
Los controladores definen rutas, DTO, roles y la llamada al servicio. Las
transacciones, las escrituras de varios pasos y las decisiones de negocio van en
los servicios. Por ejemplo,
apps/api/src/modules/checkout/checkout.controller.ts sólo describe la
superficie del comprador; checkout.service.ts elige el método de pago, resuelve
el cliente opcional y delega el cambio de estado en PaymentsService.
#Los plugins traducen; nunca escriben
apps/api/src/core/providers/provider-plugin.ts define la costura del proveedor:
routes · resource · idempotency · credentials · statuses · errors
webhooks · ledger · checkout · validate · toCanonical · toWire
Un plugin puede validar la entrada del proveedor, traducirla a una orden canónica
y devolver el estado canónico como respuesta del proveedor. No puede usar
EntityManager, tomar locks, escribir asientos ni encolar trabajos. El registro
de apps/api/src/core/providers/plugin-registry.ts es la única lista central de
implementaciones.
#El estado canónico se queda neutral
payment_intents guarda el hecho económico: importe en unidades mínimas, moneda,
método de captura, estado canónico y origen del fondeo. provider_resources
guarda el identificador del proveedor, la petición original y la respuesta con su
forma. Campos como el status_detail de Mercado Pago o el client_secret de
Stripe nunca se convierten en columnas del intento canónico.
#Un solo camino de escritura
apps/api/src/modules/payments/payments.service.ts es el único camino de
escritura de un pago. Las peticiones de proveedor, la confirmación del checkout y
las transiciones por reloj convergen ahí. Él es dueño del orden entre locks,
transiciones de estado, efectos contables y programación transaccional del
webhook.
#La persistencia es un runtime, no una capa de repositorio
Los servicios usan el EntityManager de MikroORM directamente. Las transacciones
pasan el mismo gestor transaccional por las escrituras de pago, libro mayor y
outbox. El SQL crudo se usa para agregar lecturas y para los locks de PostgreSQL,
nunca para escribir apuntes.
#El camino de una petición de proveedor
Toda petición de proveedor sigue este orden en
apps/api/src/modules/gateway/gateway.service.ts:
- Acepta la ruta del proveedor, opcionalmente con el prefijo
/mercadopagoo/stripeor/paypal. - Resuelve la aplicación y el proveedor desde la credencial; si hay prefijo, tiene que coincidir.
- Mapea método y ruta a una operación canónica con
plugin.routes. - Resuelve el escenario de la aplicación o el
X-Emulator-Scenarioque lo pise. - Corre
plugin.validateantes de escribir un pago. - Aplica la latencia, el tiempo de espera o el fallo HTTP que declare el escenario.
- Traduce con
plugin.toCanonicaly llama aPaymentsService.execute. - Compone la respuesta con
plugin.toWire. - Confirma la fila de webhook que se deba en la misma transacción que el pago.
Los diagramas de secuencia están en Flujos de petición críticos.
#Responsabilidades del worker
apps/api/src/worker.ts es un segundo proceso sobre el mismo código y la misma
base. Corre tres bucles independientes:
| Bucle | Intervalo por defecto | Trabajo |
|---|---|---|
| Despacho del outbox | 1 segundo | Reclama los webhook_events que tocan y se los da al transporte. No arranca con el driver postgres, que lee esas filas él mismo |
| Transiciones por reloj | 1 segundo | Mueve los pagos en requires_action que tocan a captured o failed |
| Liquidación | 60 segundos | Libera la reserva capturada hacia el pagadero del comercio pasado T+N |
El manejador de entrega firma con el plugin del proveedor, manda sólo a un host permitido y anota cada intento. Los locks consultivos y la relectura del estado hacen seguras las transiciones por reloj y la liquidación cuando hay varios workers.
#Autorización en el plano de control
Usa RolesGuard más declaraciones @Roles(...) explícitas. Hay dos tipos de
credencial:
ADMIN_TOKENes la credencial raíz de arranque. Actúa con autoridad de administrador pero no es una persona.POST /auth/loginemite un token de sesión revocable para unadmin,merchantocustomer.
ApplicationScope y ApplicationScopeGuard centralizan la visibilidad por fila.
Una aplicación fuera del alcance de quien llama se responde con 404, para no
revelar que existe. La propiedad se toma de la sesión; un comercio no puede
asignarle una aplicación a otro dueño desde el cuerpo de la petición.
Más detalle en Identidad, roles y alcance.
#Reglas de las interfaces
#Consola
Las páginas declaran lo que necesitan a través del gateway y los composables de
colección de core. Nitro es el único cliente de la API. PrimeVue se usa sólo
aquí. Los listados usan el contrato compartido { data, meta } con listas
blancas por endpoint para ordenar, filtrar y buscar. Ver
la consola.
#Checkout
El armazón compartido es neutral. La apariencia y el flujo de un proveedor viven
en apps/checkout/providers/<id>/; las páginas con ruta eligen el kit con
apps/checkout/app/core/kits.ts. No añadas un if por proveedor a un
componente compartido, y no importes código de la consola ni del backend. Ver
el checkout.
#Reglas de dependencia
Código del diagrama
flowchart LR
Controlador --> Servicio
Servicio --> EntityManager[EntityManager de MikroORM]
Pasarela --> Plugin
Pasarela --> Pagos
Pagos --> Idempotencia
Pagos --> LibroMayor
Pagos --> Webhooks
Plugin --> Tipos[tipos de core y ayudantes de dinero]
Pagina --> GatewayFront[gateway de core]
GatewayFront --> Nitro
Nitro --> API
Direcciones de dependencia prohibidas:
- Plugins → base de datos, servicios de pago, libro mayor o cola.
- Core o módulos → una condición sobre el identificador de un proveedor.
- Código de navegador → la API de NestJS directamente.
- Consola ↔ checkout importándose, o cualquiera de las dos → el backend.
- Saldo del libro mayor → una columna mutable guardada.
#Decisiones y aplazamientos conocidos
Las decisiones principales están en docs/en/architecture/decisions/: monolito
modular, MikroORM, JSON canónico, PostgreSQL/JSONB, la cola como runtime no
durable, plugins de proveedor, escenarios deterministas, ingestión explícita de
contratos, contabilidad por partida doble, sin proxy a proveedores en ejecución, y
un transporte de webhooks intercambiable —PostgreSQL, MongoDB o Redis tras un
mismo puerto.
Queda un aplazamiento de disposición: las entidades están centralizadas en
apps/api/src/infrastructure/database/entities/ y los módulos son casi todos
planos, en vez de la forma por módulo del enlace de NestJS. Está documentado y no
debe «arreglarse» de paso dentro de un cambio de funcionalidad.
El intercambio OAuth y el certificado público local de PayPal son rutas auxiliares de la pasarela, fuera del recorrido de transacción de pago anterior.