Como crear un nuevo Método de Pago para Liferay Commerce - (Parte 2)

NOTICE: This blog  post has Spanish and English version.

 ... Terminada la parte 1, podemos continuar con la integración del módulo con la pasarela de pago...

Paso 3, Integrando el método de pago con el gateway Redsys por Redirección

1.  En el paquete “com.liferay.commerce.payment.method.redsys” crea una clase llamada “RedsysCommercePaymentMethodRequest” que extenda “CommercePaymentRequest”. Dentro de la clase define un nuevo constructor como abajo (aquí añadimos en ”HttpServletRequest” ya que vamos a necesitar tener acceso a este servlet a futuro):

   public RedsysCommercePaymentMethodRequest(BigDecimal amount, String cancelUrl, long commerceOrderId, Locale locale, HttpServletRequest httpServletRequest, String returnUrl, String transactionId) {
        super(amount, cancelUrl, commerceOrderId, locale, returnUrl, transactionId);
        _httpServletRequest = httpServletRequest;
    }

    public HttpServletRequest getHttpServletRequest(){
        return _httpServletRequest;
    }

    private final HttpServletRequest _httpServletRequest;

2.  En el paquete “com.liferay.commerce.payment.method.redsys” crea una clase llamada “RedsysCommercePaymentRequestProvider” que implemente “CommercePaymentRequestProvider” e implemente el método “getCommercePaymentRequest”. 

3.  Anota esta clase con  @Component y establece 3 propiedades. Debería quedar así:

@Component(   immediate = true,
property = "commerce.payment.engine.method.key=" + RedsysCommercePaymentMethod.KEY,
service = CommercePaymentRequestProvider.class
)
Importante: fijáte que en la propiedad “commerce.payment.engine.method.key”, informaremos la “RedsysCommercePaymentMethod.KEY”, como payment engine, así es como vamos integrando todo. 

4.  Ahora, al final de la clase inyecta el servicio de “CommerceOrderLocalService” a través de la anotación @Reference.

5.  Dentro del método “getCommercePaymentRequest”, recuperamos el “CommerceOrder” y retornamos una instancia de  “RedsysCommercePaymentMethodRequest”. Al final, tu clase debería quedarse así:

@Component(   immediate = true,
      property = "commerce.payment.engine.method.key=" + RedsysCommercePaymentMethod.KEY,
      service = CommercePaymentRequestProvider.class
)
public class RedsysCommercePaymentRequestProvider implements CommercePaymentRequestProvider {

  @Override
  public CommercePaymentRequest getCommercePaymentRequest(String cancelUrl, long commerceOrderId, HttpServletRequest httpServletRequest, Locale locale, String returnUrl, String transactionId) throws PortalException {
      CommerceOrder commerceOrder = _commerceOrderLocalService.getCommerceOrder(commerceOrderId);
      return new RedsysCommercePaymentMethodRequest(
              commerceOrder.getTotal(), cancelUrl, commerceOrderId, locale, httpServletRequest, returnUrl, transactionId);
  }

  @Reference
  private CommerceOrderLocalService _commerceOrderLocalService;

6.  En el paquete “com.liferay.commerce.payment.method.redsys.connector”, crea una nueva clase llamada “RedsysConfigClient” (la utilizaremos como un bean de los datos de conexión), copia y pega el siguiente código dentro de la clase: 

public RedsysConfigClient(String dsSignatureVersion, String signatureSecret,
RedsysEnvironment environment, String merchantCode)
throws Exception {

if (environment == null) {
throw new Exception("Invalid Environment specified");
}

if (Validator.isBlank(dsSignatureVersion)) {
throw new Exception("Invalid DS Signature Version specified");
}

if (Validator.isBlank(signatureSecret)) {
throw new Exception("Invalid Signature Secret specified");
}

if (Validator.isBlank(merchantCode)) {
throw new Exception("Invalid Merchant CODE specified");
}

_signatureSecret = signatureSecret;
_dsSignatureVersion = dsSignatureVersion;
_environment = environment;
_merchantCode = merchantCode;

}

public URI getEnvironmentUrl() {
return URI.create(_environment.getUrl());
}

public String getSignatureSecret() {
return _signatureSecret;
}

public String getDsSignatureVersion() {
return _dsSignatureVersion;
}

public String getMerchantCode() {
return _merchantCode;
}

private final RedsysEnvironment _environment;
private final String _signatureSecret;
private final String _dsSignatureVersion;
private final String _merchantCode;

Entonces, como nuestra conexión con el gateway de pago será por redirección, debemos crear nuestro servlet, entonces :

7.  Crea un nuevo paquete:

  •       “ com.liferay.commerce.payment.method.redsys.servlet”

8.  Crea una clase llamada ”RedsysCommercePaymentMethodServlet” que extienda “HttpServlet“, y la anote como un @Component, configura el contexto del Servlet e implementa el método “doGet”. Deberá quedar así:

@Component(immediate = true,
          property = {
            "osgi.http.whiteboard.context.path=/" + RedsysCommercePaymentMethodConstants.SERVLET_PATH,
            "osgi.http.whiteboard.servlet.name=com.liferay.commerce.payment.method.redsys.servlet.RedsysCommercePaymentMethodServlet",
            "osgi.http.whiteboard.servlet.pattern=/" + RedsysCommercePaymentMethodConstants.SERVLET_PATH
                    + "/*"
          },
          service = Servlet.class)
public class RedsysCommercePaymentMethodServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }
}

9.  Crea otro nuevo paquete:

  • “ com.liferay.commerce.payment.method.redsys.servlet.filter”

10.  Dentro de este paquete, crea una clase llamada “RedsysCommercePaymentMethodVerifierFilter” que extenda el “AuthVerifierFilter“, la anote como un @Component y la configure para poder acceder al Servlet desde el navegador con un usuario autenticado a través de su sesión. Deberá quedar así:

@Component(
        immediate = true,
        property = {
                "filter.init.auth.verifier.PortalSessionAuthVerifier.urls.includes=/" + RedsysCommercePaymentMethodConstants.SERVLET_PATH + "/*",
                "osgi.http.whiteboard.filter.name=com.liferay.commerce.payment.method.redsys.servlet.filter.RedsysCommercePaymentMethodVerifierFilter",
                "osgi.http.whiteboard.servlet.pattern=/" + RedsysCommercePaymentMethodConstants.SERVLET_PATH + "/*"
        },
        service = Filter.class
)
public class RedsysCommercePaymentMethodVerifierFilter extends AuthVerifierFilter {
}

11.  Ahora debemos descargar las librerías de Redsys, las puedes descargar en el portal de “Redsys” en la opción de GENERACIÓN HMAC_SHA256” o en mi repo Git (sé que es una mala práctica las subidas de .jar al Git, pero aquí por cuestión de facilidades en el acceso lo he puesto en el proyecto, luego tú lo puedes poner en un repositorio de dependencias de tu preferencia y añadirlo a tu proyecto de una manera más elegante). Entonces seguimos: 

12.  Crea una carpeta “lib” en la raíz de tu módulo, copia y pega los ficheros “.jar” dentro de ella.

 

13.  Abre el fichero “build.gradle” y añade dichos ficheros como dependencia, más la api del “currency”:

compileInclude fileTree(dir: 'libs', include: ['*.jar'])
compileOnly group: 'com.liferay.commerce', name: 'com.liferay.commerce.currency.api', version: '7.0.2'

14.  Ahora volvemos a la primera clase que hemos creado en el inicio de este proyecto, la “RedsysCommercePaymentMethod”. Dentro de esta clase, vamos a terminar de implementar nuestros métodos existentes. Empezando por el “getPaymenteType”, retornaremos el tipo de pago que estamos implementando, como ya hemos hablado, implementaremos la integración con Redsys por redirección, este método deberá quedar así:

@Override
public int getPaymentType() {
  return CommercePaymentConstants.COMMERCE_PAYMENT_METHOD_TYPE_ONLINE_REDIRECT;
}

15.  En el método “getServletPath”, retornaremos lo que hemos definido anteriormente.

@Override
public String getServletPath() {
  return RedsysCommercePaymentMethodConstants.SERVLET_PATH;
}

16.  Cambia el “return” de los 3 métodos “boolean” restantes a “true” para habilitar las 3 opciones que utilizaremos en seguida.

@Override
public boolean isCancelEnabled() {
  return true;
}

@Override
public boolean isCompleteEnabled() {
  return true;
}

@Override
public boolean isProcessPaymentEnabled() {
  return true;
}

17.  Al final de la clase, inyecta los siguientes servicios con la anotación @Reference.

