En progreso

Entry 005 - Payment Gateway, SSE reactivo, separación de agregados

dddpaymentgatewayssehexagonalevent-drivenapiaggregates

Payment Gateway Mock

Se implementó un procesador de pago externo (externals/payment-gateway/) usando node:http puro. Simula latencia asíncrona (5–20s) y tasa de fallo configurable (20%). Al resolver, envía webhook callback al backend.

Endpoints del gateway:

Contrato del dominio (PaymentGateway port):

Arquitectura SSE

Se diseñó un sistema de push server-to-client basado en Server-Sent Events:

Infraestructura (SSEStreamAdapter):

EventBus tipado:

Endpoint /api/payment/events:

Migración a Server-Side Execution

Todo el dominio se ejecuta exclusivamente en el servidor. Los servicios del cliente (CatalogService, SaleService, PaymentService) usan fetch — cero imports de package/core.

Endpoints creados:

Separación de agregados: Payment ↔ PaymentOrder

Se separaron Payment y PaymentOrder como agregados independientes con sus propios repositorios.

PaymentOrder (agregado fuerte — decide):

Payment (agregado autónomo — actúa):

Principio de diseño:

Screaming Architecture en Frontend

Se reorganizó src/ siguiendo screaming architecture:

Decisiones arquitectónicas clave

  1. SSE como trigger, HTTP como source of truth: el evento dice “algo cambió”, el fetch a /api/payment/status dice “qué cambió”
  2. Un EventListener por servicio: cada contexto (payment, sales) tiene su propio listener, no comparten canal
  3. El gateway no bloquea: ProcessPayment es fire-and-forget. La resolución llega vía webhook o reconciliación
  4. Errores de infra vs dominio: gateway unreachable → excepción (infra). Payment rechazado → Result.fail (dominio)
  5. PaymentOrder es autosuficiente: nunca necesita cargar Payment[] para decidir. Estado acumulado, no computado.

Test Results

Aggregate Separation (Iter 5)

Payment y PaymentOrder como agregados independientes: processing(), guards de estado, estado incremental, PaymentRepository

9 passed 10 failed
processing() sets externalId on CARD payment
processing() sets externalId on TRANSFER payment
processing() fails for CASH payment
processing() fails when payment is COMPLETED (not PENDING)
processing() fails when payment is FAILED (not PENDING)
complete() fails for CARD without externalId
complete() fails for TRANSFER without externalId
complete() succeeds for CASH without externalId
complete() succeeds for CARD with externalId set (after processing)
complete() succeeds for TRANSFER with externalId set (after processing)
fail() fails when payment is COMPLETED (not PENDING)
fail() is idempotent when payment is already FAILED
unknown

po.registerPendingPayment is not a function

unknown

po.applyPayment is not a function

unknown

po.registerFailedAttempt is not a function

Cash overpayment transitions to PARTIAL with correct change ($50)
PaymentRepository.findByExternalId finds payment by transaction ID
PaymentRepository.findByPaymentOrderId returns all payments for an order
PaymentRepository update fails with stale version (concurrency check)

Gateway Integration (Iter 5)

ProcessPayment con gateway mock, ReconcilePayment con retry/backoff, PaymentWebhookHandler idempotente

3 passed 5 failed
TypeError: transactionIdResult.getError is not a function
ProcessPayment fails when payment not found
unknown

queryResult.getError is not a function

unknown

Cannot read properties of undefined (reading 'isSuccess')

TypeError: transactionIdResult.getError is not a function
TypeError: transactionIdResult.getError is not a function
Webhook confirms payment via transactionId
Webhook with duplicate transactionId is idempotent (no error thrown)

SSE Event Routing (Iter 5)

subscribeWithFilter: multi-evento, predicados where, enrutamiento tipado

3 passed
subscribeWithFilter receives matching events
subscribeWithFilter respects where predicate (only success=true events)
subscribeWithFilter handles multi-event subscription