Transbank y PHP (parte 2): Integrando Webpay Plus Normal

Ricardo Cisterna
16 min readMay 31, 2020

--

Transbank nos entrega distintos productos para pagar en la web a través de su API SOAP¹. Además, deja a nuestra disposición varios SDK para distintos lenguajes, que nos facilitan la vida a la hora de conectarnos al API, haciendo la parte difícil por nosotros. ¿El problema? Que ni la documentación ni los SDK son tan amigables como podrían ser.

Esta es la segunda parte de una serie de guías en que integraremos todos los productos de Transbank utilizando contenedores en Docker y el framework de PHP Lumen. Acá nos conectaremos al API de Transbank para realizar transacciones con el producto Webpay Plus Normal, y lo haremos utilizando el SDK oficial de Transbank para PHP. Consideraremos las validaciones que deben realizarse en las transacciones, y los casos de borde conocidos y documentados por Transbank, así como los casos que usualmente se consultan en su slack.

Aclaración: Estas guías no pretenden ser un tutorial para PHP, Docker, ni ninguna otra de las herramientas que se utilizan, pero consideran breves explicaciones de los cambios realizados en cada paso.

  1. Preparación de ambiente
  2. Integrando Webpay Plus Normal
  3. Integrando Webpay Plus Mall (en proceso)
  4. Integrando Webpay Oneclick Normal (en proceso)
  5. Integrando Webpay Oneclick Mall (en proceso)

Sobre el producto

Webpay Plus Normal es el producto base de Transbank, y también el más conocido. Permite que los usuarios de nuestra plataforma realicen pagos con tarjetas de crédito o débito de distintos bancos, y nos entrega el resultado de la transacción realizada.

El flujo normal de compra es como sigue:

  1. El usuario inicial el proceso de compra.
  2. Redireccionamos al usuario por POST al formulario de Transbank (y del banco), para ingresar datos de autorización y pago.
  3. El usuario es redireccionado de regreso por POST para obtener el resultado del pago y reconocer la transacción.
  4. Redireccionamos al usuario por POST a la vista final de Transbank, donde se le indica el resultado de su proceso de pago.
  5. El usuario es redireccionado por POST para mostrar el resultado de su compra.

Sin embargo, además debemos considerar algunos casos de borde, en cualquiera de los cuales el usuario será redireccionado por POST para mostrar el resultado de su compra:

  1. Aborto del pago por parte del usuario.
  2. Timeout en el formulario de Transbank, o del banco.
  3. Error en el formulario de Transbank, o del banco.

Revisaremos el detalle de cada paso de estos flujos a medida que vayamos integrándonos.

1. Configuración en modo integración

La conexión con el API de Transbank puede realizarse en dos modos:

  • Integración: Para pruebas iniciales de comportamiento de nuestra plataforma, realizando pagos ficticios.
  • Producción: Una vez se ha contratado el producto con transbank para nuestro comercio, para realizar cobros reales.

Para indicarle a Transbank en cuál modo estamos conectados, el SDK nos facilita una clase Configuration (Transbank\Webpay\Configuration), que nos permite indicar los datos necesarios para cada petición al servicio. Esta configuración es el único parámetro que recibe el constructor de Webpay (Transbank\Webpay\Webpay), desde el que podemos obtener la instancia de transacción. Esta instancia es la que utilizaremos para realizar los llamados al API.

La forma fácil

Si necesitamos configurar rápido, la clase Configuration tiene varios métodos estáticos que permiten obtener una instancia configurada en modo integración para distintos productos. En este caso, obtenemos una instancia de configuración lista para Webpay Plus Normal con Configuration::forTestingWebpayPlusNormal().

La forma tortuosa

El problema con esto es que, al intentar configurar en modo producción, muchos integradores no entienden muy bien cómo crear una instancia personalizado con los datos necesarios. Por eso acá crearemos a mano una instancia de Configuration preparada en modo integración.

