Playbook
Diseño de la máquina de estado de pago: ¿Por qué el pago y el pago no son lo mismo? (Diseno DE La Maquina DE Estado DE Pago Por Que El Pago Y El Pago No Son Lo Mismo)
El pago realizado no significa que el pedido se haya completado. Si no se separan los ciclos de vida de pago y pago, las dos realidades se superponen en la producción.
Motor de pago distribuido
Parte 2 de 22
Una serie de arquitecturas de pago distribuidas que cierran la brecha entre la captura y la finalización.
Dos calendarios, una pantalla
El cliente ve un único "estado del pedido" en la pantalla; Pero al menos dos máquinas de estado independientes se ejecutan en segundo plano: el ciclo de vida del pago y el ciclo de vida del pago. Intentar gestionar estas dos máquinas a través de un único campo (status) es la versión de máquina de estados de la ilusión de "verdad única" de la que hablamos en la primera sección.```text
Checkout: Init → Processing → FinalizePending → Completed
↘ Failed / Expired
Payment: Init → Processing → Captured → Completed ↘ Failed / Expired
## Conceptos en la primera mención```text
📦 Checkout Lifecycle
Siparişin müşteri gözünden geçtiği aşamalar: başlatıldı, işleniyor, tamamlanma bekliyor, tamamlandı.
📦 Payment Lifecycle
Para hareketinin PSP gözünden geçtiği aşamalar: başlatıldı, işleniyor, çekildi (captured), tamamlandı.
📦 Terminal Durum
Geriye dönüşü olmayan, makinenin o dal için sonlandığı durum (Completed, Failed, Expired).
📦 Transition Guard
Bir durumdan diğerine geçişe izin vermeden önce kontrol edilen ön koşul.
📦 State Drift
İki ilişkili durum makinesinin, senkronize olması gereken noktada birbirinden kopması.
````Captured` es PSP que dice "Tengo el dinero". `Completed` es tu dicho "Terminé el pedido". Estos dos no son el mismo evento; Hay un estado de espera entre ellos llamado `FinalizePending` y este estado puede durar no segundos sino a veces minutos.
## Por qué un solo campo `status` no es suficiente
En muchos sistemas, hay una única columna `status` en la tabla de pedidos, y tanto el "estado de pago" como el "estado del pedido" se incluyen en esta columna. Este es el síntoma clásico de agrupar dos responsabilidades diferentes en un solo campo: una línea con estado `Completed` cuando llega el webhook de pago muestra el pedido como “terminado” cuando en realidad el stock no ha sido reservado ni facturado.```text
Orders
id | status
1 | Completed ← webhook geldi, ama finalization saga'sı henüz çalışmadı
```## Separando dos máquinas
El modelo correcto define el pago y el pago como máquinas de estado separadas y establece solo una relación de activación unidireccional entre ellos: la transición del pago al estado `Captured` desencadena la transición del pago al estado `FinalizePending`; pero que el pago sea `Completed` depende del final de su propia saga.```text
Payment.Captured --(tetikler)--> Checkout.FinalizePending
|
stok, finans, bildirim, sepet temizliği tamamlanınca
↓
Checkout.Completed
```Gracias a esta distinción, “el pago se realizó correctamente pero el pedido aún se está procesando” ya no es un error, sino un estado intermedio esperado y demostrable. Decirle al cliente "Su pago ha sido recibido, su pedido se está preparando" refleja con precisión el estado real del sistema.
## ¿Quién activa el error y el vencimiento?
Ambas máquinas tienen sus propias ramas `Failed` y `Expired`, que se pueden activar de forma independiente. En el lado del pago, `Expired` significa que el PSP no responde dentro de un período de tiempo determinado (por ejemplo, la confirmación 3D Secure no se completa). En el lado del pago, `Expired` significa que la saga de finalización no finaliza dentro de un tiempo específico, incluso si el pago es `Captured`.```text
Payment.Captured + Checkout finalization 30 dakika içinde bitmedi
↓
Checkout.Expired (ama Payment.Captured hâlâ geçerli — para geri iade edilmeli mi, saga retry mi edilmeli, karar operasyonel bir konudur)
```Este escenario muestra el beneficio real de separar las dos máquinas: puede iniciar un proceso de recuperación separado en el lado del pago, con el estado `Payment.Captured` intacto. Si hubiera un solo campo `status`, no podrías representar estos dos hechos al mismo tiempo.
## Guardias: pases de guardia
Cada pase debe tener un guardia. Por ejemplo, la transición a `Checkout.Processing → Checkout.FinalizePending` debe ocurrir solo después de verificar que el registro de pago asociado esté en el estado `Captured`. Una máquina de estados sin Guard puede caer en estados no válidos si los webhooks llegan fuera de servicio (lo que detallaremos en la sexta parte de esta serie).```text
Guard: Checkout.FinalizePending'e geçiş
→ İlişkili Payment kaydı var mı?
→ Payment.Status == Captured mı?
→ Payment.Amount, Checkout snapshot'ıyla eşleşiyor mu?
Hepsi doğruysa geçiş serbest; değilse geçiş reddedilir ve olay bir “beklemede” kuyruğuna düşer.
Los enfrentamientos más confusos de este episodio.```text
❌ Payment.Succeeded = Checkout.Completed ✓ Payment.Succeeded, Checkout.FinalizePending'i tetikler; Completed ayrı bir karardır
❌ Tek bir status alanı hem ödeme hem sipariş durumunu taşıyabilir ✓ İki bağımsız yaşam döngüsü, iki bağımsız alan (veya tablo) gerektirir
❌ Failed durumu her zaman “para geri gitti” anlamına gelir ✓ Checkout.Failed, Payment.Captured'ı geçersiz kılmaz; ayrı bir telafi süreci gerekir
❌ Guard'sız bir geçiş, sadece “fazladan kontrol”dür ✓ Guard, sırasız veya tekrarlı olaylara karşı tek savunma hattıdır
## Haga una lista de verificación de su máquina de estados
1. ¿El estado del pago y el estado del pedido se mantienen en la misma columna en la tabla de pedidos? Separado.
2. ¿Qué guardias están controladas por el código que desencadena la transición entre `Captured` y `Completed`?
3. ¿Qué sucede si la saga de pago no finaliza dentro de los 30 minutos posteriores al pago `Captured`? ¿Hay alguna alarma?
4. ¿Puede enumerar claramente quién ingresó a los estados `Failed` y `Expired` y con qué evento?
5. Si dos webhooks llegan desordenados por el mismo pago, ¿el guardia lo detecta o permite una transición no válida?
Si no tiene respuestas seguras a estas cinco preguntas, probablemente haya combinado dos máquinas de estados en una.
## Cosas para recordar de esta sección
1. El pago y el pago son dos máquinas de estado relacionadas pero independientes; no se puede comprimir en un solo campo `status`.
2. Pago.Succeeded no garantiza Checkout.Completed; Existe un rango medible y demostrable de `FinalizePending` entre ellos.
3. Cada pase debe estar conectado a una guarda; Los pases sin guardias conducen a situaciones inválidas en eventos desordenados o repetitivos.
4. `Failed` y `Expired` se pueden activar de forma independiente en dos máquinas; Uno no anula automáticamente al otro.
> El día que escribes en una misma columna el estado del pago y el estado del pedido, conviertes dos verdades diferentes en una sola mentira.
FAQ
Frequently asked questions
¿Qué es el ciclo de vida del pago?
Las etapas por las que pasa el pedido por los ojos del cliente son: iniciado, en proceso, en espera de finalización, completado.
¿Qué es el ciclo de vida de pago?
Las etapas por las que pasa el movimiento de dinero desde la perspectiva del PSP: iniciado, procesado, capturado, completado.
¿Es correcto "Pago.Succeeded = Checkout.Completed"?
Payment.Succeeded activa Checkout.FinalizePending; Completado es una decisión separada.
¿Qué soluciona esta sección?
Esta sección explica por qué debería diseñar estas dos máquinas por separado y considerar el retraso entre ellas como una decisión de diseño, no como un error. La compra y el pago son dos máquinas de estados relacionadas pero independientes; no se puede comprimir en un solo campo `status`. El cliente ve un único "estado del pedido" en la pantalla; Pero al menos dos máquinas de estado independientes se ejecutan en segundo plano: el ciclo de vida del pago y el ciclo de vida del pago. Intentar gestionar estas dos máquinas a través de un único campo (`status`) es la versión de máquina de estados de la ilusión de "verdad única" de la que hablamos en la primera sección.
Principios de ingeniería aprendidos
- Los ciclos de vida de pago y pago están relacionados pero son independientes; no pueden estar representados en la misma zona.
- Payment.Succeeded es un desencadenante, no un resultado; Checkout.Completed depende de la finalización de su propia saga.
- Un pase sin guardia deja la puerta abierta a un estado inválido cuando se encuentra con un evento repetitivo o fuera de orden.
Continuar leyendo
Continuar leyendo
Siguiente en la serie
¿Por qué la recopilación (captura) es fácil pero la finalización es difícil?
Es sólo un paso para que PSP consiga el dinero. Terminar el pedido; Es una saga que requiere pasos de inventario, finanzas, informes y limpieza para que…
Siguiente en la serie
¿Por qué los sistemas de pago son sistemas distribuidos?
Un pago no es tarea de un solo servicio: cesta, stock, pasarela proveedor y finanzas deben ponerse de acuerdo en el mismo hecho.…
Misma serie
Diseño de instantáneas de pago inmutable: la decisión que congela el carrito
Leer el carrito en vivo cuando comienza el pago deja indeciso el monto y la moneda. La finalización no funcionará de manera confiable sin una instantánea…