@Reference
private CommerceOrderService _commerceOrderService;

@Reference
private ConfigurationProvider _configurationProvider;

@Reference
private Http _http;

@Reference
private Portal _portal;

18.  Crea los 2 métodos de abajo dentro de la clase, uno para la recuperación de la configuración del método de pago por canal que hemos guardado y el otro para montar nuestro enlace del servlet para la llamada.

private RedsysPaymentMethodCardGroupServiceConfiguration _getConfiguration(long groupId)
      throws ConfigurationException {

  return _configurationProvider.getConfiguration(
          RedsysPaymentMethodCardGroupServiceConfiguration.class,
          new GroupServiceSettingsLocator(
                  groupId, RedsysCommercePaymentMethodConstants.SERVICE_NAME));
}

private String _getServletUrl(
      RedsysCommercePaymentMethodRequest redsysCommercePaymentMethodRequest) {

  return StringBundler.concat(
          _portal.getPortalURL(
                  redsysCommercePaymentMethodRequest.getHttpServletRequest()),
          _portal.getPathModule(), StringPool.SLASH,
          RedsysCommercePaymentMethodConstants.SERVLET_PATH);
}

​​​​​​

19.  Ahora dentro del método “processPayment”, implementaremos la siguiente lógica:

RedsysCommercePaymentMethodRequest redsysCommercePaymentMethodRequest = (RedsysCommercePaymentMethodRequest) commercePaymentRequest;

CommerceOrder commerceOrder = _commerceOrderService.getCommerceOrder(
      redsysCommercePaymentMethodRequest.getCommerceOrderId());

String transactionUuid = PortalUUIDUtil.generate();
String transactionId = StringUtil.replace(transactionUuid, CharPool.DASH, StringPool.BLANK);

CommerceCurrency commerceCurrency = commerceOrder.getCommerceCurrency();
Currency currencyIso4217 = Currency.getInstance(commerceCurrency.getCode());

URL returnUrl = new URL(redsysCommercePaymentMethodRequest.getReturnUrl());
String urlReturnDecoded = URLCodec.decodeURL(returnUrl.toString());

String newRedirectOK= StringUtil.extractLast(urlReturnDecoded, RedsysCommercePaymentMethodConstants.REDIRECT_PARAM);
String servlet = StringUtil.extractFirst(urlReturnDecoded, RedsysCommercePaymentMethodConstants.REDIRECT_PARAM).concat(StringPool.AMPERSAND.concat(RedsysCommercePaymentMethodConstants.REDIRECT_PARAM));

String urlCodedOK = Base64.getEncoder().encodeToString(newRedirectOK.getBytes());

URL urlOk = new URL(servlet.concat(urlCodedOK));

URL cancelUrl = new URL(redsysCommercePaymentMethodRequest.getCancelUrl());
String urlCancelDecoded = URLCodec.decodeURL(cancelUrl.toString());

String newRedirectKO= StringUtil.extractLast(urlCancelDecoded.toString(), RedsysCommercePaymentMethodConstants.REDIRECT_PARAM);

String urlCodedKO = Base64.getEncoder().encodeToString(newRedirectKO.getBytes());
URL urlNok = new URL(servlet.concat(urlCodedKO));

RedsysPaymentMethodCardGroupServiceConfiguration paymentMethodCardGroupServiceConfiguration =
      _getConfiguration(commerceOrder.getGroupId());

String environment = StringUtil.toUpperCase(paymentMethodCardGroupServiceConfiguration.mode());
String clientSecret = paymentMethodCardGroupServiceConfiguration.clientSecret();
String merchantCode = paymentMethodCardGroupServiceConfiguration.merchantCode();
String dsSignatureV = paymentMethodCardGroupServiceConfiguration.dsSignatureVersion();
String typeTransaction = paymentMethodCardGroupServiceConfiguration.typeTransaction();
String terminal = paymentMethodCardGroupServiceConfiguration.terminal();
RedsysTypeOfTransaction transactionValue = RedsysTypeOfTransaction.valueOf(typeTransaction.toUpperCase());

RedsysConfigClient redsysConfigClient = new RedsysConfigClient(dsSignatureV, clientSecret, RedsysEnvironment.valueOf(environment), merchantCode);

ApiMacSha256 apiMacSha256 = new ApiMacSha256();

apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_AMOUNT, commerceOrder.getTotal().setScale(2, RoundingMode.DOWN).toString().replace(".", ""));
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_ORDER, Long.toString(commerceOrder.getCommerceOrderId()));
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_MERCHANTCODE,redsysConfigClient.getMerchantCode());
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_CURRENCY, Integer.toString(currencyIso4217.getNumericCode()));
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_TRANSACTIONTYPE, transactionValue.getTypeOfTransaction());
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_TERMINAL, terminal);
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_MERCHANTURL, URLCodec.encodeURL(_getServletUrl(redsysCommercePaymentMethodRequest)));
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_URLOK,  URLCodec.encodeURL(urlOk.toString()));
apiMacSha256.setParameter(RedsysCommercePaymentMethodConstants.DS_MERCHANT_URLKO, URLCodec.encodeURL(urlNok.toString()));

String claveModuloAdmin = redsysConfigClient.getSignatureSecret();
String signature = apiMacSha256.createMerchantSignature(claveModuloAdmin);
String params = apiMacSha256.createMerchantParameters();
String dsSignatureVersion = redsysConfigClient.getDsSignatureVersion();

List<String> resultMessage =  new ArrayList<String>();

String url = StringBundler.concat(
      _getServletUrl(redsysCommercePaymentMethodRequest),
      StringPool.QUESTION, RedsysCommercePaymentMethodConstants.REDIRECT_URL,StringPool.EQUAL,URLCodec.encodeURL(redsysConfigClient.getEnvironmentUrl().toString()),
      StringPool.AMPERSAND,RedsysCommercePaymentMethodConstants.PARAMS,StringPool.EQUAL, URLEncoder.encode(params, StringPool.UTF8),
      StringPool.AMPERSAND,RedsysCommercePaymentMethodConstants.SIGNATURE, StringPool.EQUAL,URLEncoder.encode(signature, StringPool.UTF8),
      StringPool.AMPERSAND,RedsysCommercePaymentMethodConstants.DS_SIGNATURE_VERSION_PARAM, StringPool.EQUAL,dsSignatureVersion );

CommercePaymentResult commercePaymentResult = new CommercePaymentResult(
      transactionId, commerceOrder.getCommerceOrderId(),
      CommerceOrderConstants.ORDER_STATUS_PENDING, true, url, null,
      resultMessage, true);

return commercePaymentResult;
Importante: en resumen en esa implementación, puntos relevantes:
  • Creamos nuestro “RedsysCommercePaymentMethodRequest” con el valor de “CommercePaymentRequest”, y luego recuperamos algunos valores necesarios para la implementación.

  • Recuperamos la configuración del método de pago por canal, necesario para la conexión con la pasarela  de pago. 

  • Siguiendo la documentación de Redsys, creamos un objeto del tipo “ApiMacSha256”, lo seteamos con los valores necesarios, hacemos las llamadas de dos métodos para encriptar los datos. 

  • Creamos un enlace con nuestro servlet, concatenando 4 valores que necesitaremos en el “form” que crearemos a futuro (redsys-form.jsp).

  • Por fin, creamos un objeto “CommercePaymentResult” y seteamos en el retorno de nuestro método de “processPayment”. Cómo estamos en el proceso del pago, seteamos también el status del pedido como “ORDER_STATUS_PENDING”.

20.  En el paquete “com.liferay.commerce.payment.method.redsys.constants”, crea una nueva clase enum llamada “RedsysMessages” para tratar los mensajes que vendrán de Redsys, copia y pega el código de abajo dentro del enum:

M0000("authorized-transaction-payments-and-pre-authorizations"),
M0900("authorized-transaction-refund-and-confirmations"),
M0400("transaction-authorized-cancellations"),
M0101("expired-card"),
M0102("card-temporary-exception-suspicion-fraud"),
M0106("pin-attempts-exceeded"),
M0125("card-not-effective"),
M0129("incorrect-security-code"),
M0180("non-service-card"),
M0184("error-authentication-holder"),
M0190("refusal-without-specifying-reason"),
M0191("wrong-expiration-date"),
M0202("card-temporary-exception-suspicion-fraud-card-withdrawal"),
M0904("trade-not-registered-fuc"),
M0909("system-error"),
M0913("repeat-order"),
M0944("incorrect-session"),
M0950("refund-operation-not-allowed"),
M9912("issuer-not-available"),
M0912("issuer-not-available"),
M9064("incorrect-number-card-positions"),
M9078("type-operation-not-allowed-for-card"),
M9093("card-does-not-exist"),
M9094("rejection-international-servers"),
M9104("trade-with-secure-and-owner-without-secure-purchase-key"),
M9218("trade-does-not-allow-op-safe-for-entry"),
M9253("card-does-not-satisfy-check-digit"),
M9256("merchant-cannot-perform-pre-authorizations"),
M9257("card-does-not-allow-pre-authorization-operations"),
M9261("operation-stopped-exceeding-control-sis"),
M9913("error-confirmation-merchant-sends-only-applicable-soap-synchronization"),
M9914("confirmation-ko-merchant-only-applicable-soap-synchronization"),
M9915("users-request-payment-has-been-canceled"),
M9928("authorization-cancellation-perform-by-sis"),
M9929("cancellation-deferred-authorization-by-merchant"),
M9997("another-transaction-is-being-processed-with-same-card"),
M9998("operation-in-process-requesting-card-data"),
M9999("operation-has_been-redirected-to-issuer-to-authenticate");

public String getMessage() {
return _message;
}

private RedsysMessages(String message) {
_message = message;
}

private final String _message;

21.  Ahora volvemos a la primera clase la “RedsysCommercePaymentMethod”. Dentro de esta clase copia y pega el código de abajo, para crear el método para tratar los mensajes recibidos por Redsys:

private List<String> getMessagesFromResponse(RedsysCommercePaymentMethodRequest redsysCommercePaymentMethodRequest){

  HttpServletRequest httpServletRequest = redsysCommercePaymentMethodRequest.getHttpServletRequest();
  String codResponse = (String) httpServletRequest.getAttribute(RedsysCommercePaymentMethodConstants.COD_RESPONSE_REDSYS);
  RedsysMessages enumMessage = RedsysMessages.valueOf(("M").concat(codResponse));
  String messageLanguage = enumMessage.getMessage();
  ResourceBundle resourceBundle = ResourceBundleUtil.getBundle("content.Language", PortalUtil.getLocale(httpServletRequest), getClass());
  String messageLocalizable = LanguageUtil.get(resourceBundle, messageLanguage);
  List<String> messages = new ArrayList<String>();
  messages.add(messageLocalizable);

  return messages;
}

22.  Ahora dentro del método “cancelPayment”, implementaremos la siguiente lógica

List<String> messages = getMessagesFromResponse(redsysCommercePaymentMethodRequest);

return new CommercePaymentResult(
redsysCommercePaymentMethodRequest.getTransactionId(),
redsysCommercePaymentMethodRequest.getCommerceOrderId(),
CommerceOrderPaymentConstants.STATUS_CANCELLED, false, null, null,messages, true);

23.  Ahora dentro del método “completePayment”, implementaremos la siguiente lógica

RedsysCommercePaymentMethodRequest redsysCommercePaymentMethodRequest = (RedsysCommercePaymentMethodRequest)commercePaymentRequest;

List<String> messages = getMessagesFromResponse(redsysCommercePaymentMethodRequest);

return new CommercePaymentResult(redsysCommercePaymentMethodRequest.getTransactionId(),
redsysCommercePaymentMethodRequest.getCommerceOrderId(), CommerceOrderConstants.PAYMENT_STATUS_PAID, false, null, null, messages, true);
Importante: fijáte que recuperamos el mensaje en los métodos, añadimos al objeto de retorno “CommercePaymentResult” y en él también seteamos el estado del pedido de acuerdo con cada método.  

24.  Dentro de “META-INF/resources” crea una nueva carpeta “redsys_form” y dentro un fichero JSP con nombre “redsys-form.jsp”, copia y pega este código (aquí montamos el form que enviaremos a redsys)

<%@ include file="./../init.jsp" %>
<%@ page import="java.net.URLDecoder" %>

<%
String redirectUrlAttribute = (String)request.getAttribute(RedsysCommercePaymentMethodConstants.REDIRECT_URL);
String redirectUrl = URLCodec.decodeURL(redirectUrlAttribute);
String paramsAtrribute = (String)request.getAttribute(RedsysCommercePaymentMethodConstants.PARAMS);
String params = URLDecoder.decode(paramsAtrribute, StringPool.UTF8);
String signatureAttribute = (String)request.getAttribute(RedsysCommercePaymentMethodConstants.SIGNATURE);
String signature = URLDecoder.decode(signatureAttribute, StringPool.UTF8);
String dsSignatureVersion = (String)request.getAttribute(RedsysCommercePaymentMethodConstants.DS_SIGNATURE_VERSION_PARAM);
%>