Para el objeto de configuración, necesitamos lo siguiente:

  • Código de comercio (commerce code): Un número de 12 dígitos, que siempre comienza con 5970. Identifica el producto de Transbank que adquirimos para nuestro comercio.
  • Entorno (environment): String que identifica si estamos en modo producción (PRODUCCION) o integración (INTEGRACION).
  • Llave privada (private key): LLave RSA privada, utilizada para firmar toda la información que enviamos a Transbank. Es altamente sensible, no debe compartirse ni publicarse.
  • Certificado público (public cert): Certificado x509 público, utilizado para que Transbank verifique que los mensajes que le lleguen de nuestro comercio tengan nuestra firma (usando la llave privada). Puede compartirse sin problemas, pues sólo sirve para verificar la firma.
  • Certificado de transbank (transbank cert): Certificado x509 público, entregado por transbank para verificar que la información que recibimos esté firmada por ellos. Viene incluido en el SDK para las validación de los responses.

Cuando contratamos el producto con Transbank, nos asignan el código de comercio, mientras que la llave y el certificado tendremos que generarlos nosotros cuando queramos hacer el paso a producción. Pero para integración, ya tenemos toda esta información disponible en un repositorio púbico oficial de transbank. Allí, en el directorio integracion/Webpay Plus - CLP, encontramos los datos necesarios para crear nuestra instancia de Configuration.

Para comenzar, crearemos un nuevo controlador en app/Http/Controllers/WebpayPlusNormalController.php con un método estático privado getTransaction. Utilizaremos la opción de visualización raw que nos entrega Github para leer directamente los archivos, como sigue:

Archivo app/Http/Controllers/WebpayPlusNormalController.php

Observemos que, además de instanciar el objeto de configuración, realizamos los pasos necesarios para finalmente retornar una instancia del objeto de transacción, que es el que utilizaremos para conectarnos al API.

Guardamos los cambios con:

$ git add .
$ git commit -m 'Add Webpay Plus Normal configuration'

2. Configurando las rutas

Para integrarnos correctamente con este producto, tendremos que habilitar tres rutas específicas, relacionadas al flujo de normal funcionamiento.

  1. Ruta de inicialización de pago con Webpay Plus Normal
  2. Ruta de revisión del resultado de pago con Webpay Plus Normal
  3. Ruta de presentación del resultado de la compra

Para ello, agregaremos tres métodos nuevos a nuestro controlador:

Métodos necesarios para flujo de Webpay Plus Normal

Y modificamos routes/web.php para crear las rutas con los verbos HTTP correspondientes, de acuerdo al flujo del producto:

Rutas necesarias para flujo de Webpay Plus Normal

Guardamos los cambios con:

$ git add .
$ git commit -m 'Add Webpay Plus Normal routes'

3. Iniciando el proceso de pago

¡Al fin comenzamos con la parte entretenida! 🎉

Para iniciar el flujo, primero tenemos que llegar a la ruta webpayplus_normal.init, así que modificaremos la vista resources/views/payments/select_payment_method.blade.php, y agregaremos un link para iniciar el proceso de pago con Webpay Plus Normal:

Agregar link para iniciar pago con Webpay Plus Normal

Así, al agregar un nuevo pago, veremos algo como esto en el navegador:

Selección de pago con Webpay Plus Normal

