¿Cómo incorporar Bizum en tu eCommerce en Sylius?

Javier Carrillo Desarrollo Tecnología

El modelo de los pagos en eCommerce ha evolucionado de manera exponencial, especialmente desde la entrada en juego de Bizum como método de pago para estas tiendas online. Un servicio que ha duplicado el número de usuarios en tan solo el último año y que simplifica notoriamente el final del proceso de compra y que más de 50 marcas han sabido aprovechar esta oportunidad.

Cómo integrar Bizum y Redsys con Sylius

En este post contamos paso a paso cómo integramos Bizum en Redsys con Sylius para Centros Único, ante la demanda de sus clientes de tener esta opción como método de pago, si pueden pagar por móvil en el centro físico, ¿por qué no también utilizarlo en compras online?

Centros Único, empresa líder en depilación láser y medicina estética con más de 200 centros y una fuerte expansión en Reino Unido, Portugal, Italia, Alemania, Suiza y México, ha logrado su éxito en tres pilares: la democratización de la oferta ofreciendo precios competitivos y posibilidad de financiación lo que ha permitido que cualquier tipo de cliente tenga la oportunidad de acceder a estos servicios que antes eran prácticamente inaccesibles; la localización estratégica y la flexibilidad horaria poniendo así a disposición de todos los públicos tratamientos de calidad; y la innovación tecnológica.

Este último punto ha sabido aplicarlo tanto para el desarrollo de sus tratamientos como para la manera de ofrecer su servicio, apostando por Sylius, una de las tecnologías más avanzadas en el mercado, como solución para su nueva tienda online, como hicieron en su momento empresas como Natura y Relais & Chateaux.

Sylius es una solución e-commerce basada en php, construido sobre Symfony, el framework diseñado para desarrollar aplicaciones web utilizando su misma filosofía. Una de las ventajas de usar Sylius por encima de otras soluciones más conocidas es poder partir de un código moderno, de arquitectura limpia, el cual es robusto y fácil de acoplar a soluciones específicas. Esto se traduce como una plataforma flexible y de fácil mantenimiento en el tiempo para nuestros clientes.

Antes de empezar con la parte de código en Sylius, repasemos los puntos importantes de la documentación de Redsys:

  • Redys es una reconocida plataforma de pago virtual a nivel nacional que admite diferentes tipos de pagos asociados a los eCommerce, entre ellos, Bizum.
  • En su documentación oficial encontramos un apartado específico sobre cómo implementar esta forma de pago en nuestro eCommerce.

Vamos a comentar algunos puntos de la documentación para entender más claramente la parte del código:

Funcionamiento redirección con Redsys

El proceso de una operación con el TPV virtual de Redsys es el siguiente:

  1. El usuario selecciona los productos que desea comprar en nuestro eCommerce.
  2. Redirigimos la sesión del navegador del usuario a la URL de Redsys. En esta URL el usuario introduce los datos de Bizum.
  3. El TPV virtual nos notifica por una llamada post del resultado de la operación.
  4. Redsys devuelve la sesión del navegador del usuario a nuestra web.

El formulario de envío de petición tiene que tener los siguientes campos:

  • Ds_MerchantParameters: Datos de la petición de pago.
  • Ds_SignatureVersion: Versión del algoritmo de firma.
  • Ds_Signature: Firma de los datos de la petición de pago.

A continuación, detallamos cómo se obtiene el valor de cada parámetro:

Ds_MerchantParameters

Se debe montar una JSON con todos los datos de la petición.

{
    
"DS_MERCHANT_AMOUNT": "145",
  
    "DS_MERCHANT_CURRENCY": "978",
  
    "DS_MERCHANT_MERCHANTCODE": "999008881",
  
    "DS_MERCHANT_MERCHANTURL": "http://www.prueba.com/urlNotificacion.php",
  
    "DS_MERCHANT_ORDER": "1446068581",
  
    "DS_MERCHANT_TERMINAL": "1",
  
    "DS_MERCHANT_TRANSACTIONTYPE": "0",
  
    "DS_MERCHANT_URLKO": "http://www.prueba.com/urlKO.php",
  
    "DS_MERCHANT_URLOK": "http://www.prueba.com/urlOK.php",
  
    "DS_MERCHANT_PAYMENTHODS": "z"

}

El parámetro DS_MERCHANT_PAYMENTHODS con valor “Z” es necesario para que Redsys interprete la transacción como pago de Bizum.

Tras montar la cadena, es necesario codificarla en BASE64, y sin espacios en blanco. La cadena resultante de la codificación en BASE64 será el valor del parámetro «Ds_MerchantParameters»:

