Análisis · Pago con tarjeta por Dinelco Checkout embebido (iframe)¶
Qué es este documento. Estudio de la documentación oficial de dinelco Checkout (BEPSA) para el Embedded Checkout, con el detalle que hace falta para decidir si lo integramos y cuánto trabajo cuesta. Todo lo que dice está contrastado contra lo que ya construimos para Bancard vPOS (ver integracion-bancard-vpos-iframe.md), porque la pregunta real no es "¿se puede?" sino "¿cuánto de lo que ya tenemos sirve?".
Fuente.
https://dev-sgwf-01.bepsa.com.py/dinelco-checkout/docs/es/, consultada el 2026-09-17. Es la documentación del ambiente de pruebas, publicada por BEPSA. Todavía no tenemos alta comercial, ni claves, ni contrato: nada de esto está probado contra el servicio real.
1. Resumen¶
- Qué ofrece. Lo mismo que Bancard vPOS: un formulario de tarjeta de Dinelco dentro de un iframe en nuestro checkout. Los datos de la tarjeta nunca tocan nuestros servidores, así que la responsabilidad PCI sigue siendo del proveedor.
- En qué se parece a Bancard. El esqueleto es idéntico: el backend abre una sesión de
pago contra la pasarela, devuelve un identificador al navegador, el navegador abre el
iframe, el cliente paga, la pasarela avisa por
postMessageal navegador y porPOSTservidor a servidor a una URL nuestra. Nuestro patrón actual (módulo de proveedor de pago, registro de intentos, webhook que responde rápido y procesa después, conciliación por consulta, SSE hacia el navegador) encaja sin cambios de arquitectura. - En qué se diferencia, y esto es lo importante.
- Autenticación moderna. Dinelco usa una clave secreta (
di_sk_…) en cabeceraAuthorization: Bearer. Bancard usapublic_keyen el cuerpo más untokenmd5 calculado con la clave privada. Con Dinelco desaparece toda la maquinaria de md5. - El iframe no se abre con un script. No hay SDK. Se abre posteando un formulario
oculto con un JWT (
integrityToken) contra una URL de Dinelco, contargetapuntando al iframe. Es más simple que elbancard-checkout-5.0.1.jsque hoy servimos desdepublic/vendor/, pero exige un<form method="POST" target="…">real. - El webhook no viene firmado. Bancard manda un md5 que valida autenticidad y monto en un solo paso. En la documentación de Dinelco no aparece ninguna firma ni cabecera de verificación: la propia documentación dice que hay que "validar contra la API antes de dar la operación por definitiva". Eso convierte la consulta de sesión en obligatoria, no opcional. Es el punto más delicado de la integración.
- 3D Secure incluido. Dinelco trae 3DS de Cybersource activado por análisis de riesgo,
sin trabajo nuestro. En Bancard el 3DS es un formulario aparte (
Charge3DS). - Trae medios que Bancard vPOS no tiene: Click To Pay y Google Pay, que exigen
cabecera
Permissions-Policyyallow="payment"en el iframe. - Lo que falta y bloquea. Alta comercial con BEPSA, claves por ambiente, registro de los
dominios desde los que embebemos (si no, el navegador bloquea el iframe por
frame-ancestors), y la URL de notificación por defecto del comercio. La documentación pública no dice nada sobre costos, comisiones, ni si hay un proceso de certificación como el de Bancard.
2. Requisitos previos¶
| Requisito | Detalle | Responsable |
|---|---|---|
| Alta de comercio | Proceso de registro con BEPSA / Dinelco. La documentación no publica costos ni comisiones, ni el circuito comercial. Hay que preguntarlo. | Comercial / Administración |
| Clave secreta de API | Formato di_sk_ + bloque hexadecimal + segmento final. Se pide una por ambiente (pruebas y producción). Solo en el backend: si se filtra, cualquiera puede generar cobros a nombre del comercio |
TI |
| Permisos de la clave | Los endpoints validan permisos por clave: un 403 Insufficient API Key permissions significa clave válida sin ese permiso habilitado. Para reversas hace falta payment:reversal |
TI |
| Dominios autorizados | Hay que registrar con Dinelco todos los dominios desde los que se embebe el checkout, incluido staging. Dinelco los pone en su frame-ancestors; un dominio no registrado lo bloquea el navegador |
TI |
| URL de notificación por defecto | Se configura en el perfil del comercio. Sirve de respaldo cuando la sesión no trae callbackUrl, y es la única vía para cobros con token y reversas |
TI |
| HTTPS con certificado válido | Obligatorio. Click To Pay y Google Pay no funcionan sobre HTTP ni con contenido mixto | TI (ya cumplido) |
Cabecera Permissions-Policy |
payment=* en nuestra respuesta, más allow="payment" en el iframe, si se quiere Google Pay |
TI |
| Valor del desafío 3DS de pruebas | Para completar el desafío 3DS en el ambiente de pruebas hay que pedirle a soporte de Dinelco el valor vigente para nuestro comercio | TI |
3. Configuración: ambientes y URLs¶
| Pruebas | Producción | |
|---|---|---|
| Base de la API | https://dev-sgwf-01.bepsa.com.py |
https://checkout.dinelco.com.py |
| Prefijo | /dinelco-checkout/api/v1/ (el cobro con token es v3) |
igual |
Origen del iframe (validar event.origin) |
https://dev-sgwf-01.bepsa.com.py |
https://checkout.dinelco.com.py |
| URL de validación (destino del formulario) | https://dev-sgwf-01.bepsa.com.py/d/api/checkout-session/validate |
https://checkout.dinelco.com.py/d/api/checkout-session/validate |
A diferencia de Bancard, el cambio de ambiente es solo un cambio de dominio: no hay puerto raro (el 8888 de staging de Bancard, que además nos obligó a habilitar salida en el cortafuegos) ni un build distinto del script.
Variables de entorno que haría falta agregar, calcando el módulo de vPOS:
DINELCO_API_KEY=di_sk_… # solo backend, nunca en el storefront
DINELCO_API_URL=https://dev-sgwf-01.bepsa.com.py
DINELCO_CALLBACK_URL=https://stg-store.compulandia.com.py/webhooks/dinelco
NEXT_PUBLIC_DINELCO_ORIGIN=https://dev-sgwf-01.bepsa.com.py # para CSP y para validar postMessage
En el storefront hay que sumar el origen de Dinelco a frame-src en
src/proxy.ts del storefront, igual que se hizo con Bancard
(ver ADR-0007). No hace falta script-src: no hay script de terceros que cargar.
4. Endpoints¶
Todos con Authorization: Bearer di_sk_… y Content-Type: application/json, salvo los dos
últimos, que los usa el navegador y no llevan clave.
| Método | Ruta | Para qué | ¿Lo usamos? |
|---|---|---|---|
| POST | /dinelco-checkout/api/v1/checkout-session |
Abrir la sesión de pago y obtener el integrityToken |
Sí |
| GET | /dinelco-checkout/api/v1/checkout-session/{sessionId} |
Consultar el estado por identificador de sesión | Sí (conciliación) |
| GET | /dinelco-checkout/api/v1/checkout-session?clientReferenceId={id} |
Consultar por nuestra referencia | Sí (conciliación) |
| POST | /dinelco-checkout/api/v1/payment/reversal |
Anular un pago aprobado | Sí, segunda etapa |
| POST | /dinelco-checkout/api/v1/payment-link |
Link de pago (cobro fuera del sitio) | No |
| POST | /dinelco-checkout/api/v1/checkout-session/registry |
Sesión de catastro de tarjeta | Segunda etapa |
| POST | /dinelco-checkout/api/v1/registry/setup-link |
Link de catastro | No |
| POST | /dinelco-checkout/api/v3/payment |
Cobrar con una tarjeta catastrada | Segunda etapa |
| POST | /d/api/checkout-session/validate |
Desde el navegador: destino del formulario que abre el iframe | Sí |
| POST | /d/api/registry/validate |
Ídem para el catastro | Segunda etapa |
Comparado con Bancard, que resuelve todo con POST a /vpos/api/0.3/single_buy,
/confirmations y /rollback distinguiendo la operación por un md5, acá cada operación tiene
su propia ruta y método. Es una API más convencional y más fácil de tipar.
5. Abrir la sesión de pago¶
POST /dinelco-checkout/api/v1/checkout-session
{
"clientReferenceId": "cart_01K…",
"amount": 120000,
"currency": "PYG",
"targetOrigin": "https://shop.compulandia.com.py",
"callbackUrl": "https://stg-store.compulandia.com.py/webhooks/dinelco",
"returnUrl": "https://shop.compulandia.com.py/py/payment/dinelco/return",
"lineItems": [
{ "name": "Notebook", "description": "14 pulgadas", "price": 120000, "quantity": 1, "img": "https://…" }
],
"metadata": { "cartId": "cart_01K…", "attemptId": "…" },
"customer": {
"customerId": "cus_01K…",
"name": "Fernando",
"lastname": "López",
"email": "fernando.lopez@example.com",
"phone": "+595991000000"
}
}
Obligatorios: amount, currency, targetOrigin.
Opcionales: todo lo demás.
Detalles que importan:
amountes un número entero en guaraníes (120000). Bancard exige un texto con dos decimales y punto ("120000.00"). Un detalle chico que ya nos costó un error en vPOS.currency:PYG.targetOrigin: los dominios desde los que se abre el checkout, separados por coma. No puede ir vacío. Declara, no configura: las políticas del navegador las arma Dinelco con los dominios que registremos en el alta, así que los dos tienen que coincidir.callbackUrl: exige esquema (http://ohttps://) y dominio con extensión. No admitelocalhost, ni con puerto. Sí admite direcciones IP, subdominios y puertos. Esto obliga a que el desarrollo local reciba las notificaciones por un túnel, igual que hoy con Bancard.clientReferenceId: nuestra referencia. Si no se envía, Dinelco asigna una interna y no la devuelve, con lo cual quedamos sin forma de correlacionar. Hay que enviarla siempre. Es el equivalente exacto delshop_process_idde Bancard, pero es texto, no un entero de hasta 15 dígitos: se puede mandar el identificador del intento tal cual, sin la derivación numérica que tuvimos que inventar para Bancard.metadata: objeto plano, nunca un arreglo. Hay límites de cantidad de claves, largo de nombres y de valores, anidamiento y tamaño total, pero la documentación no publica los números, y un 400 no dice qué clave lo causó. Guardar solo identificadores.lineItems: solo para que el cliente vea el detalle dentro del iframe. Ojo: si se usan, el ancho mínimo del iframe sube a 1100 px (ver §8).
Respuesta 201 Created:
{
"integrityToken": "eyJhbGc…",
"expirationDate": "2026-09-17T16:20:49.000Z",
"sessionId": 179
}
El integrityToken es un JWT de vida corta: hay que respetar expirationDate y volver a
abrir sesión si venció. Guardar sessionId en el intento, que es la llave para consultar
después.
Esta llamada va desde el backend, nunca desde el navegador: lleva la clave secreta. Igual que hoy con
POST /store/bancard-vpos/process.
6. Abrir el iframe en el navegador¶
No hay SDK. Se hace con un formulario oculto que postea el JWT contra la URL de validación,
con target apuntando al iframe:
<iframe id="dinelcoCheckout" name="dinelcoCheckout" style="width:100%;height:720px;border:0"
allow="payment"></iframe>
<form id="checkoutForm" method="POST" target="dinelcoCheckout" style="display:none"
action="https://dev-sgwf-01.bepsa.com.py/d/api/checkout-session/validate">
<input type="hidden" name="JWT" id="integrityToken" />
</form>
document.getElementById("integrityToken").value = integrityToken
document.getElementById("checkoutForm").submit()
Requisitos del marcado, tal como los pide la documentación: el iframe con name="dinelcoCheckout"
(es el destino del formulario), el campo oculto con name="JWT", y la URL de validación
exacta, sin variantes.
En React/Next hay que hacerlo con referencias y disparar el submit() en un efecto, cuidando
la hidratación. La documentación trae un ejemplo específico para Next.js con App Router: una
ruta app/api/checkout/create-session/route.ts que llama a Dinelco con la clave desde
process.env, y un componente cliente que hace el resto. En nuestro caso esa ruta no va en
el storefront: el que habla con la pasarela es Medusa, como con Bancard.
Diferencia concreta contra Bancard: hoy servimos bancard-checkout-5.0.1.js desde
public/vendor/ (con su versión sandbox aparte para staging), llamamos a
Bancard.Checkout.createForm(...) y escuchamos los avisos de alto (iframeHeight) con
src/lib/bancard-iframe.ts del storefront porque su script
solo ajusta min-height y el formulario quedaba cortado. Con Dinelco no hay script, no hay
avisos de alto y no hay ajuste dinámico: se fija un alto según la tabla de la §8 y listo.
Es decir: esa lógica de alto no se reutiliza para Dinelco, pero sigue siendo de Bancard
y se queda donde está mientras Bancard siga cobrando. Solo se borraría si algún día Dinelco
reemplaza a Bancard, que hoy no está decidido. Lo que se pierde del lado de Dinelco es la
adaptación automática: el alto lo fijamos nosotros.
7. Eventos hacia el navegador¶
El iframe avisa por postMessage. Dos eventos:
| Evento | Cuándo | Contenido |
|---|---|---|
payment.success |
El pago fue aprobado | paymentStatus, receiptData (número de operación, código de autorización, monto, moneda, fecha), timestamp |
payment.failed |
El pago falló | paymentStatus, data con el detalle del rechazo, timestamp |
window.addEventListener("message", (event) => {
if (event.origin !== DINELCO_ORIGIN) return // imprescindible
const data = event.data
if (data.paymentStatus === "payment.success") { /* … */ }
})
La documentación es explícita: sin comparar event.origin, cualquier otro contenido embebido
en la página puede publicar un mensaje con la forma de un evento del checkout. Es la misma
regla que ya aplicamos con Bancard, y la misma advertencia que ya está en nuestro análisis:
lo que dice el navegador es informativo, no prueba que el pago exista. La verdad la da la
notificación servidor a servidor, y con Dinelco, además, la consulta a la API.
8. Tamaños y aspecto¶
| Configuración | Mínimo |
|---|---|
| Sin datos personales | 680 × 720 px |
| Con datos personales | 680 × 860 px |
| Con detalle de productos | 1100 × 860 px |
Recomendaciones de la documentación: width: 100%, mismo alto en escritorio y móvil, márgenes
con calc(100% - 16px) para no provocar barra horizontal, y nunca overflow: hidden ni
alturas por debajo del mínimo (se cortan botones y campos). Soporta modo oscuro.
Esto choca de frente con nuestro checkout actual: la columna donde hoy va el iframe de Bancard
es angosta (por eso hicimos el ajuste dinámico de alto con piso de 620 px). 1100 px de ancho
no entran en esa columna. Si se quiere el detalle de productos dentro del iframe hay que
rediseñar el paso de pago a ancho completo, o directamente no usar lineItems y quedarse con
680 px, que sí entra. Es una decisión de diseño a tomar antes de estimar.
9. La notificación servidor a servidor¶
Dinelco hace POST con Content-Type: application/json a la URL que corresponda.
Precedencia de la URL (se notifica a la primera disponible):
| Operación | Orden |
|---|---|
| Pago / link | callbackUrl del link → callbackUrl de la sesión → URL por defecto del comercio |
| Catastro | callbackUrl del link de catastro → de la sesión → URL por defecto |
| Cobro con token y reversas | Solo la URL por defecto del comercio (no se puede sobrescribir por operación) |
Esto es mejor que Bancard, donde la URL de confirmación vive solo en el perfil del portal y no viaja en la petición: acá podemos mandar la URL correcta por ambiente desde el código.
Contenido (checkout embebido): message, clientReferenceId, metadata, payment
(estado, número de operación, código de autorización, monto, moneda, fecha), paymentInfo
(marca y número enmascarado), merchantInfo, merchantCustomer, customer.
Cuidado: el sobre cambia según la operación. El checkout embebido trae los campos en la
raíz; los cobros con token usan { event, timestamp, data }; las reversas usan ese mismo sobre
con payment.reversal adentro. Si más adelante sumamos catastro, el manejador tiene que
distinguir el formato.
Qué hay que hacer, según la documentación:
- Responder HTTP 200 enseguida y procesar después. (Es exactamente lo que ya hace
src/api/webhooks/bancard-vpos/route.ts, por el límite de 30 segundos de Bancard. El patrón se reusa tal cual.) - Mirar el campo de estado antes de dar el pedido por pagado.
- Validar contra la API antes de tratar la operación como definitiva.
- Correlacionar por
clientReferenceId. - Restringir el endpoint a los rangos de IP de Dinelco. La documentación no publica esos rangos: hay que pedirlos.
- Conciliar contra la API las notificaciones que no lleguen.
- No filtrar solo por aprobado: los rechazos, fallos y cancelaciones también notifican.
Lo que no dice la documentación, y es el hueco grande: no hay firma, ni cabecera de
verificación, ni secreto compartido, ni política de reintentos documentada. Con Bancard, el
md5 private_key + shop_process_id + "confirm" + amount + currency nos deja verificar
autenticidad y monto sin salir del proceso. Acá no hay equivalente, y por eso la propia
documentación manda a consultar la API. En la práctica:
El webhook de Dinelco es un aviso, no una prueba. Nunca capturar ni crear el pedido con lo que llega en el cuerpo. Al recibirlo: responder 200, y recién entonces hacer
GET /checkout-session?clientReferenceId=…, compararpayment.status,amountycurrencycontra lo guardado en el intento, y solo ahí capturar.
Eso agrega una llamada de red al camino crítico que con Bancard no existe, y hace más importante todavía el registro de intentos y la conciliación que ya tenemos.
Faltan también las respuestas a estas preguntas, que hay que hacerle a soporte de Dinelco:
- ¿Reintentan la notificación si no responde 200? ¿Cuántas veces, con qué espera?
- ¿Cuál es el tiempo máximo para responder? (Bancard: 30 segundos)
- ¿Hay una señal de vida periódica como el
POSTvacío cada 5 minutos de Bancard? - ¿Cuáles son los rangos de IP de origen?
- ¿Hay alguna firma o cabecera de verificación no documentada?
10. Consultar el estado de la sesión¶
GET /checkout-session/{sessionId} o GET /checkout-session?clientReferenceId={id}.
Misma respuesta en los dos casos.
Estados de la sesión (sessionStatus):
| Valor | Significado |
|---|---|
REQUIRES_PAYMENT |
Espera que el cliente complete el pago |
REQUIRES_ACTION |
Necesita una acción adicional del cliente (típicamente el desafío 3DS) |
SUCCESS |
La sesión se completó |
Estados del pago (payment.status):
| Valor | Significado |
|---|---|
APPROVED |
Aprobado |
REJECTED |
Rechazado |
PROCESSING |
En proceso |
VOIDED |
Anulado por una reversa |
uiMode vale EMBEDDED (sesión creada por API para el iframe) o HOSTED (link de pago o de
catastro).
Respuesta, recortada a lo que usaríamos:
{
"sessionId": 1641,
"sessionStatus": "SUCCESS",
"uiMode": "EMBEDDED",
"clientReferenceId": "cart_01K…",
"metadata": { "cartId": "cart_01K…" },
"payment": {
"id": 939,
"status": "APPROVED",
"message": "Pago realizado con éxito. ¡Gracias por su compra!",
"extendedMessage": "APROBADO",
"operationNumber": "526124796438",
"authorizationCode": "796438",
"amount": 5000,
"currency": "PYG",
"transactionDate": "2026-09-17T17:15:15.577Z"
},
"paymentInfo": {
"paymentMethodType": "CARD",
"paymentMethodPayload": { "cardBrand": "DIN", "cardNumber": "620000******0047" }
}
}
Ventaja clara sobre Bancard: payment.message viene redactado para mostrarle al cliente.
Con Bancard tuvimos que armar a mano el catálogo de ~80 códigos de la red y nuestros propios
textos de ayuda en
src/modules/bancard-vpos/types.ts (RESPONSE_CODE_DESCRIPTIONS,
customerFacingReason, customerFacingAdvice), porque el texto crudo de Bancard
("Transacción denegada") no le dice nada a nadie. Acá eso podría no hacer falta —a
confirmar con rechazos reales en el ambiente de pruebas, porque la documentación no publica
la tabla de mensajes de rechazo.
Tampoco hay nada parecido al índice de riesgo (0 a 9) de Bancard, con su regla operativa de no entregar mercadería con riesgo alto sin confirmar. Dinelco resuelve el riesgo adentro, con 3DS de Cybersource, y no nos devuelve un indicador. Si operaciones depende hoy de ese dato para Bancard, con Dinelco no lo va a tener.
11. Anular un pago¶
POST /dinelco-checkout/api/v1/payment/reversal (requiere el permiso payment:reversal).
{ "operationNumber": "668908000639", "clientReferenceId": "ORD-14787-1774879943" }
Respuesta 200: paymentStatus: "VOIDED" y un objeto reversal con estado, mensaje,
responseCode y fecha.
Reglas:
- Solo pagos en estado
APPROVEDde las últimas 24 horas. - Total, no parcial: el cuerpo no acepta monto.
- Solo pagos con tarjeta.
- Errores típicos:
APPROVED_PAYMENT_NOT_FOUND_FOR_REVERSAL(no existe, ya fue anulado o pasó el plazo),PAYMENT_METHOD_TYPE_NOT_SUPPORTED_FOR_REVERSAL,CARD_BRAND_NOT_SUPPORTED_FOR_REVERSAL. - Genera notificación:
payment.reversal.approvedopayment.reversal.rejected. Hay que procesar las dos; quedarse con las aprobadas esconde las reversas que fallaron.
Contra Bancard: el rollback de Bancard sirve solo el mismo día y después hay que pedir la
anulación por el portal; Dinelco da 24 horas corridas, que es algo más holgado. Los dos son
totales. En ninguno de los dos el reembolso automático sirve como red de seguridad para un pago
cobrado sin pedido: la ventana es demasiado corta.
12. Catastro de tarjetas (segunda etapa)¶
Mismo concepto que el catastro de Bancard, que ya tenemos empezado en
feature/bancard-vpos-catastro:
| Paso | Endpoint |
|---|---|
| Abrir sesión de catastro | POST /dinelco-checkout/api/v1/checkout-session/registry |
| Abrir el iframe | POST /d/api/registry/validate (mismo mecanismo de formulario + JWT) |
| Cobrar con el token | POST /dinelco-checkout/api/v3/payment |
Detalles: en la sesión embebida se manda amount: 0 (en el link de catastro va un monto de
verificación); 3DS es obligatorio para catastrar Visa y Mastercard; las tarjetas Dinelco
usan "dinelco Challenge" en lugar de 3DS. El catastro no cobra: solo deja el token para
cobrar después.
Fuera del alcance de una primera entrega, igual que con Bancard.
13. Errores¶
Dos formas de error:
- De negocio: el cuerpo es válido pero una regla lo rechaza. Devuelve
messageyerrorCode. - De validación: tipo, formato o campo faltante. Devuelve
statusCode,message(arreglo, con el detalle por campo) yerror.
| HTTP | Cuándo |
|---|---|
| 400 | No pasó la validación, o una regla de negocio rechazó la operación |
| 401 | Clave ausente, con formato inválido, incorrecta, deshabilitada o vencida |
| 403 | Clave válida sin permiso para ese endpoint |
| 404 | Recurso inexistente o que no pertenece al comercio |
| 500 | Error del servidor; incluye trace y cabecera X-Trace-Id |
Se puede mandar un x-trace-id propio para seguir una petición de punta a punta — vale la pena
usarlo con el identificador del intento, ayuda cuando hay que escalar a soporte. La
documentación aclara que solo tiene sentido reintentar tal cual un 400, porque devuelve siempre
lo mismo.
Es una tabla mucho más chica que la de Bancard (que mezcla claves de error de aplicación como
UnauthorizedOperationError o PublicKeyNotFoundError con los ~80 códigos de la red).
14. Pruebas¶
Tarjetas del ambiente de pruebas (CVV 000, vencimiento futuro cualquiera):
| Tarjeta | Marca | Comportamiento |
|---|---|---|
| 5200 0000 0000 2151 | Mastercard | Con desafío 3DS |
| 4456 5300 0000 1096 | Visa | Con desafío 3DS |
| 5200 0000 0000 2235 | Mastercard | Sin desafío |
| 4456 5300 0000 1005 | Visa | Sin desafío |
Solo funcionan contra https://dev-sgwf-01.bepsa.com.py. Para catastrar hay que usar las
tarjetas con desafío: las otras fallan y disparan una notificación registry.failed.
Para completar el desafío 3DS hay que pedirle a soporte el valor vigente para nuestro comercio.
La documentación incluye además un banco de pruebas interactivo dentro del propio sitio de documentación: arma el JSON, genera el JWT, abre el checkout y muestra los eventos. Sirve para entender el flujo antes de escribir una línea. Como excepción, llama a la API desde el navegador con la clave — eso es solo de la herramienta, no del modelo de integración.
Limitación heredada: callbackUrl no admite localhost, así que para probar el webhook en
desarrollo hace falta una URL pública. Es el mismo problema que ya resolvimos con Bancard, y
el simulador que escribimos para vPOS (BANCARD_VPOS_SIMULATOR) es el patrón a repetir.
15. Comparación punto por punto con Bancard vPOS¶
| Tema | Bancard vPOS (implementado) | Dinelco Checkout embebido | Efecto |
|---|---|---|---|
| Autenticación | public_key en el cuerpo + token md5 por operación |
Authorization: Bearer di_sk_… |
A favor de Dinelco. Desaparece tokens.ts entero |
| Apertura del iframe | Script propio bancard-checkout-5.0.1.js servido desde public/vendor/, build distinto para staging |
Formulario oculto POST con JWT; sin script | A favor de Dinelco. Un archivo menos que versionar y servir; CSP más simple (no hace falta script-src) |
| Identificador del intento | shop_process_id, entero de hasta 15 dígitos; tuvimos que derivarlo |
clientReferenceId, texto libre |
A favor de Dinelco. Se manda el identificador del intento tal cual |
| Formato del monto | Texto con dos decimales: "120000.00" |
Entero: 120000 |
Distinto; ajustar el mapeo |
| URL de notificación | Solo en el perfil del portal, una por ambiente | callbackUrl por sesión, con respaldo en el perfil |
A favor de Dinelco |
| Verificación de la notificación | md5 que valida autenticidad y monto | Ninguna documentada; hay que consultar la API | A favor de Bancard. Es la mayor pérdida del cambio |
| Plazo para responder | 30 s, documentado | No documentado | Preguntar |
| Reintentos de la notificación | No hay; consultar a los 10 min | No documentado | Preguntar |
| Señal de vida | POST vacío cada 5 min |
No documentada | Si no existe, perdemos ese indicador de disponibilidad |
| Consulta de estado | POST /single_buy/confirmations con md5 |
GET /checkout-session/… |
A favor de Dinelco, más natural |
| Mensajes de rechazo | Catálogo de ~80 códigos armado a mano por nosotros | payment.message listo para mostrar |
A favor de Dinelco, a confirmar en pruebas |
| Índice de riesgo | 0 a 9, con regla operativa | No lo devuelve (3DS resuelve adentro) | Se pierde un dato que operaciones usa |
| 3D Secure | Formulario aparte (Charge3DS) |
Automático, Cybersource, por análisis de riesgo | A favor de Dinelco |
| Anulación | Solo el mismo día, total | 24 h, total | Leve ventaja de Dinelco |
| Medios de pago | Tarjetas, Zimple | Tarjetas, Click To Pay, Google Pay | A favor de Dinelco (con más requisitos de cabeceras) |
| Ancho del iframe | Mínimo 320 px | Mínimo 680 px, 1100 px con productos | A favor de Bancard. No entra en la columna actual del checkout |
| Alto del iframe | Lo informa Bancard por postMessage; lo ajustamos nosotros |
Fijo, según tabla | Más simple, menos adaptable |
| Ambientes | Puerto 8888 en staging (hubo que abrir salida) | Solo cambia el dominio | A favor de Dinelco |
| Certificación | Lista de pruebas + solicitud + compra de soporte | No documentada | Preguntar |
| Bloqueo por rechazos | 7 en 24 h bloquean la tarjeta 30 días | No documentado | Preguntar |
| Facturación electrónica | billing en single_buy |
No aparece | Si a futuro importa, es un punto en contra |
| Costos | Gs 74.900 + comisión no publicada | No publicados | Preguntar |
16. Qué se reutiliza de lo que ya tenemos¶
Lo bueno de haber hecho vPOS primero es que la forma ya está. Un módulo dinelco-checkout
sería un calco estructural de src/modules/bancard-vpos/:
Cómo leer esta tabla. La columna dice si la pieza se reutiliza para escribir el proveedor de Dinelco. "No se reutiliza" no significa borrarla: son piezas de Bancard y Bancard sigue funcionando. Salvo que se decida reemplazar una pasarela por la otra, las dos integraciones conviven y cada una se queda con lo suyo.
| Pieza de vPOS | ¿Se reutiliza para Dinelco? |
|---|---|
Módulo de proveedor de pago de Medusa (index.ts, service.ts) |
Sí, misma forma |
client.ts (cliente HTTP con tiempo límite) |
Sí, cambiando autenticación y rutas |
tokens.ts (md5) |
No, Dinelco no usa md5 (el archivo sigue siendo de Bancard) |
types.ts (tipos y catálogo de códigos) |
Se rehace; mucho más corto |
config.ts (opciones desde el entorno) |
Sí, mismo patrón |
webhook-handler.ts + webhook-deps.ts |
Sí, con un cambio de fondo: donde vPOS valida el md5, Dinelco consulta la API |
src/api/webhooks/…/route.ts (responder 200 y procesar después) |
Sí, tal cual |
reconcile.ts + src/jobs/…-reconcile.ts |
Sí, y pesa más que con Bancard |
attempt-status.ts + registro de intentos |
Sí, tal cual |
simulator.ts |
Sí, mismo patrón (imprescindible: no hay localhost) |
Flujos open-attempt / cancel-attempt |
Sí, cambiando rollback por payment/reversal |
SSE y use-bancard-payment-sse.ts |
Sí, es agnóstico de la pasarela |
bancard-iframe.ts (alto dinámico) |
No, el alto de Dinelco es fijo (el archivo sigue siendo de Bancard) |
| Componente del checkout | Se rehace: formulario + iframe en vez de createForm |
public/vendor/bancard/* |
No, Dinelco no tiene script (los archivos siguen siendo de Bancard) |
Entrada de CSP en proxy.ts |
Se agrega el origen de Dinelco en frame-src |
La decisión de arquitectura vigente —para los medios cuyo pago confirma un tercero, el pedido se crea después de capturar el pago, con el stock reservado y el carrito bloqueado mientras tanto— vale igual para Dinelco, sin cambios.
17. Riesgos y cosas a definir¶
- La notificación no viene firmada. Es el punto central. Obliga a consultar la API antes de capturar. Si la consulta falla o tarda, el pago queda pendiente hasta la conciliación. Hay que diseñar ese camino con cuidado y no como excepción rara.
- El ancho mínimo de 680 px no entra en la columna actual del checkout, y 1100 px menos. Decisión de diseño previa a cualquier estimación.
- Preguntas abiertas a soporte de Dinelco: reintentos, plazo de respuesta, señal de vida,
rangos de IP, límites reales de
metadata, tabla de mensajes de rechazo, proceso de certificación, bloqueo por rechazos repetidos, y el valor del desafío 3DS de pruebas. - Preguntas comerciales: costo de alta, comisión por transacción, marcas de tarjeta aceptadas, plazos de acreditación. Nada de eso está publicado. Es lo que va a decidir si Dinelco se suma a Bancard o lo reemplaza.
- Dos pasarelas conviviendo. Si Dinelco entra sin sacar Bancard, hay que definir cómo se elige una u otra (¿el cliente?, ¿por marca de tarjeta?, ¿por disponibilidad?) y unificar la vista de intentos del admin para que no queden dos pantallas parecidas.
- La documentación es del ambiente de pruebas. Está bien armada, pero puede cambiar. Antes de implementar conviene volver a leerla y, sobre todo, probar en el banco de pruebas interactivo.
18. Próximo paso sugerido¶
Antes de escribir código: pedir el alta de pruebas y la clave, y hacer una prueba de punta a punta con el banco de pruebas interactivo de la documentación más un endpoint nuestro de notificación expuesto por túnel. Con eso se responden solas la mitad de las preguntas de la §17, y recién ahí tiene sentido estimar.