Integración Bancard vPOS (tarjeta por iframe)¶
| Producto | Bancard vPOS 2.0 "Single Buy", formulario de tarjeta embebido en un iframe |
| Manual | "eCommerce Bancard — Compra simple" v1.23.1 (PDF del portal de comercios; no se versiona en el repo) |
| API | POST {base}/vpos/api/0.3/..., JSON, sin cabeceras de autenticación: la autenticidad la da un token md5 por operación |
| Requisito funcional | RF-011 Pago con tarjeta por Bancard vPOS |
| Modelo interno | Intentos de pago |
Ambientes¶
| Staging | Producción | |
|---|---|---|
| API | https://vpos.infonet.com.py:8888 |
https://vpos.infonet.com.py |
| Script del iframe | bancard-checkout-5.0.1-sandbox.js |
bancard-checkout-5.0.1.js |
| Claves | Propias de la aplicación de staging | Se cargan tras la certificación |
El script se sirve desde nuestro sitio (public/vendor/bancard/), no desde un CDN de Bancard,
para que la política de seguridad de contenido no tenga que permitir un origen de scripts externo.
El iframe sí carga desde el origen de Bancard, declarado en la CSP con
NEXT_PUBLIC_BANCARD_VPOS_ORIGIN.
Credenciales y tokens¶
Cada aplicación tiene una clave pública (viaja en el cuerpo de cada pedido) y una clave privada (nunca sale del backend; solo sirve para calcular y verificar tokens md5).
| Operación | Token = md5 de |
|---|---|
single_buy |
private_key + shop_process_id + amount + currency |
| Confirmación (la que manda Bancard) | private_key + shop_process_id + "confirm" + amount + currency |
single_buy/confirmations |
private_key + shop_process_id + "get_confirmation" |
single_buy/rollback |
private_key + shop_process_id + "rollback" + "0.00" |
El monto entra en el token como texto con dos decimales y punto ("10330.00"). Por eso el
intento guarda amount_text: al verificar la confirmación se usa el monto guardado, nunca el que
llega en el mensaje. Implementación en src/modules/bancard-vpos/tokens.ts.
Operaciones que usamos¶
POST /vpos/api/0.3/single_buy — iniciar la compra¶
{
"public_key": "...",
"operation": {
"token": "md5(private_key + shop_process_id + amount + currency)",
"shop_process_id": 175823041200101,
"currency": "PYG",
"amount": "10330.00",
"additional_data": "",
"description": "Compulandia",
"return_url": "https://.../py/payment/bancard/vpos/return?cart_id=...",
"cancel_url": "https://.../py/payment/bancard/vpos/return?cart_id=...&canceled=1"
}
}
Respuesta: {"status": "success", "process_id": "..."}. Ese process_id es lo único que el
navegador necesita para montar el iframe.
Restricciones del manual que respetamos: description de hasta 20 caracteres,
shop_process_id entero de hasta 15 dígitos y único por operación, monto como texto.
POST /vpos/api/0.3/single_buy/confirmations — consultar el estado¶
La usa la conciliación cuando la confirmación no llegó. Es de lectura: se aceptan como respuestas
válidas PaymentNotFoundError (el cliente no llegó a pagar) y AlreadyRollbackedError, que llega
con HTTP 422. Por eso el cliente HTTP tolera los códigos 400, 404, 409 y 422 en esta
operación: tratarlos como fallo hacía que el admin mostrara "error desconocido".
POST /vpos/api/0.3/single_buy/rollback — revertir¶
Solo funciona el mismo día y por el total, antes de que la transacción esté cuponada. Se
consideran éxito success, PaymentNotFoundError y AlreadyRollbackedError. Cualquier otra
respuesta se informa al operador con su motivo, para que gestione la devolución por el portal de
comercios.
Confirmación entrante — URL de confirmación¶
Bancard hace POST a la URL cargada en el portal de comercios con el objeto operation:
{
"operation": {
"token": "md5(private_key + shop_process_id + 'confirm' + amount + currency)",
"shop_process_id": "175823041200101",
"response": "S",
"response_details": "...",
"amount": "10330.00",
"currency": "PYG",
"authorization_number": "...",
"ticket_number": "...",
"response_code": "00",
"response_description": "Transaccion aprobada",
"security_information": { "customer_ip": "...", "risk_index": "0" }
}
}
Reglas del manual y cómo las cumplimos:
| Regla | Implementación |
|---|---|
| Responder HTTP 200 en menos de 30 s, sin reintentos de parte de Bancard | La respuesta se decide y se envía antes de crear el pedido; el resto se procesa después |
| El cuerpo de la respuesta lleva el envoltorio de Bancard | {"status":"success"} o {"status":"error","messages":[{...}]} |
Monitoreo: POST vacío cada 5 minutos |
Se responde success y se registra como señal de vida (observabilidad) |
| Confirmaciones duplicadas | Idempotente: un intento ya confirmado con pedido responde success sin repetir nada |
| Autenticidad | Token md5 verificado contra el monto y la moneda guardados en el intento |
La URL no lleva autenticación de Medusa (Bancard no envía credenciales): la seguridad es el token.
Debe quedar accesible desde internet para POST, sin Access ni desafío del WAF.
| Ambiente | URL de confirmación |
|---|---|
| Staging | https://stg-store.compulandia.com.py/webhooks/bancard-vpos |
| Producción | https://medusa.compulandia.com.py/webhooks/bancard-vpos |
Códigos de respuesta¶
response es S (aprobada) o N (rechazada); response_code 00 es aprobación. Al cliente se
le muestra el motivo del catálogo de la red por código ("Fondos insuficientes", "Tarjeta
vencida") y un consejo, nunca el código ni la respuesta extendida: el manual lo prohíbe en la
página de resultado. Catálogo en src/modules/bancard-vpos/types.ts.
Claves de error de la API¶
InvalidJsonError, UnauthorizedOperationError, ApplicationNotFoundError,
InvalidPublicKeyError, PublicKeyNotFoundError, InvalidTokenError, InvalidOperationError,
BuyNotFoundError, PaymentNotFoundError, AlreadyRollbackedError, PosCommunicationError,
TransactionAlreadyConfirmed. La más frecuente al empezar es UnauthorizedOperationError:
significa que la aplicación no tiene habilitada esa operación, y lo resuelve Bancard.
Estructura de archivos¶
src/modules/bancard-vpos/
├── index.ts # registro del proveedor
├── service.ts # proveedor de pago de Medusa (autorizar, capturar, reembolsar)
├── client.ts # cliente HTTP de la API 0.3
├── tokens.ts # cálculo y verificación de los tokens md5
├── types.ts # tipos del manual, claves de error, catálogo de rechazos
├── config.ts # opciones desde el entorno, id del proveedor
├── webhook-handler.ts # decisiones del webhook, sin base de datos (probable en aislamiento)
├── webhook-deps.ts # acceso a datos que necesita el manejador
├── attempt-status.ts # estado que ve el storefront
├── reconcile.ts # conciliación de intentos sin confirmación
└── simulator.ts # Bancard simulado para desarrollo
src/workflows/bancard-vpos/ # abrir y cancelar el intento
src/api/store/bancard-vpos/ # process, status, cancel, simulator
src/api/webhooks/bancard-vpos/ # URL de confirmación
src/jobs/bancard-vpos-reconcile.ts # cada 2 minutos
Variables de entorno¶
BANCARD_VPOS_PUBLIC_KEY=...
BANCARD_VPOS_PRIVATE_KEY=... # secreto; solo para tokens md5
BANCARD_VPOS_API_URL=https://vpos.infonet.com.py:8888 # sin puerto en producción
BANCARD_VPOS_RETURN_URL_BASE=https://.../py/payment/bancard/vpos/return
BANCARD_VPOS_CONFIRMATION_TIMEOUT_MIN=10
BANCARD_VPOS_SIMULATOR=false # true solo en desarrollo
En producción las claves van en Secret Manager (ADR-0005).
Con BANCARD_VPOS_SIMULATOR=true el arranque falla si el ambiente es productivo.
Seguridad¶
- Los datos de la tarjeta nunca pasan por nuestros servidores: los recibe el iframe de Bancard.
- La clave privada solo se usa para tokens; no se registra en el log ni se devuelve por ninguna ruta.
- No se persisten el número de autorización, la información de seguridad, el token ni la respuesta
extendida, porque
payment.datallega al navegador por la API store. - Las rutas
/store/bancard-vpos/*exigen la clave publicable y el carrito dueño; si el carrito tiene cliente y hay cliente autenticado, deben coincidir. - La ruta del simulador responde 404 cuando el simulador está apagado.
Pruebas¶
| Suite | Qué cubre |
|---|---|
src/modules/bancard-vpos/__tests__/ |
Tokens md5, cliente HTTP, decisiones del webhook, conciliación |
integration-tests/http/bancard-vpos.spec.ts |
Apertura, bloqueo del carrito, estado, cancelación, webhook, rechazo, reembolso |
integration-tests/http/bancard-vpos-reconcile.spec.ts |
Conciliación de intentos sin confirmación |
integration-tests/http/bancard-vpos-simulator.spec.ts |
Camino del simulador |
Bancard se simula con un fetch de prueba; ninguna suite sale a la red.
Referencias¶
- Manual "eCommerce Bancard — Compra simple" v1.23.1 (portal de comercios).
- Análisis de la integración.
- ADR-0006 · Pedido después del pago.
- Runbook · Intentos de pago.