ewoJIkRTX01FUkNIQU5UX0FNT1VOVCI6IjE0NSIsCgkiRFNfTUVSQ0hBTlRfT1JERVIiOiIxNDQ2MDY4NTgxIiwKCSJEU19NRVJDSEFOVF9NRVJDSEFOVENPREUiOiI5OTkwMDg4ODEiLAoJIkRTX01FUkNIQU5UX0NVUlJFTkNZIjoiOTc4IiwKCSJEU19NRVJDSEFOVF9UUkFOU0FDVElPTlRZUEUiOiIwIiwKCSJEU19NRVJDSEFOVF9URVJNSU5BTCI6IjEiLAoJIkRTX01FUkNIQU5UX01FUkNIQU5UVVJMIjoiaHR0cDpcL1wvd3d3LnBydWViYS5jb21cL3VybE5vdGlmaWNhY2lvbi5waHAiLAoJIkRTX01FUkNIQU5UX1VSTE9LIjoiaHR0cDpcL1wvd3d3LnBydWViYS5jb21cL3VybE9LLnBocCIsCgkiRFNfTUVSQ0hBTlRfVVJMS08iOiJodHRwOlwvXC93d3cucHJ1ZWJhLmNvbVwvdXJsS08ucGhwIgp9IA==

Ds_SignatureVersion

En la petición se debe identificar la versión concreta de algoritmo que se está utilizando para la firma. Actualmente se utiliza el valor HMAC_SHA256_V1 para identificar la versión de todas las peticiones, por lo que este será el valor de este parámetro.

Ds_Signature

La firma de la operación («Ds_Signature») se calculará utilizando el parámetro «Ds_MerchantParameters» (ya codificado en BASE64), y la clave específica del terminal (clave del comercio). Una vez tengamos estos dos parámetros haremos los siguientes pasos:

  1. La clave del comercio debe codificarse en BASE64.
  2. Diversificar la clave de firma realizando un cifrado 3DES entre esta clave codificada y el valor del número de pedido de la operación (Ds_Merchant_Order).
  3. Se realiza el cálculo del HMAC-256 con el parámetro «Ds_MerchantParameters» y la clave de firma diversificada.
  4. El valor del HMAC SHA256 debe codificarse en BASE64, siendo su resultado el valor de la firma.

Payum en Sylius

Payum es una librería de código abierto, que a modo de interfaz, genera una abstracción entre nuestros dominio y la infraestructura del proyecto. Es el componente que viene incluido en Sylius para los métodos de pago.

Integración De Bizum en Sylius para Centros Único

Una vez leída y entendida la documentación de Redsys, vamos a por el código:

Creamos nuestra puerta de entrada

El primer paso será crear una clase factoría que implemente la clase GatewayFactoryInterface de Payum.

<?php

namespace App\Gateways\Bizum;
use Payum\Core\GatewayFactoryInterface;

class BizumPaymentGatewayFactory implements GatewayFactoryInterface
{
    public function __construct( array $defaultConfig = [], GatewayFactoryInterface $coreGatewayFactory = null )
    {
        $this->coreGatewayFactory = $coreGatewayFactory ?: new CoreGatewayFactory();
        $this->defaultConfig = $defaultConfig;
    }
    public function create(array $config = [])
    {
        return $this->coreGatewayFactory->create($this->createConfig($config));
    }
    protected function populateConfig(ArrayObject $config): void
    {
        $config->defaults([
            'payum.factory_name' => 'bizum',
            'payum.factory_title' => 'Bizum',
        ]);
    }
}

Creamos nuestra formulario de configuración

Ahora, crearemos el formulario de configuración, donde añadiremos los parámetros de autorización, la posibilidad de modo sandbox y cualquier parámetro extra que necesitemos.

<?php

namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;

final class BizumGatewayConfigurationType extends AbstractType
{
    public function buildForm( FormBuilderInterface $builder, array $options): void
    {
        $builder
        ->add('merchant_code', TextType::class)
        ->add('secret_key', TextType::class)
        ->add('sandbox', CheckboxType::class);
    }
}

Configuración de parámetros para Bizum con Redsys

Para poder añadir los campos necesarios para nuestra configuración de Bizum, tenemos que crear una clase que se encargue de esta tarea:

<?php

