RF-003 — Pago con Bancard QR¶
| Estado | Migrado al modelo "pedido después del pago" (tarea #263, rama feature/bancard-vpos); pendiente de pruebas en staging y despliegue propio |
| Tipo | Payment provider propio + workflows de intento de pago + rutas API + job programado |
| Ubicación | src/modules/bancard-qr/ · src/workflows/bancard-qr/ · src/lib/payment-attempt/ · src/workflows/payment-attempt/ · src/api/store/bancard-qr/ · src/api/webhooks/bancard-qr/ · src/jobs/bancard-qr-reconcile.ts |
| Depende de | Módulo Payment de Medusa — RF-000 · notificación en vivo — RF-004 · ADR-0006 |
| Ver también | Integración Bancard QR · Modelo de intentos de pago · RF-011 Bancard vPOS |
Requisito¶
Cobrar mediante QR de la red Bancard: emitir un QR por carrito listo para pagar, exhibirlo al cliente para que lo abone desde su aplicación bancaria, crear el pedido recién cuando Bancard confirma el cobro, y revertir el QR ante vencimiento o cancelación liberando el stock reservado, sin dejar pedidos sin pago.
Solución adoptada¶
El QR usa el mismo modelo de intentos de pago que Bancard vPOS (ADR-0006). La sesión de pago se
crea vacía; al llegar al paso de revisión el storefront abre el intento (process): el backend
valida el carrito, reserva el stock, bloquea el carrito, pide el QR a Bancard y lo
devuelve. Cuando Bancard confirma, el webhook ejecuta confirmPaymentAttemptWorkflow
(processPaymentWorkflow → captura → completeCart), que crea el pedido con el pago ya
capturado. Si el QR vence, la conciliación lo revierte y libera el stock.
sequenceDiagram
participant SF as Storefront
participant API as Backend
participant BAN as Bancard
SF->>API: Seleccionar Bancard QR · initiatePayment
API-->>SF: Sesión pendiente, sin QR
SF->>API: POST /store/bancard-qr/process {cart_id}
API->>API: createPaymentAttempt (valida, reserva stock, bloquea carrito)
API->>BAN: generate-qr-express (monto, "Compulandia <shop_process_id>")
BAN-->>API: hook_alias · url · qr_data · supported_clients
API->>API: intento pending · external_reference = hook_alias
API-->>SF: QR y vigencia
Note over SF,BAN: El cliente abona desde su aplicación bancaria
BAN->>API: POST /webhooks/bancard-qr (Basic Auth)
API-->>BAN: {status: success}
API->>API: confirmPaymentAttempt → processPayment → completeCart → pedido
API-->>SF: Evento SSE captured {order_id} (+ sondeo de respaldo)
SF->>SF: cierra el checkout y muestra el pedido
Rutas y disparadores¶
| Superficie | Método | Función |
|---|---|---|
/store/bancard-qr/process |
POST {cart_id} |
Abrir el intento: reserva, bloqueo, QR de Bancard. Reutiliza el QR vigente si ya hay uno |
/store/bancard-qr/sessions/:id/status |
GET ?cart_id |
Estado del intento (respaldo del SSE), con el QR vigente; sin datos internos |
/store/bancard-qr/sessions/:id/cancel |
POST {cart_id} |
"Cancelar pago": reversión en Bancard, stock liberado, carrito desbloqueado |
/store/bancard-qr/simulator/confirm |
POST | Solo con BANCARD_QR_SIMULATOR=true (404 si no): simula el aviso de Bancard |
/webhooks/bancard-qr |
POST | Aviso de Bancard (confirmado, vencido, cancelado, fallido); crea el pedido |
Job bancard-qr-reconcile |
cada 2 min | Vence los QR sin confirmación más viejos que BANCARD_QR_TIMEOUT_MS: reversión y liberación de stock |
Las rutas /store/bancard-qr/generate, /store/bancard-qr/cancel y
/store/payment-status/:session_id, el temporizador en memoria (qr-timeout-manager.ts) y el
job bancard-qr-cleanup del modelo anterior fueron eliminados en #263.
Reglas de negocio¶
- El pedido se crea solo con el pago confirmado por Bancard (webhook o conciliación). El
proveedor responde
pendingal autorizar mientras el intento no estéconfirmed, por lo que completar el carrito desde el navegador no crea pedidos. - Mientras hay un QR vigente el carrito está bloqueado (
payment_attempt_in_progress) y el stock reservado a nombre de sus ítems. Cancelar, vencer o rechazar libera ambos. - El webhook exige Basic Auth con credenciales propias, responde siempre HTTP 200 con el envoltorio de Bancard, verifica que el monto informado sea el del intento, e ignora reenvíos.
- La sesión se localiza por
data.attempt.external_reference = hook_alias(consulta jsonb, no filtrado en memoria). - No se persisten
authorization_code,binnicard_last_numbersdel aviso (src/lib/payment-attempt/confirmation-policy.ts):payment.datallega al cliente por la API store. - Vencimiento por job sobre la base (sin temporizadores en memoria). Si al revertir Bancard informa que devolvió un pago acreditado (confirmación perdida), se registra y se avisa en el resumen: el cliente recibe su dinero y no hay pedido.
- Sesiones creadas antes de la migración (con
hook_aliasdirecto endata) siguen aceptando su confirmación por el camino anterior (captura del pedido ya creado) durante la transición. - El reembolso desde el admin intenta la reversión en Bancard; si no aplica, el intento queda
refund_pendingpara gestión manual.
Estados del intento¶
Los del modelo común (src/lib/payment-attempt/types.ts): created → pending →
confirmed (pedido) / rejected / expired / rolled_back / failed /
paid_without_order / refund_pending.
Configuración¶
| Variable | Uso |
|---|---|
BANCARD_QR_PUBLIC_KEY · BANCARD_QR_PRIVATE_KEY |
Credenciales de la API de Bancard (Basic Auth saliente) |
BANCARD_QR_API_URL |
Base de la API; por defecto el ambiente productivo |
BANCARD_QR_COMMERCE_CODE · BANCARD_QR_BRANCH_CODE |
Comercio y sucursal emisores del QR |
BANCARD_QR_CALLBACK_USER · BANCARD_QR_CALLBACK_PASSWORD |
Credenciales exigidas al webhook entrante |
BANCARD_QR_TIMEOUT_MS |
Vigencia del QR (por defecto 300.000 = 5 min); la conciliación revierte los más viejos |
BANCARD_QR_SIMULATOR |
true solo en desarrollo: no sale nada hacia Bancard; el arranque falla en producción |
Criterios de aceptación¶
| # | Criterio | Prueba |
|---|---|---|
| 1 | Abrir el intento reserva stock, pide el QR con el monto del carrito y bloquea el carrito; una segunda apertura reutiliza el QR | integration-tests/http/bancard-qr.spec.ts "abrir el QR" |
| 2 | Si Bancard falla, el intento queda fallido con la clave, sin reserva, y el carrito sigue modificable | ídem |
| 3 | Webhook sin Basic Auth válido → error fijo de Bancard, sin procesar | ídem "webhook" |
| 4 | Webhook confirmado con el monto exacto → pedido con el pago capturado, sin authorization_code/bin/card_last_numbers guardados; reenvío ignorado |
ídem |
| 5 | Webhook con otro monto → error y sin pedido; failed → intento rechazado, stock liberado, carrito modificable |
ídem |
| 6 | Cancelar revierte por hook_alias, libera el stock y desbloquea; segunda cancelación 400 |
ídem "cancelar y vencer" |
| 7 | Conciliación: QR reciente intacto; vencido → reversión y expired; red caída → pospuesto; pago revertido por Bancard → contado y avisado |
ídem + src/modules/bancard-qr/__tests__/*.unit.spec.ts |
Limitaciones conocidas¶
- La API de Bancard QR no ofrece consulta de estado por
hook_alias: la conciliación solo puede revertir. Un pago cuya confirmación se perdió se devuelve al cliente al vencer el QR. - El aviso en tiempo real (SSE en memoria) no funciona con réplicas del backend; el sondeo del estado es el respaldo obligatorio.
- Pendiente: pruebas contra el ambiente real de Bancard y despliegue separado del vPOS.