Saltar al contenido
Payment Emulator LabDocumentaciónEntrar a la consola

Arquitectura

Arquitectura de Payment Emulator Lab

Payment Emulator Lab es un sistema modular que delega en un runtime, con tresaplicaciones:

8 min de lectura

Índice / Index · English

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
mermaid
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:

text
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:

  1. Acepta la ruta del proveedor, opcionalmente con el prefijo /mercadopago o /stripe or /paypal.
  2. Resuelve la aplicación y el proveedor desde la credencial; si hay prefijo, tiene que coincidir.
  3. Mapea método y ruta a una operación canónica con plugin.routes.
  4. Resuelve el escenario de la aplicación o el X-Emulator-Scenario que lo pise.
  5. Corre plugin.validate antes de escribir un pago.
  6. Aplica la latencia, el tiempo de espera o el fallo HTTP que declare el escenario.
  7. Traduce con plugin.toCanonical y llama a PaymentsService.execute.
  8. Compone la respuesta con plugin.toWire.
  9. 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_TOKEN es la credencial raíz de arranque. Actúa con autoridad de administrador pero no es una persona.
  • POST /auth/login emite un token de sesión revocable para un admin, merchant o customer.

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
mermaid
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.

Payment Emulator Lab · RonuSoftwareMIT