<form action="<%= redirectUrl %>" class="hide" id="formRedsys" method="post" name="formRedsys">
<input name="<%= RedsysCommercePaymentMethodConstants.DS_SIGNATURE_VERSION %>" type="hidden" value="<%= dsSignatureVersion %>" />
<input name="<%= RedsysCommercePaymentMethodConstants.DS_MERCHANT_PARAMETERS %>" type="hidden" value="<%= params %>" />
<input name="<%= RedsysCommercePaymentMethodConstants.DS_SIGNATURE %>" type="hidden" value="<%= signature %>" />

<button id="btnContinue">Continue</button>
</form>

<script>
 (function () {
     if(document.readyState == 'complete') {
        document.querySelector('form').submit()
     } else {
    window.addEventListener('load', function(){
    document.querySelector('form').submit()
  })
 }
 })()
</script>

​​​​​​

25.  Abre la clase “RedsysCommercePaymentMethodServlet” y al final de la clase copia y pega este código a seguir, para crear logs e inyectar algunos servicios que iremos a utilizar

private static final Log _log = LogFactoryUtil.getLog(RedsysCommercePaymentMethodServlet.class);

@Reference
private CommerceOrderService _commerceOrderService;

@Reference
private ConfigurationProvider _configurationProvider;

@Reference
private CommercePaymentEngine _commercePaymentEngine;

@Reference
private CommerceSubscriptionEngine _commerceSubscriptionEngine;

@Reference
private Portal _portal;

@Reference(target = "(osgi.web.symbolicname=com.liferay.commerce.payment.method.redsys)")
private ServletContext _servletContext;

26.  Ahora crea el método para recuperar las configuraciones

private RedsysPaymentMethodCardGroupServiceConfiguration _getConfiguration(long groupId) throws ConfigurationException {
return _configurationProvider.getConfiguration(RedsysPaymentMethodCardGroupServiceConfiguration.class,
new GroupServiceSettingsLocator(groupId, RedsysCommercePaymentMethodConstants.SERVICE_NAME));
}

27.  Para finalizar, implementamos la lógica dentro del método “doGet”, que deberá quedar así

