Cerrada

Entry 003 - Flujo asincronico de pagos, procesamiento externo y resolucion definitiva

dddpaymentsaleseventsstate-machineasynctesting

Reconocer el procesamiento externo del pago

El iter2 asumia que agregar un pago era una operacion inmediata y determinista: addPayment() marcaba la order como COMPLETED apenas los pagos cubrian el total. Pero el procesamiento de un pago (tarjeta, transferencia, etc.) es externo al sistema — el resultado llega despues, desde un procesador que no controlamos.

Este iter rediseña el flujo para reflejar esa realidad. El sistema nunca declara el exito o fallo de un pago; solo reacciona al resultado que le comunica el procesador externo.

Payment como entidad asincronica

Payment ahora arranca en PENDING cuando se registra. Se confirma via complete() o fail() solo cuando llega el resultado externo:

El ID del pago se recibe como parametro en la creacion (en vez de generarse internamente), permitiendo correlacionar con el procesador externo.

Nueva semantica de PaymentOrderStatus

Con pagos asincronicos, el estado de la order depende de dos cosas: cobertura (suma de pagos no-fallidos) y confirmacion (pagos en COMPLETED):

CANCELLED fue eliminado del enum — el ciclo se simplifica y la cancelacion es ahora una consecuencia del fallo definitivo de los pagos, manejada a nivel de Sale.

recalculateStatus como fuente unica de verdad

Toda transicion de estado pasa por un unico metodo privado recalculateStatus() que implementa las reglas de arriba. Se invoca despues de cualquier operacion que modifique los pagos (addPayment, registerPayment), garantizando consistencia.

PaymentCommit: el punto de entrada del resultado externo

Se introdujo el use case PaymentCommit que recibe (paymentId, success: boolean). Su responsabilidad:

  1. Localiza la PaymentOrder que contiene ese pago (via findByPaymentId)
  2. Delega a PaymentOrder.registerPayment(paymentId, success)
  3. El agregado recalcula su estado
  4. Si la order transiciona a COMPLETED, publica PaymentOrderCompleted

Este es el unico punto donde se materializa el resultado externo en el dominio.

Pago fallido hace retroceder la order

Cuando un pago falla (registerPayment(id, false)), el pago se marca como FAILED y deja de contar para la cobertura. Si la suma de no-fallidos cae por debajo del total, la order vuelve a PENDING — el usuario puede agregar otro pago para cubrir la diferencia.

Esto evita que la order quede en un estado inconsistente donde “estaba cubierta” pero ahora no. La cobertura siempre refleja la realidad de los pagos no-fallidos.

Fallo definitivo: PaymentOrderFailed y FailSale

Cuando los reintentos se agotan (politica pendiente de implementar), el use case emite el evento PaymentOrderFailed(saleId). Sales reacciona via SaleFailedOnPayment handler, que invoca FailSale:

Product.restoreStock(quantity) es una nueva operacion que incrementa stock sin tocar reservedStock, porque el stock ya fue confirmado (committed) al registrar la venta.

Comunicacion bidireccional entre contextos

Los tres eventos son DTOs simples. Ningun contexto importa clases del otro. Toda la comunicacion pasa por el InMemoryEventBus.

Flujo completo de una venta

  1. Sale DRAFT — agregando items, stock reservado
  2. confirmSale()READY_TO_PAY — stock committed
  3. SalesReadyToPay → Payment crea PaymentOrder en PENDING
  4. addPayment(s) → pagos PENDING, order PENDING o PARTIAL
  5. paymentCommit(id, true/false) → pagos COMPLETED/FAILED
  6. Cuando todos COMPLETED y cubierto → PaymentOrderCompleted → Sale COMPLETED
  7. Si los pagos fallan definitivamente → PaymentOrderFailed → Sale CANCELLED + stock restaurado

Maquinas de estado resultantes

Sale: DRAFT → READY_TO_PAY → COMPLETED, DRAFT → CANCELLED, READY_TO_PAY → CANCELLED (via PaymentOrderFailed)

PaymentOrder: PENDING ↔ PARTIAL → COMPLETED — la transicion PARTIAL → PENDING ocurre cuando un pago falla y reduce la cobertura.

Payment: PENDING → COMPLETED, PENDING → FAILED — terminal en ambos casos.

Tests de la iteracion

El suite iter3-payment-lifecycle cubre:

Test Results

Payment Lifecycle (Iter 3)

Transiciones PENDING → PARTIAL → COMPLETED con resultados externos

11 passed
New PaymentOrder starts as PENDING
Payment below total keeps order as PENDING
Second payment still below total keeps order as PENDING
Covering payment transitions order to PARTIAL (awaiting external confirmation)
Payment Order transitions to COMPLETED once all payments are externally confirmed
Adding payment to COMPLETED order returns domain error
Single payment covering full amount transitions to PARTIAL
Cash overpayment transitions to PARTIAL with correct change
Failed payment reverts order to PENDING (coverage dropped below total)
Sale transitions to COMPLETED via cross-context event after all payments confirmed
Sale stays READY_TO_PAY while PaymentOrder is PARTIAL (awaiting confirmation)

DDD Artifacts

Sales

Sale

Aggregate Root Sales

Properties

id string
items SaleItem[]
total PriceVO
status SaleStatus
createdAt Date

Methods

create()addItem()confirmSale()completeSale()cancelSale()recalculateTotal()

Invariants

Estado inicial siempre es DRAFT Solo se pueden agregar items en estado DRAFT confirmSale() solo desde DRAFT → READY_TO_PAY completeSale() solo desde READY_TO_PAY → COMPLETED cancelSale() solo desde DRAFT → CANCELLED

SaleStatus

Value Object
DRAFT | READY_TO_PAY | CANCELLED | COMPLETED

Payment

PaymentOrder

Aggregate Root Payment

Properties

id UuidVO
saleId UuidVO
totalAmount PriceVO
payments Payment[]
status PaymentOrderStatus
change PriceVO

Methods

create()addPayment()getStatus()getChange()

Invariants

Estado inicial PENDING, sin pagos Pago parcial transiciona a PARTIAL Pagos que cubren el total transicionan a COMPLETED No se aceptan pagos en estado COMPLETED

PaymentOrderStatus

Value Object
PENDING | PARTIAL | COMPLETED | CANCELLED

Shared Domain

EventBus

Value Object Shared
publish(event) subscribe(eventName, handler)

InMemoryEventBus

Value Object Shared
Singleton Map<string, EventHandler[]>

Domain Events

Sales SalesReadyToPay Payment Confirmar venta crea PaymentOrder automaticamente
Payment PaymentOrderCompleted Sales Pago completo transiciona Sale a COMPLETED

State Machines

Sale

DRAFTREADY_TO_PAYCOMPLETEDCANCELLED
DRAFT READY_TO_PAY confirmSale()
READY_TO_PAY COMPLETED PaymentOrderCompleted event
DRAFT CANCELLED cancelSale()

PaymentOrder

PENDINGPARTIALCOMPLETEDCANCELLED
PENDING PARTIAL addPayment() parcial
PARTIAL PARTIAL addPayment() parcial
PENDING COMPLETED addPayment() cubre total
PARTIAL COMPLETED addPayment() cubre total