Saltar a contenido

RF-010 — Intentos de pago en el admin

Estado Implementado en la rama feature/bancard-vpos (historia #264); pendiente de revisión y despliegue
Tipo Rutas de admin + página y widgets del admin + job diario
Ubicación src/lib/payment-attempt-admin/ · src/api/admin/payment-attempts/ · src/admin/routes/intentos-de-pago/ · src/admin/widgets/order-payment-attempts.tsx · src/admin/widgets/order-payment-details.tsx · src/jobs/payment-attempts-daily-summary.ts
Depende de Modelo de intentos de pago — RF-003, RF-011, ADR-0006 · correo — RF-007

Requisito

Con el modelo "pedido después del pago", los pagos que no se confirman no generan pedido y el admin de Medusa no los muestra. Operaciones y ventas necesitan ver todos los intentos de pago (en especial los que no terminaron en pedido), seguir a esos clientes, resolver reclamos de cobros y detectar problemas con las pasarelas; y recibir un aviso diario con los casos que requieren acción.

Solución adoptada

Una vista propia del admin sobre las tres fuentes del modelo nativo (análisis §5): el intento en curso en payment_session.data.attempt, los cerrados de la misma sesión en data.attempts_history y el archivo en cart.metadata.payment_attempts (sobrevive al borrado de la sesión). Una consulta única las une, elimina duplicados por (sesión, número) y agrega cliente, carrito y pedido. Las acciones sobre la pasarela las aporta cada proveedor por un registro común (gateways.ts), sin exponer claves ni tokens.

Superficies

Superficie Función
GET /admin/payment-attempts Listado con filtros status (lista), provider_id, q (correo, nombre, referencia, pedido, carrito, sesión), from/to, cart_id, order_id (incluye los intentos anteriores del mismo carrito), paginación
GET /admin/payment-attempts/:id Detalle: intento, sesión y pagos, carrito con ítems, pedido, historial de la sesión, aviso de la pasarela (sin datos de tarjeta) y acciones permitidas. id = <sesión>.<número>
POST …/:id/consult Consulta en la pasarela (vPOS: single_buy/confirmations; QR: no disponible)
POST …/:id/rollback Revierte un intento en curso y lo cierra (stock liberado, carrito editable)
POST …/:id/complete-order "Crear el pedido igual" para un pagado sin pedido (stock comprometido, análisis §5.5)
POST …/:id/release Cierra un pagado sin pedido sin crear el pedido: refund_pending si hay pago capturado, rolled_back si no
GET /admin/payment-attempts/stats Conteos por estado en la ventana, abiertos ahora, pagados sin pedido, devoluciones pendientes y salud del monitoreo de Bancard
Página "Intentos de pago" Barra lateral del admin; tabla con filtros (estado, medio, fecha), búsqueda y vista por defecto de no confirmados; indicadores arriba; detalle con acciones confirmadas
Widget "Intentos de pago" en el pedido Zona order.details.after: los intentos del carrito que originó el pedido, incluidos los rechazos previos
Widget "Datos del pago" en el pedido Zona order.details.side.after: estado, monto, referencia, ticket, respuesta, fechas; nunca número de autorización ni datos de tarjeta
Job payment-attempts-daily-summary 08:00 de Asunción: resumen de intentos no confirmados de las últimas 24 h al correo SALES_DEPARTMENT_EMAIL, con alertas

Alertas (observabilidad, tarea #165)

Alerta Dónde Señal en el log
Pago cobrado sin pedido Resumen diario, conciliación cada 2 min, indicador del admin [bancard-vpos] conciliación: {...} — hay intentos pagados sin pedido… (warn)
Devolución manual pendiente Resumen diario, indicador del admin [bancard-vpos] Reversión manual pendiente… (error)
Monitoreo de Bancard sin señal (> 15 min sin el POST vacío) Conciliación del vPOS, resumen diario, indicador del admin [bancard-vpos] ALERTA: monitoreo de Bancard sin señal desde … (warn)
Confirmación con token inválido Webhook [bancard-vpos] webhook: token inválido… (warn)
Tasa de rechazo alta (≥ 30 % con ≥ 5 pagos decididos) Resumen diario [payment-attempts] resumen diario … — ALERTAS: … (warn)

Las reglas de alerta en Grafana/Loki se definen sobre esos prefijos (tarea de infraestructura).

Reglas de negocio

  • Solo se puede operar (consultar, revertir, liberar, crear pedido) sobre el intento vigente de una sesión existente; los archivados son de solo lectura.
  • Revertir solo aplica a created o pending; crear el pedido y liberar, solo a paid_without_order. Toda acción pide confirmación en la interfaz.
  • Ningún listado ni detalle expone claves, tokens, huella del carrito ni datos de tarjeta.
  • Rutas bajo /admin protegidas por la autenticación estándar del admin.

Configuración

Variable Uso
SALES_DEPARTMENT_EMAIL Destinatario del resumen diario (vacío: no se envía, solo se registra en el log)
ADMIN_CORS Su primer origen arma los enlaces al admin en el correo

Criterios de aceptación

# Criterio Prueba
1 Las rutas exigen autenticación de admin integration-tests/http/payment-attempts-admin.spec.ts
2 El listado une QR y tarjeta, filtra por medio, texto y estado, y no expone datos internos ídem
3 El detalle trae intento, sesión, carrito con ítems, pedido y acciones según el estado ídem
4 Consultar devuelve la respuesta de la pasarela sin token; revertir cierra el intento y desbloquea el carrito; repetirlo da 400 ídem
5 Un intento confirmado muestra su pedido y su pago capturado; un intento cerrado sigue listándose tras uno nuevo (historial) ídem
6 El resumen diario cuenta los no confirmados y arma las alertas; sin correo configurado solo registra ídem
7 stats refleja la señal de monitoreo de Bancard ídem
8 Página, detalle y widgets: verificación manual en el admin (/app/intentos-de-pago) Manual

Limitaciones conocidas

  • El monitoreo de Bancard se guarda en memoria del proceso: con réplicas, cada una ve su última señal; tras un reinicio cuenta como "sin señal" hasta el primer POST.
  • Bancard QR no ofrece consulta de estado; la acción "Consultar" solo existe para vPOS.
  • El resumen diario lista hasta 200 intentos.