namespace App\Gateways\Bizum;
class Api {
    const TRANSACTIONTYPE_AUTHORIZATION = 0;
    const DS_MERCHANT_TERMINAL = 1;
    const PAYMETHOD_BIZUM = 'z';
    const SIGNATURE_VERSION = 'HMAC_SHA256_V1';
    const TEST_URL = 'https://sis-t.redsys.es:25443/sis/realizarPago';
    const PRODUCTION_URL = 'https://sis.redsys.es/sis/realizarPago';
    public function __construct(array $options) 
    {
        $this->options = options;
    }
    public function getRedsysUrl() 
    {
        return $this->options['sandbox'] ?
            self::TEST_URL :
            self::PRODUCTION_URL;
    }
    public function getMerchantCode() 
    {
        return $this->options['merchant_code'];
    }
    function createMerchantParameters(array $params)
    {
        $json = json_encode($params);
        return base64_encode($json);
    }
    public function createSignature(array $params)
    {
        $base64DecodedKey = base64_decode($this->options['secret_key']);
        $key = $this->encrypt3DES($params['Ds_Merchant_Order'],
        $base64DecodedKey);
        $res = $this->mac256(
            $this->createMerchantParameters($params),
        $key);
        return base64_encode($res);
    }
    private function encrypt3DES($merchantOrder, $key) {
       $ciphertext = substr(
       openssl_encrypt(
           $merchantOrder . str_repeat("\0", $l - strlen($merchantOrder)),
           'des-ede3-cbc', $key, OPENSSL_RAW_DATA, "\0\0\0\0\0\0\0\0"),
           0,
           ceil(strlen($merchantOrder) / 8) * 8);
       return $ciphertext;
    }
}

La función createSignature genera, con las especificaciones vistas antes en la documentación de Redsys, la firma encriptada para el parámetro ‘Ds_Signature’.

La function createMerchantParameters genera el valor para ‘Ds_MerchantParameters’: un JSON codificado en base64 como se especifica en la documentación de Redsys.

Creamos nuestra acción para capturar el pago

Creamos la clase que capturará el pago y creará la url de redirección con todos los parámetros necesarios para la comunicación de la respuesta de Redsys.

<?php
namespace App\Gateways\Redsys\Action;
use App\Gateways\Bizum\Api;
use Payum\Core\Action\ActionInterface;
use Payum\Core\ApiAwareInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Reply\HttpPostRedirect;
use Payum\Core\Security\GenericTokenFactoryAwareInterface;
use Payum\Core\Security\GenericTokenFactoryInterface;
class CaptureAction implements ActionInterface, ApiAwareInterface, GenericTokenFactoryAwareInterface {
    protected $api;
    protected $tokenFactory;
    public function execute($request) {
        RequestNotSupportedException::assertSupports($this, $request);
        $postData = ArrayObject::ensureArrayObject($request->getModel());
        if (empty($postData['Ds_Merchant_MerchantURL'])) {
            $notifyToken = $this->tokenFactory->createNotifyToken(
                    $request->getToken()->getGatewayName(),
                    $request->getToken()->getDetails()
            );
            $postData['Ds_Merchant_MerchantURL'] = $notifyToken->getTargetUrl();
        }
        $postData['Ds_Merchant_UrlOK'] = $this->addQueryParameter(
                $request->getToken()->getAfterUrl(),
                'accept');
        $postData['Ds_Merchant_UrlKO'] = $this->addQueryParameter(
                $request->getToken()->getAfterUrl(),
                'cancel');
        $postData->validatedKeysSet([
            'Ds_Merchant_Amount',
            'Ds_Merchant_Order',
            'Ds_Merchant_Currency',
            'Ds_Merchant_TransactionType',
            'Ds_Merchant_MerchantURL',
        ]);
        $postData['Ds_Merchant_Order'] = $request->getFirstModel()->getId();
        $details['Ds_Merchant_MerchantCode'] = $this->api->getMerchantCode();
        $details['Ds_Merchant_Terminal'] = Api::DS_MERCHANT_TERMINAL;
        $details['Ds_SignatureVersion'] = Api::SIGNATURE_VERSION;
        $details['Ds_MerchantParameters'] = $this->api->
                createMerchantParameters($postData->toUnsafeArray());
        $details['Ds_Signature'] = $this->api->
createSignature($postData->toUnsafeArray());
        throw new HttpPostRedirect($this->api->getRedsysUrl(), $details);
    }
}

Acción para saber el estado de la compra

Creamos la clase necesaria para que Redsys nos notifique del estado del pago. StatusAction actualizará el estado de pago según los detalles proporcionados por CaptureAction. Según el valor enviado por redsys el estado el pago se ajustará de la siguiente manera:

  • HTTP 400 (solicitud incorrecta): el pago ha fallado.
  • HTTP 200 (OK): pago realizado correctamente.
<?php

namespace App\Gateways\Redsys\Action;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\GatewayAwareInterface;
use Payum\Core\GatewayAwareTrait;
use Payum\Core\Request\GetHttpRequest;
use Payum\Core\Security\GenericTokenFactoryAwareInterface;
use Payum\Core\Security\GenericTokenFactoryAwareTrait;