@Override
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {

   try {     
       HttpSession httpSession = httpServletRequest.getSession();
        if (PortalSessionThreadLocal.getHttpSession() == null) {
           PortalSessionThreadLocal.setHttpSession(httpSession);
       }      
       PermissionChecker permissionChecker = PermissionCheckerFactoryUtil.create(_portal.getUser(httpServletRequest));       
       PermissionThreadLocal.setPermissionChecker(permissionChecker);       
       String redirectCod = ParamUtil.getString(httpServletRequest, "redirect");      
       if (Validator.isNotNull(redirectCod)) {           
           long groupId = ParamUtil.getLong(httpServletRequest, "groupId");
           String uuid = ParamUtil.getString(httpServletRequest, "uuid");           
           CommerceOrder commerceOrder = _commerceOrderService.getCommerceOrderByUuidAndGroupId(uuid, groupId);
           byte[] bytesDecoded = Base64.getDecoder().decode(redirectCod);
           String redirect = new String(bytesDecoded);           
           ApiMacSha256 apiMacSha256 = new ApiMacSha256();
           String params = ParamUtil.getString(httpServletRequest, RedsysCommercePaymentMethodConstants.DS_MERCHANT_PARAMETERS);
           String signature = ParamUtil.getString(httpServletRequest, RedsysCommercePaymentMethodConstants.DS_SIGNATURE);

           apiMacSha256.decodeMerchantParameters(params);           
           String codResponseRedsys = apiMacSha256.getParameter("Ds_Response");

           httpServletRequest.setAttribute(RedsysCommercePaymentMethodConstants.COD_RESPONSE_REDSYS, codResponseRedsys);          
            if (!redirect.contains("cancel")) {              
               RedsysPaymentMethodCardGroupServiceConfiguration paymentMethodCardGroupServiceConfiguration = _getConfiguration(commerceOrder.getGroupId());             
               String clientSecret = paymentMethodCardGroupServiceConfiguration.clientSecret();

               String signatureCalculed = apiMacSha256.createMerchantSignatureNotif(clientSecret, params);              
               if (signatureCalculed.equals(signature)) {                  
                   String dsOrder = apiMacSha256.getParameter("Ds_Order");
                   String dsAmount = apiMacSha256.getParameter("Ds_Amount");
                   String dsMerchantCode = apiMacSha256.getParameter("Ds_MerchantCode");
                   String dsTerminal = apiMacSha256.getParameter("Ds_Terminal");
                   String dsTransactionType = apiMacSha256.getParameter("Ds_TransactionType");                   
                   String order = Long.toString(commerceOrder.getCommerceOrderId());
                   String amount = commerceOrder.getTotal().setScale(2, RoundingMode.DOWN)).toString().replace(".","");
                   String merchantCode = paymentMethodCardGroupServiceConfiguration.merchantCode();
                   String terminal = paymentMethodCardGroupServiceConfiguration.terminal();
                   String typeTransaction = paymentMethodCardGroupServiceConfiguration.typeTransaction();
                   RedsysTypeOfTransaction transactionValue = RedsysTypeOfTransaction.valueOf(typeTransaction.toUpperCase());

             if(dsOrder.equalsIgnoreCase(order) && dsAmount.equals(amount) && dsMerchantCode.equalsIgnoreCase(merchantCode)&& dsTerminal.equalsIgnoreCase("0".concat(terminal)) && dsTransactionType.equalsIgnoreCase(transactionValue.getTypeOfTransaction())) {           
                   _commercePaymentEngine.completePayment(commerceOrder.getCommerceOrderId(), null,httpServletRequest);                  
              }else {
                       _log.error(RedsysCommercePaymentMethodConstants.ERROR_PARAMETERS_COMPARE);
                       throw new Exception(RedsysCommercePaymentMethodConstants.ERROR_PARAMETERS_COMPARE);
                   }

               }else {
                   _log.error(RedsysCommercePaymentMethodConstants.ERROR_SIGNATURE);
                   throw new Exception(RedsysCommercePaymentMethodConstants.ERROR_SIGNATURE);
               }
               } else {
               _commercePaymentEngine.cancelPayment(commerceOrder.getCommerceOrderId(), null, httpServletRequest);

           }           
    httpServletResponse.sendRedirect(redirect);        
       } else {        
       RequestDispatcher requestDispatcher = _servletContext.getRequestDispatcher("/redsys_form/redsys-form.jsp");          
       requestDispatcher.forward(httpServletRequest, httpServletResponse);

       }  
       }catch (Exception e) {

       _portal.sendError(e, httpServletRequest, httpServletResponse);

   }
Importante: los 3 métodos “processPayment”, “completePayment”, “cancelPayment” pasan siempre por el método “doGet”, por eso dentro de él tenemos las 3 lógicas:
  • Cuando no venga el parámetro “redirect” en el httpServletRequest, significa que es una llamada del método de “processPayment”  y que debemos entonces hacer un request dispatcher con el servlet lanzado al form creado en “redsys-form.jsp”. Entonces se redireccionará a la plataforma de pago de Redsys. 

  • Cuando venga el parámetro “redirect” en el httpServletRequest, significa que es una respuesta de Redsys, validamos si existe el parámetro “cancel” en la cadena del redirect:

    • Si contiene el parámetro, llamamos el método “cancelPayment”.

    • Si no contiene el parámetro, validamos los dados de retorno de la transacción de acuerdo con lo recomendado en la documentación:

      • Si todo está Ok, llamamos el método “completePayment”.

      • Si no pasa por la validación, lanzamos un error y el estado del pedido seguirá como “PENDING”.

28.  Por terminar, añada las traducciones de las mensajes recibidas por Redsys en el fichero “Language.properties”

