Saltar a contenido

ADR-0006 · Pedido después del pago para los medios confirmados por terceros

  • Estado: aceptado
  • Decisores: hquintero, equipo de comercio electrónico
  • Fecha de la decisión: 2026-09-09

Contexto y problema

Bancard QR crea el pedido antes del pago: el proveedor responde "autorizado" al completar el carrito, Medusa crea el pedido y reserva el stock, y recién después el cliente escanea el QR. Si nadie paga, el pedido queda pendiente para siempre, el stock sigue reservado, el cliente ya recibió el correo de "pedido realizado" y operaciones espera un pedido que no va a llegar. Al sumar el pago con tarjeta por Bancard vPOS (historia #149) había que decidir si repetir ese modelo o cambiarlo, sabiendo que a futuro pueden entrar otras pasarelas con la misma característica: la confirmación del pago la da un tercero, de forma asíncrona, por un webhook. Restricciones: Medusa 2.15.5 sin actualizar, un solo equipo, y no romper el QR que está en producción mientras se construye lo nuevo.

Opciones consideradas

  1. Pedido después del pago para los medios confirmados por terceros; la transferencia bancaria sigue con pedido primero (elegida).
  2. Pedido antes del pago para todos los medios (como el QR), con un job que cancele pedidos sin pagar y libere stock. Se eligió el 2026-09-08 y se revirtió al día siguiente: mantiene pedidos fantasma, correos anticipados y número de pedido consumido, y solo mueve el problema a un job de limpieza.
  3. Pedido después del pago para todos los medios, incluida la transferencia. Descartada: la transferencia no tiene confirmación de un tercero; el pedido es justamente lo que le da al cliente los datos para transferir.

Decisión

Para Bancard vPOS, Bancard QR y cualquier pasarela futura con confirmación asíncrona, el pedido se crea solo cuando el pago está capturado. El cliente paga en el paso de revisión del checkout; el webhook de la pasarela, validado por firma y monto, dispara processPaymentWorkflow, que captura el pago y completa el carrito. El storefront nunca llama a placeOrder para estos medios; el proveedor responde "pendiente" al autorizar hasta que el intento esté confirmado, de modo que completar el carrito desde el navegador falla.

La decisión trae tres piezas que forman parte de ella:

  • Reserva temporal de stock. Al abrir el pago se verifica el stock y se crean reservas sueltas (módulo de inventario) por cada ítem con inventario gestionado. Se liberan al confirmar (justo antes de completar el carrito, que vuelve a reservar a nombre del pedido), al rechazar, al expirar o al cancelar. Si al crear el pedido igual falta stock por ventas fuera de Medusa, el pedido se crea con allow_backorder forzado solo durante esa completación y se alerta a operaciones ("stock comprometido"). Se descartó revertir el cobro: la reversión automática de Bancard solo sirve el mismo día.
  • Bloqueo del carrito mientras hay un pago en curso. Todo cambio de carrito que altere el total borra las sesiones de pago en Medusa, y el webhook no tendría sobre qué capturar. Se rechazan las modificaciones y el cambio de medio de pago con el punto de validación de refreshPaymentCollectionForCartWorkflow y el middleware de creación de sesiones. El cliente puede cancelar el pago; el intento expira a los 10 minutos. Red de seguridad en el webhook: si la sesión no existe o el total o la huella del carrito no coinciden, no se crea el pedido y el intento queda marcado "pagado sin pedido" con alerta.
  • Registro de intentos común a las pasarelas (payment_attempt): un registro por intento con carrito, monto exacto enviado, huella del carrito, estado, respuesta de la pasarela, reservas y pedido. Es la traza de los pagos que no se concretan, hoy invisibles, y la base de la vista en el admin (historia #264).

El QR migra a este modelo en una tarea propia (#263), con despliegue separado. La migración se desarrolló el 2026-09-11 en la misma rama (feature/bancard-vpos); su salida a producción se programa aparte de la del vPOS.

Consecuencias

Positivas

  • No hay pedidos sin pago: lo que operaciones ve en el admin está cobrado, salvo la transferencia, que ya se trata a mano.
  • El stock no queda retenido por carritos abandonados; la reserva vive lo que vive el intento.
  • El correo de "pedido realizado" llega con el pago hecho; no hace falta una plantilla de "pendiente de pago".
  • Es el flujo nativo de Medusa para proveedores de tarjeta (el mismo que Stripe), lo que reduce código propio en el camino crítico.
  • Los intentos fallidos quedan registrados y consultables, incluido el contacto del cliente.

Negativas / deuda asumida

  • Pago cobrado sin pedido es un estado posible (stock, carrito cambiado, error interno). Se mitiga con la reserva, el bloqueo y el último recurso de stock comprometido, pero exige alerta, procedimiento en el runbook y, en el peor caso, reversión manual con Bancard.
  • El carrito bloqueado es una experiencia nueva para el cliente; hay que explicarla bien y dar siempre el botón de cancelar.
  • Hasta que la migración del QR (#263) se despliegue conviven dos modelos en producción; el webhook nuevo acepta también las sesiones del modelo anterior durante la transición.
  • El registro de intentos es una tabla más que mantener, con su migración y sus enlaces.
  • El aviso en tiempo real (SSE en memoria) no funciona con réplicas del backend; el sondeo del estado del intento es obligatorio como respaldo.