Ahora, necesitamos iniciar nuestro proceso de compra. Para eso, llamaremos al método initTransaction del objeto de transacción. Este método recibe varios parámetros:

  1. Monto (amount): Número entero con un máximo de 10 dígitos. Indica el monto que se le cobrará al usuario.
  2. Orden de compra (buyOrder): String de largo máximo 26, que puede contener carácteres alfanuméricos (mayúsculas y minúsculas) y los signos |_=&%.,~:/?[+!@()>-. Este valor no puede repetirse para un mismo código de comercio.
  3. Identificador de sesión (session id): String de largo máximo 61. Es para uso interno del comercio, y se incluye en todas las respuestas posteriores relacionadas a la misma transacción.
  4. URL de retorno (return url): String de largo máximo 256. Es la ruta a la URL que procesrá el resultado del pago. Tiene comportamiento errático si la URL contiene un querystring.
  5. URL final (final url): String de largo máximo 256. Es la ruta a la URL que mostrará el resultado de la compra. Tiene comportamiento errático si la URL contiene un querystring.

Si todo va bien, el método nos responderá con un objeto que contiene la siguiente información:

  1. Token (token): String de largo 64. Identificador único de la transacción.
  2. URL (url): String de largo máximo 256. Es la ruta a la que tendremos que redireccionar para mostrar el formulario de transbank, enviando el token que recibimos por POST.

Si en cambio hay problemas, obtendremos en cambio un array con la siguiente información:

  1. Error (key ”error"): Identificador del error. Está fijo en el código del SDK, por lo que siempre es el mismo texto.
  2. Detalle (key ”detail"): Descripción del error obtenido en el servicio SOAP. Varía dependiendo del problema, la información valiosa está acá.

Considerando estos parámetros, podemos agregar el siguiente contenido al método init de nuestro controlador:

Inicio de proceso de pago con Webpay Plus Normal

Es posible que les llame la atención el valor que le damos a la orden de compra, pero debemos recordar que este identificador debe ser único para cada código de comercio. Si a estos sumamos que todos utilizamos el mismo código de comercio para realizar pruebas (597020000540), podemos notar que los valores típicos de pruebas (con identificadores 1, 2, etc.) probablemente ya están utilizados. Por eso, nos aseguramos enviando un string con formato que contiene texto, el id del pago en nuestro sistema y un timestamp.

Además, debemos crear la vista resources/views/webpayplus/normal/init.blade.php, que llamamos al final de nuestro método init en el controlador:

Archivo resources/views/webpayplus/normal/init.blade.php

Ahora podemos probar la ejecución. Si todo está bien, al intentar pagar deberíamos ser redireccionados al formulario de pruebas de transbank.

Formulario de transbank

Si hay algún problema, seremos redireccionados de vuelta a la página principal.

Guardando información

Todo bien hasta el momento, pero necesitamos guardar en nuestra base de datos la información que le enviamos a transbank para iniciar la transacción, y guardar lo que nos devuelve. Para ello, crearemos una nueva tabla ejecutando:

$ docker-compose exec transbank php artisan make:migration create_webpayplus_normal_transactions_table --create=webpayplus_normal_transactions

Y modificamos el método up para agregar la relación con nuestra tabla payments, y guardar los campos nullables de token y error:

Agregar columnas a la nueva tabla webpayplus_normal_transactions

Ahora crearemos un modelo en app/WebpayplusNormalTransaction.php que nos permita trabajar con esta tabla de forma amigable:

Archivo app/WebpayplusNormalTransaction.php

Con el modelo listo, modificamos nuestro controlador para guardar la información en nuestro modelo y utilizar sus relaciones y accesores:

Utilizamos el modelo WebpayplusNormalTransaction al iniciar la transacción

Ya estamos casi listos. Sólo nos falta modificar en app/Payment.php las constantes de estado para indicar los nuevos estados posibles de pago:

Agregar nuevos estados de pago

Y finalmente actualizamos el estado del pago cuando sea necesario, y realizamos además algunas validaciones:

Uso de nuevos estados de pago

Si realizamos en nuestra plataforma pagos correcta e incorrectamente (para esto podemos simplemente comentar la línea en que se llama a setPublicCert en el método getTransaction), deberíamos ver algo como esto:

Pagos iniciados correctamente y con error

Como hemos comprobado que la información se está guardando sin problemas y que el flujo funciona, podemos guardar nuestros cambios en git (cuidando de no agregar las modificaciones que hicimos para que falle) con:

$ git add .
$ git commit -m 'Add Webpay Plus Normal init transaction'

4. Obteniendo el resultado de pago

Ya hemos iniciado el proceso de pago en transbank. Para los datos de tarjeta, podemos usar datos de prueba:

  • Tarjeta de crédito: 4051 8856 0044 6623
  • CVV: 123
  • Cualquier fecha de expiración
  • Rut del banco ficticio: 11.111.111-1
  • Contraseña del banco ficticio: 123.

En este link a la documentación pueden encontrar más detalles y considerar otros datos de prueba.

Una vez hayamos completado el flujo de ambos formularios (el de transbank y el del banco), llegaremos de vuelta a la URL que configuramos como el parámetro urlReturn del método initTransaction, que en nuestro caso nos lleva al método return de nuestro controlador.

Al ser redireccionados, recibiremos por POST el parámetro token_ws, que podemos utilizar para llamar al resultado de la transacción mediante el método getTransactionResult. Este método nos retorna la siguiente información:

  1. Orden de compra (buy order): La orden de compra indicada en initTransaction.
  2. Identificador de sesión (session id): El identificador de sesión indicado en initTransaction.
  3. Monto (amount): Entero de largo máximo 10. Monto de la transacción indicado en initTransaction.
  4. Código de comercio (commerce code): String de largo máximo 12. Código comercio de la tienda que utilizamos en el objeto de transacción de Webpay Plus Normal.
  5. Fecha de la transacción (transaction date): String de fecha en formato DATE_W3C (ejemplo 2020-05-30T22:00:00.000-04:00). La fecha de la autorización de la transacción.
  6. Fecha contable ( accounting date): String de fecha en formato MMDD. La fecha contable de la autorización de la transacción.
  7. VCI (VCI): String. Resultado de la autenticación bancaria del usuario. Los valores pueden cambiar constantemente y sin aviso en la documentación, pero actualmente están descritos en esta tabla de la referencia oficial.
  8. Número de tarjeta (card number): String de largo máximo 16. Por defecto, los cuatro últimos dígitos de la tarjeta. Para comercios autorizados, puede recibir los 16 dígitos.
  9. Expiración de tarjeta (card expiration date): String de fecha. Indica la fecha de expiración de la tarjeta. Si el comercio no está autorizado para recibir esta información, se recibirá null.
  10. Código de autorización (authorization code): String de largo máximo 6. Código de autorización de la transacción.
  11. Tipo de pago de la transacción (payment type code): String de largo máximo 3. Para tarjeta de crédito, los posibles están descritos en este apartado de la documentación oficial.
  12. Código de respuesta (response code): Número entero de largo máximo 2. Es el código de respuesta de la autorización. Si el valor es 0, significa que el pago fue aprobado. Cualquier otro valor indica rechazo en el pago por distintos motivos, descritos en esta tabla de la referencia oficial.
  13. Descripción de respuesta (response description): String. Descripción del código de respuesta. No se incluye si el código de respuesta es 0.
  14. Cantidad de cuotas (shares number): Entero de largo máximo 2.
  15. URL de redireccionado (url redirection): String de largo máximo 256. Ruta a la que debemos redireccionar para que transbank muestre el resultado de pago.

Al igual que el método initTransaction, si hay problemas obtendremos un array con la siguiente información:

  1. Error (key ”error"): Identificador del error. Está fijo en el código del SDK, por lo que siempre es el mismo texto.
  2. Detalle (key ”detail"): Descripción del error obtenido en el servicio SOAP. Varía dependiendo del problema, la información valiosa está acá.

Modificaremos entonces el método return de nuestro controlador para obtener información sobre el resultado de la transacción:

Obteniendo resultado de transacción

Y creamos la vista resources/views/webpayplus/normal/return.blade.php, que llamamos al final de nuestro método, para redireccionar al usuario a la vista de estado del pago en transbank:

Archivo resources/views/webpayplus/normal/return.blade.php

Si probamos una nueva ejecución, luego del flujo de pago exitoso deberíamos ver algo como esto:

Voucher de transbank mostrando el resultado del pago

Reconociendo la transacción

Un punto importante a considerar, es que una vez se ha autorizado la transacción (independiente de si fue aprobado o rechazado el pago), disponemos de 30 segundos para reconocer la transacción. Si no realizamos el reconocimiento en ese tiempo, la transacción (aprobada o rechazada) será reversada, lo que implica que:

  • Si el pago fue aceptado, el dinero será devuelto al usuario. Además, se marcará en los registros de Transbank como transacción reversada.
  • Si el pago no fue aceptado, sólo se marcará en los registros de Transbank como transacción reversada. Esto puede generar confusiones más tarde, cuando deseemos saber si una transacción no terminó bien por algún motivo específico, o fue reversada por no reconocimiento.

Por suerte, la gente de Transbank nos facilitó las cosas, ya que el método getTransactionResult llama por debajo a dos endpoints del API de Transbank:

  1. Endpoint getTransactionResult. Recupera el resultado de la transacción, y es lo que finalmente retorna el método.
  2. Endpoint acknowledgeTransaction. Reconoce la transacción.

Sin embargo, debemos seguir considerando los tiempos de respuesta de nuestra plataforma. Si por cualquier motivo, nuestro sistema demora más de 30 segundos en ejecutar el método getTransactionResult desde que el usuario fue autorizado (por ejemplo, porque el servidor está sobrepasado por l cantidad de peticiones que está recibiendo), entonces todas esas transacciones serán reversadas. Evitémonos dolores de cabeza, y preparémonos para estas situaciones excepcionales (¿alguien dijo Black Friday?).

Guardando información

Hasta aquí todo funciona correctamente. Es hora entonces de guardar toda la información que nos devuelve getTransactionResult en nuestra base de datos. Crearemos una nueva migración con:

$ docker-compose exec transbank php artisan make:migration create_webpayplus_normal_responses_table --create=webpayplus_normal_responses

Y la modificamos, agregando una columna nullable por cada campo con información que nos devuelve getTransactionResult, más un campo de error:

Agregar columnas a la nueva tabla webpayplus_normal_responses

Creamos el modelo app/WebpayplusNormalResponse.php con la información necesaria para lidiar con la información en nuestra base de datos:

Archivo app/WebpayplusNormalResponse.php

Y modificamos también el modelo app/WebpayplusNormalTransaction.php para agregar esta nueva relación:

Agregar relación

Con los modelos preparados, podemos utilizarlo en el método return de nuestro controlador para guardar la información recuperada:

Utilizar modelo WebpayplusNormalResponse

Estamos casi casi. Agregamos en app/Payment.php los nuevos posibles estados de pago:

Agregar nuevos estados de pago

Y los utilizamos en el método return del controlador, actualizando en cada caso (y validando donde sea posible):

Uso de nuevos estados de pago

Ahora, si realizamos transacciones correctamente, con errores del api (podemos forzarlos igual que para método init) y errores de validación (podemos forzarlos modificando el if (!$db_response->is_valid)), veremos algo como esto:

Pagos finalizados correctamente y con error

Finalmente, guardamos los cambios realizados en git (cuidando de deshacer las modificaciones que hicimos para forzar errores) con:

$ git add .
$ git commit -m 'Add Webpay Plus Normal finish transaction'

5. Mostrando el resultado de la compra

Cuando Transbank presenta el resultado de pago, muestra al usuario un botón para enviarlo al detalle de la compra, que es la pantalla de resultado de la compra de nuestro sitio. Esta vista debe mostrarse en la URL que configuramos como URL final en el método initTransaction. Si el usuario no presiona el botón, de todas formas será redireccionado en 10 segundos.

En nuestro caso, esta URL nos lleva al método final de nuestro controlador. Allí recibiremos por POST el parámetro token_ws, que podemos utilizar para buscar la transacción en la base de datos y presentarla al usuario.

Mostrar resultado de compra

Y creamos la vista resources/views/webpayplus/normal/final.blade.php con el contenido:

Archivo resources/views/webpayplus/normal/final.blade.php

Ahora, al finalizar un proceso de compra exitoso, veremos algo como:

Resultado de la compra

Con estos cambios hemos completado el flujo feliz de Webpay Plus Normal. Podemos guardar estos últimos cambios con:

$ git add .
$ git commit -m 'Add Webpay Plus Normal payment finished visualization'

6. Manejando los casos de borde

Además del flujo feliz, debemos manejar los casos de borde que comentamos al principio del artículo. En cada uno de estos casos, Transbank nos redireccionará directamente a la URL final, y por tanto debemos manejar nuestros casos en el método final de nuestro controlador.

Lo primero que haremos, será aislar nuestro caso feliz primero, realizando algunas validaciones y reorganizando el código para que nos sea más cómodo introducir los nuevos casos más adelante:

Refactorizando resultado de compra

Como notarán, al final del método un switch para diferenciar los casos de borde. Esto es porque en la ejecución del caso feliz sólo llegaremos a este punto con estado STATUS_WP_NORMAL_FINISH_SUCCESS, pero los casos de borde, por haber sido ejecutado sólo el método de inicio de la transacción, el estado siempre será STATUS_WP_NORMAL_INIT_SUCCESS. Además, este case no tiene la sentencia break, para que se ejecute además la sentencia del caso default y redireccionemos al usuario a la página principal.

Y ya está, sólo queda guardar estos cambios para que los siguientes casos queden cada uno en su propio commit:

$ git commit -am 'Add Webpay Plus Normal final validations'

Abortando el proceso de pago

Para abortar el proceso de pago, el usuario sólo debe hacer click al link que aparece en el formulario de transbank:

Link para abortar el pago

Al clickearlo, llegaremos directo la URL final, pero esta vez no recibiremos la variable token_ws. En cambio, recibiremos 3 datos por POST:

  • TBK_TOKEN: Token de la transacción.
  • TBK_ORDEN_COMPRA: Orden de compra de la transacción.
  • TBK_ID_SESION: Identificador de sesión de la transacción.

Agregaremos primero un nuevo estado final que indique que estamos acá por aborto en el proceso de compra:

Agregar nuevos estados de pago

Y modificamos nuestro código en el controlador para identificar este caso y actualizar el estado:

Considerar aborto de pago

Ahora, si probamos una compra abortando el pago, veremos algo como esto:

Resultado de pago abortado

Guardamos los cambios para este caso de borde con:

$ git commit -am 'Add Webpay Plus Normal abort edge case'

Timeout en el proceso de pago

Cuando pasamos 10 minutos en el formulario de transbank, somos automáticamente redirecionados a la URL final. Al igual que al abortar, no recibiremos la variable token_ws. Esta vez recibimos 2 datos por POST:

  • TBK_ORDEN_COMPRA: Orden de compra de la transacción.
  • TBK_ID_SESION: Identificador de sesión de la transacción.

Como podemos ver, en esta oportunidad no tenemos el token para recuperar la transacción. Por suerte, cuando llamamos a initTransaction, pasamos el id de nuestro modelo Payment como identificados de sesión.

Comenzaremos agregando un nuevo estado de pago para este caso:

Agregar nuevos estados de pago

Y editamos nuevamente el método final nuestro controlador para considerar este nuevo caso de borde:

Considerar timeout en el pago

Para probar que esto funcione, debemos iniciar el proceso de pago y esperar 10 minutos en la ventana del formulario de Transbank. Al finalizar, seremos redireccionados de vuelta a la pantalla principal, donde veremos algo como esto:

Resultado de pago con timeout

Guardamos los cambios realizados para lidiar con el timeout con:

$ git commit -am 'Add Webpay Plus Normal timeout edge case'

Error en el proceso de pago

En este último caso, revisaremos lo que ocurre cuando se presenta un error en el formulario de transbank (o del banco, o en la comunicación entre ambos). La única dificultad es que no puede ser replicado en local, pues es una vista que se muestra sólo en producción.

Si desean verlo ustedes mismos, simplemente intenten realizar un pago online en alguna plataforma que tenga algún pago de Webpay Plus integrado, y en el formulario de Transbank presionen el botón de recarga de la página. Si lo hacen, verán algo como esto:

Vista de error en formulario de producción de transbank

Si se presiona el link con el texto intente nuevamente que aparece allí, también llegaremos a nuestra URL final, sólo que esta vez recibiremos 4 parámetros por POST:

  • token_ws: Token de la transacción.
  • TBK_TOKEN: Token de la transacción. Si, el dato está repetido, shame on Transbank.
  • TBK_ORDEN_COMPRA: Orden de compra de la transacción.
  • TBK_ID_SESION: Identificador de sesión de la transacción.

Con nuestro código actual, ya podemos lidiar con este caso, sólo necesitamos clasificarlo para mostrar el mensaje al usuario final. Agregamos el estado a nuestro modelo app/Payment.php:

Agregar nuevos estados de pago

Y agregamos nuestro nuevo caso en el método final de nuestro controlador:

Considerar error en el pago

Como no es posible replicar este escenario en ambiente de pruebas, tendrán que confiar en mí 😁 y creer que en producción les llegan estos 4 valores por POST. Sólo queda finalizar guardando nuestros cambios en git:

$ git commit -am 'Add Webpay Plus Normal tbk form error edge case'

¡Todo listo!

Ya están listos para presumir de su flamante integración con Webpay Plus Normal, que considera todos los casos de borde actualmente conocidos. Pronto subiré nuevos artículos con el resto de productos de transbank, así que sigan atentos a este blog.

Si tienen dudas sobre algún punto, o el código no les queda claro, comenten para ver cómo darles una mano.

¡Éxito en sus integraciones!

--

--

Ricardo Cisterna

Ingeniero de software, y líder de producto en entrenamiento en #Continuum. A veces me gusta tomar mi mochila y perderme acampando en el sur de Chile.