class StatusAction implements ActionInterface, GatewayAwareInterface, GenericTokenFactoryAwareInterface
{
    use GatewayAwareTrait;
    use GenericTokenFactoryAwareTrait;
    public function execute($request)
    {
        RequestNotSupportedException::assertSupports($this, $request);
        $model = ArrayObject::ensureArrayObject($request->getModel());
        $this->gateway->execute($httpRequest = new GetHttpRequest());
        if(isset($httpRequest->query['cancel'])) {
            $request->markCanceled();
            return;
        }
        if(isset($httpRequest->query['accept'])) {
            $request->markCaptured();
            return;
        }
        $request->markUnknown();
    }
}

Conversor para los datos del pago

Ahora, vamos a añadir una acción más: el conversor del pago entre Payum y Redsys.

<?php

namespace App\Gateways\Redsys\Action;

use App\Gateways\Bizum\Api;
use Payum\Core\Action\ActionInterface;
use Payum\Core\ApiAwareInterface;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Exception\UnsupportedApiException;
use Payum\Core\Model\PaymentInterface;
use Payum\Core\Request\Convert;
use Payum\Core\Bridge\Spl\ArrayObject;

class ConvertPaymentAction implements ActionInterface, ApiAwareInterface {

    protected $api;

    public function execute($request) {
        RequestNotSupportedException::assertSupports($this, $request);
        $payment = $request->getSource();
        $details = ArrayObject::ensureArrayObject($payment->getDetails());
        $details->defaults(array(
            'Ds_Merchant_Amount' => $payment->getTotalAmount(),
            'Ds_Merchant_Order' => $payment->getNumber(),
            'Ds_Merchant_MerchantCode' => $this->api->getMerchantCode(),
            'Ds_Merchant_Currency' => $payment->getCurrencyCode(),
            'Ds_Merchant_TransactionType' => Api::TRANSACTIONTYPE_AUTHORIZATION,
            'Ds_Merchant_Terminal' => Api::DS_MERCHANT_TERMINAL,
            'Ds_Merchant_PayMethods' => Api::PAYMETHOD_BIZUM
        ));
        $request->setResult((array) $details);
    }

    public function setApi($api) {
        if (false == $api instanceof Api) {
            throw new UnsupportedApiException('Not supported.');
        }
        $this->api = $api;
    }
}

Actualizamos nuestra clase puerta de entrada

Por último, añadimos nuestras nuevas clases a nuestro BizumGatewayFactory.

<?php

namespace App\Gateways\Bizum;

use App\Gateways\Bizum\Action\CaptureAction;
use App\Gateways\Bizum\Action\ConvertPaymentAction;
use App\Gateways\Bizum\Action\NotifyAction;
use App\Gateways\Bizum\Action\StatusAction;
use App\Gateways\Bizum\Api;
use Payum\Core\GatewayFactoryInterface;

class BizumPaymentGatewayFactory implements GatewayFactoryInterface {

    public function __construct(
            array $defaultConfig = [],
            GatewayFactoryInterface $coreGatewayFactory = null) {
        $this->coreGatewayFactory = $coreGatewayFactory ?: new CoreGatewayFactory();
        $this->defaultConfig = $defaultConfig;
    }

    public function create(array $config = array()) {
        return $this->coreGatewayFactory->create($this->createConfig($config));
    }

    protected function populateConfig(ArrayObject $config): void {
        $config->defaults([
            'payum.factory_name' => 'bizum',
            'payum.factory_title' => 'Bizum',
            'payum.action.capture' => new CaptureAction(),
            'payum.action.notify' => new NotifyAction(),
            'payum.action.convert_payment' => new ConvertPaymentAction(),
            'payum.action.status' => new StatusAction(),
        ]);

        $config['payum.api'] = function (ArrayObject $config) {
            $redsysConfig = [
                'merchant_code' => $config['merchant_code'],
                'secret_key' => $config['secret_key'],
                'sandbox' => $config['sandbox'],
            ];
            return new Api($redsysConfig);
        };
    }
}

Ya solo faltaría por configurar nuestras clases creadas en nuestros contenedor de servicios.

De esta manera tenemos configurado nuestro nuevo método de pago con Bizum totalmente desacoplado de otros métodos de pagos. A esto sumamos la posibilidad añadida de poder incorporar diferentes formas de pago con Redsys, ya que solo tendríamos que crear una nueva puerta de enlace y una clase API específica.

¿Cuál ha sido el resultado? Una rápida respuesta al mercado y mejor experiencia para nuestros usuarios que se está convirtiendo en las primeras ventas online por Bizum.