##################-----       Messages from Redsys      -----#####################
error-authentication-holder=Error in the authentication of the holder
authorized-transaction-payments-and-pre-authorizations=Authorized transaction for payments and pre-authorizations
authorized-transaction-refund-and-confirmations=Authorized transaction for refund and confirmations
transaction-authorized-cancellations=Transaction authorized for cancellations
expired-card=Expired card
card-temporary-exception-suspicion-fraud=Card in temporary exception or under suspicion of fraud
pin-attempts-exceeded=PIN attempts exceeded
card-not-effective=Card not effective
incorrect-security-code=Incorrect security code (CVV2 / CVC2)
non-service-card=Non-service card
refusal-without-specifying-reason=Refusal of the issuer without specifying reason
wrong-expiration-date=Wrong expiration date
card-temporary-exception-suspicion-fraud-card-withdrawal=Card in temporary exception or under suspicion of fraud with card withdrawal
trade-not-registered-fuc=Trade not registered in FUC
system-error=System error
repeat-order=Repeat order
incorrect-session=Incorrect Session
refund-operation-not-allowed=Refund operation not allowed
issuer-not-available=Issuer not available
incorrect-number-card-positions=Incorrect number of card positions
type-operation-not-allowed-for-card=Type of operation not allowed for that card
card-does-not-exist=Card does not exist
rejection-international-servers=Rejection international servers
trade-with-secure-and-owner-without-secure-purchase-key=Trade with "secure owner" and owner without secure purchase key
trade-does-not-allow-op-safe-for-entry=The trade does not allow op. safe for entry / operations
card-does-not-satisfy-check-digit=Card does not satisfy the check-digit
merchant-cannot-perform-pre-authorizations=The merchant cannot perform pre-authorizations
card-does-not-allow-pre-authorization-operations=This card does not allow pre-authorization operations
operation-stopped-exceeding-control-sis=Operation stopped for exceeding the control of restrictions in the entrance to the SIS
error-confirmation-merchant-sends-only-applicable-soap-synchronization=Error in the confirmation that the merchant sends to the Virtual TPV (only applicable in the SOAP synchronization option)
confirmation-ko-merchant-only-applicable-soap-synchronization=Confirmation "KO" of the merchant (only applicable in the SOAP synchronization option)
users-request-payment-has-been-canceled=User's request payment has been canceled
authorization-cancellation-perform-by-sis=Authorization cancellation perform by the SIS (batch process)
cancellation-deferred-authorization-by-merchant=Cancellation of deferred authorization by the merchant
another-transaction-is-being-processed-with-same-card=Another transaction is being processed in SIS with the same card
operation-in-process-requesting-card-data=Operation in process of requesting card data
operation-has_been-redirected-to-issuer-to-authenticate=Operation that has been redirected to the issuer to authenticate
Importante: el sistema de pago de Redsys de test, solo espera el Euro como moneda, por lo tanto en el Channel, estate seguro de haber definido la moneda de tu canal como Euro.

29.  Guarda los cambios y despliega el módulo ejecutando el siguiente comando dentro del directorio de tu módulo en el Liferay Workspace:

commerce-payment-method-redsys% ./../../gradlew deploy

30.  Ahora abre tu Liferay Portal (ej.: localhost:8080), abre la tienda online que contenga el Channel creado anteriormente. Añade al carrito unos productos y efectúa la compra. En el step de pago, debería presentarse la opción creada (Redsys), seleccionala y haz click  en “continue”

En el step de resumen, te presentará la opción de pago seleccionada y los demás datos de la compra, haz click en “continue” para que redireccione a la pasarela de pago.

Te redireccionará a la plataforma de Redsys (el entorno de test), añade los siguientes datos de prueba para efectuar el pago:

  • N de Tarjeta: 4548 8120 4940 0004

  • Caducidad: 12/20

  • Cód. Seguridad: 123

Tras cumplimentar, haz clicken “Pagar”, para simular el pago con éxito, selecciona “Autenticación con éxito“ y haz click en “Enviar”

Se direccionará al portal, pasando por el método de “completePayment” 

Para para simular el pago con error, selecciona “Denegar autenticación” y haz click  en “Enviar”

En este caso se redireccionará al portal, pasando por el método “cancelPayment” 

Como habéis comprobado, en ambos casos redireccionará de vuelta a Liferay Commerce, con el mensaje de “cancelled” capturado de la request de Redsys (y la opción de volver a intentar el pago) o en caso de éxito con el mensaje de “sucess”. 

El código usado en este paso está disponible en una etiqueta con nombre “payment-redsys-last-step” en mi repo de github. 

Finalmente terminamos por aquí nuestro “big blog”, ahora con los conocimientos de cómo crear un método de pago nuevo para Liferay Commerce.