Saltar a contenido

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 postMessage al navegador y por POST servidor 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 cabecera Authorization: Bearer. Bancard usa public_key en el cuerpo más un token md5 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, con target apuntando al iframe. Es más simple que el bancard-checkout-5.0.1.js que hoy servimos desde public/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-Policy y allow="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:

  • amount es 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:// o https://) y dominio con extensión. No admite localhost, 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 del shop_process_id de 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:

  1. 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.)
  2. Mirar el campo de estado antes de dar el pedido por pagado.
  3. Validar contra la API antes de tratar la operación como definitiva.
  4. Correlacionar por clientReferenceId.
  5. Restringir el endpoint a los rangos de IP de Dinelco. La documentación no publica esos rangos: hay que pedirlos.
  6. Conciliar contra la API las notificaciones que no lleguen.
  7. 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=…, comparar payment.status, amount y currency contra 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 POST vací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 APPROVED de 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.approved o payment.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 message y errorCode.
  • De validación: tipo, formato o campo faltante. Devuelve statusCode, message (arreglo, con el detalle por campo) y error.
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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.