Saltar a contenido

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.data llega 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