You are here

commerce_sermepa.module in Commerce sermepa 7

Provides a payment method for Drupal Commerce using Sermepa/Redsys gateway.

File

commerce_sermepa.module
View source
<?php

/**
 * @file
 * Provides a payment method for Drupal Commerce using Sermepa/Redsys gateway.
 */
use CommerceRedsys\Payment\Sermepa;

/**
 * Implements hook_hook_info().
 */
function commerce_sermepa_hook_info() {
  $hooks = array(
    'commerce_sermepa_gateway' => array(
      'group' => 'commerce_sermepa',
    ),
  );
  return $hooks;
}

/**
 * Implements hook_libraries_info().
 */
function commerce_sermepa_libraries_info() {
  $libraries['sermepa'] = array(
    'name' => 'Sermepa/Redsys API',
    'vendor url' => 'https://github.com/CommerceRedsys/sermepa',
    'download url' => 'https://github.com/CommerceRedsys/sermepa/releases',
    'version arguments' => array(
      'file' => 'CHANGELOG.md',
      'pattern' => '@([0-9.]+)@',
      'lines' => 1,
      'cols' => 20,
    ),
    'files' => array(
      'php' => array(
        'src/SermepaException.php',
        'src/SermepaInterface.php',
        'src/Sermepa.php',
      ),
    ),
  );
  return $libraries;
}

/**
 * Helper function to create a Sermepa instance.
 *
 * @param array $settings
 *   An array of the current payment method settings.
 *
 * @return mixed
 *   Initialized \CommerceRedsys\Payment\Sermepa Object, otherwise FALSE.
 */
function commerce_sermepa_library_initialize($settings) {

  // Load Sermepa API.
  if (!class_exists('Sermepa')) {
    $library = libraries_load('sermepa');
    if (!$library || empty($library['loaded'])) {
      return FALSE;
    }
  }

  // Create a Sermepa instance.
  if (!empty($settings['advanced']['override_url'])) {
    $environment = $settings['advanced']['override_url'];
  }
  else {
    $environment = $settings['mode'];
  }
  $gateway = new Sermepa($settings['Ds_MerchantName'], $settings['Ds_MerchantCode'], $settings['Ds_Merchant_Terminal'], $settings['Ds_MerchantPassword'], $environment);
  return $gateway;
}

/**
 * Returns an array of Sermepa payment method icon img elements.
 *
 * @return array
 *   The array of themed payment method icons keyed by name: visa, mastercard,
 *   maestro.
 */
function commerce_sermepa_get_icons() {
  $icons = array();
  $payment_methods = array(
    'mastercard' => t('MasterCard'),
    'visa' => t('Visa'),
    'maestro' => t('Maestro'),
    'sermepa_redsys' => t('Redsýs'),
  );
  foreach ($payment_methods as $name => $title) {
    $variables = array(
      'path' => drupal_get_path('module', 'commerce_sermepa') . '/images/' . $name . '.png',
      'title' => $title,
      'alt' => $title,
      'attributes' => array(
        'class' => array(
          'commerce-sermepa-icon',
        ),
      ),
    );
    $icons[$name] = theme('image', $variables);
  }
  return $icons;
}

/**
 * Implements hook_menu().
 */
function commerce_sermepa_menu() {
  $items['sermepa/callback/%commerce_order'] = array(
    'page callback' => 'commerce_sermepa_callback',
    'page arguments' => array(
      2,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Get POST response from sermepa.
 */
function commerce_sermepa_callback($order) {
  if (!$order) {
    watchdog('commerce_sermepa', "Bad '%order_id' order id received in feedback values.", array(
      '%order_id' => arg(2),
    ), WATCHDOG_WARNING);
    return FALSE;
  }

  // Load the payment method.
  $payment_method = commerce_payment_method_instance_load($order->data['payment_method']);
  if (!$payment_method || $payment_method['method_id'] != 'commerce_sermepa') {
    watchdog('commerce_sermepa', "Unknown or non-existent '%method_id' payment method.", array(
      '%method_id' => 'commerce_sermepa',
    ), WATCHDOG_WARNING);
    return FALSE;
  }

  // Create a sermepa instance.
  if (!($gateway = commerce_sermepa_library_initialize($payment_method['settings']))) {
    return FALSE;
  }

  // Get response data.
  if (!($feedback = $gateway
    ->getFeedback())) {
    watchdog('commerce_sermepa', "Bad feedback response data.", array(), WATCHDOG_WARNING);
    return FALSE;
  }

  // Get order number from feedback data and compare it with the order object
  // argument.
  $parameters = $gateway
    ->decodeMerchantParameters($feedback['Ds_MerchantParameters']);
  $order_id = $parameters['Ds_MerchantData'];
  if ($order_id != $order->order_id) {
    watchdog('commerce_sermepa', "The order id from feedback and the order object argument id don't match.", array(), WATCHDOG_WARNING);
  }

  // Validate feedback values.
  if (!$gateway
    ->validSignatures($feedback)) {
    watchdog('commerce_sermepa', "Bad feedback response, signatures don't match.", array(), WATCHDOG_WARNING);
    return FALSE;
  }

  // Process the transaction.
  commerce_sermepa_process_transaction($order, $payment_method, $parameters);
  return FALSE;
}

/**
 * Implements hook_commerce_payment_method_info().
 */
function commerce_sermepa_commerce_payment_method_info() {
  $payment_methods = array();

  // Get sermepa and card icons.
  $icons = commerce_sermepa_get_icons();
  $display_title = t('!logo - pay with credit or debit card', array(
    '!logo' => $icons['sermepa_redsys'],
  ));

  // Remove sermepa icon logo.
  unset($icons['sermepa_redsys']);
  $display_title .= '<div class="commerce-sermepa-icons"><span class="label">' . t('Includes:') . '</span>' . implode(' ', $icons) . '</div>';
  $payment_methods['commerce_sermepa'] = array(
    'base' => 'commerce_sermepa',
    'title' => t('Sermepa Payment'),
    'display_title' => $display_title,
    'short_title' => t('Sermepa'),
    'description' => t('Sermepa/Redsýs payment gateway integration'),
    'terminal' => TRUE,
    'offsite' => TRUE,
    'offsite_autoredirect' => TRUE,
    'active' => TRUE,
  );
  return $payment_methods;
}

/**
 * Payment method callback: settings form.
 *
 * @return array
 *   Form elements for the payment method's settings form included
 *   as part of the payment method's enabling action in Rules.
 */
function commerce_sermepa_settings_form($settings = NULL) {
  libraries_load('sermepa');
  $form = array();
  $settings = (array) $settings + array(
    'mode' => 'test',
    'Ds_MerchantName' => '',
    'Ds_MerchantCode' => '',
    'Ds_MerchantPassword' => '',
    'Ds_Merchant_Terminal' => '',
    'Ds_Merchant_PayMethods' => array(
      'C',
    ),
    'Ds_Merchant_ConsumerLanguage' => '001',
    'currency' => '978',
    'advanced' => array(
      'override_url' => '',
    ),
  );
  $form['mode'] = array(
    '#type' => 'radios',
    '#title' => t('Mode of the transactions'),
    '#default_value' => $settings['mode'],
    '#options' => array(
      'test' => t('Test'),
      'live' => t('Live'),
    ),
    '#required' => TRUE,
  );
  $form['Ds_MerchantName'] = array(
    '#type' => 'textfield',
    '#title' => t('Merchant Name'),
    '#default_value' => $settings['Ds_MerchantName'],
    '#size' => 80,
    '#maxlength' => Sermepa::getMerchantNameMaxLength(),
    '#required' => TRUE,
  );
  $form['Ds_MerchantCode'] = array(
    '#type' => 'textfield',
    '#title' => t('Merchant Code'),
    '#default_value' => $settings['Ds_MerchantCode'],
    '#size' => 80,
    '#maxlength' => Sermepa::getMerchantCodeMaxLength(),
    '#required' => TRUE,
  );
  $form['Ds_MerchantPassword'] = array(
    '#type' => 'textfield',
    '#title' => t('SHA256 Merchant Password'),
    '#default_value' => $settings['Ds_MerchantPassword'],
    '#size' => 80,
    '#maxlength' => Sermepa::getMerchantPasswordMaxLength(),
    '#required' => TRUE,
  );
  $form['Ds_Merchant_Terminal'] = array(
    '#type' => 'textfield',
    '#title' => t('Merchant Terminal'),
    '#default_value' => $settings['Ds_Merchant_Terminal'],
    '#size' => 5,
    '#maxlength' => Sermepa::getMerchantTerminalMaxLength(),
    '#required' => TRUE,
  );
  $form['Ds_Merchant_PayMethods'] = array(
    '#type' => 'select',
    '#multiple' => TRUE,
    '#title' => t('Merchant Consumer Language'),
    '#default_value' => $settings['Ds_Merchant_PayMethods'],
    '#options' => Sermepa::getAvailablePaymentMethods(),
    '#required' => FALSE,
  );
  $form['Ds_Merchant_ConsumerLanguage'] = array(
    '#type' => 'select',
    '#title' => t('Merchant Consumer Language'),
    '#default_value' => $settings['Ds_Merchant_ConsumerLanguage'],
    '#options' => Sermepa::getAvailableConsumerLanguages(),
    '#required' => TRUE,
  );
  $form['currency'] = array(
    '#type' => 'select',
    '#title' => t('Currency'),
    '#default_value' => $settings['currency'],
    '#options' => Sermepa::getAvailableCurrencies(),
    '#required' => TRUE,
  );
  $form['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Payment instructions'),
    '#description' => t('Instructions for customers on the checkout page. Use &lt;br /&gt; for line break.'),
    '#default_value' => isset($settings['description']) ? $settings['description'] : '',
    '#required' => FALSE,
    '#rows' => 3,
  );
  $form['advanced'] = array(
    '#type' => 'fieldset',
    '#title' => t('Advanced options'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['advanced']['override_url'] = array(
    '#type' => 'textfield',
    '#title' => t('Override bank connect url'),
    '#default_value' => $settings['advanced']['override_url'],
    '#size' => 80,
    '#maxlength' => 255,
    '#element_validate' => array(
      'commerce_sermepa_settings_override_url_validate',
    ),
  );
  return $form;
}

/**
 * Form element validation handler for override bank connect url.
 */
function commerce_sermepa_settings_override_url_validate($element, &$form_state, $form) {
  if (!empty($element['#value']) && !filter_var($element['#value'], FILTER_VALIDATE_URL)) {
    form_error($element, t('The specified "Override bank connect url" is not valid.'));
  }
}

/**
 * Payment method callback: redirect form to sermepa gateway.
 *
 * @param commerce_order $order
 *   The fully loaded order being paid for.
 * @param array $payment_method
 *   An array of the current settings.
 *
 * @return array
 *   Form elements that should be submitted to the redirected
 *   payment service.
 */
function commerce_sermepa_redirect_form($form, &$form_state, $order, $payment_method) {

  // Return an error if the enabling action's settings haven't been configured.
  if (empty($payment_method['settings']['Ds_MerchantCode'])) {
    drupal_set_message(t('Sermepa is not configured for use. Merchant code has not been specified.'), 'error');
    return array();
  }
  if (empty($payment_method['settings']['Ds_MerchantPassword'])) {
    drupal_set_message(t('SHA256 Sermepa password is not set for use. Merchant password has not been specified.'), 'error');
    return array();
  }
  if (empty($payment_method['settings']['Ds_Merchant_Terminal'])) {
    drupal_set_message(t('Sermepa is not configured for use. Merchant terminal has not been specified.'), 'error');
    return array();
  }
  $settings = array(
    // Return to the previous page when payment is canceled.
    'cancel_return' => url('checkout/' . $order->order_id . '/payment/back/' . $order->data['payment_redirect_key'], array(
      'absolute' => TRUE,
    )),
    // Return to the payment redirect page for processing successful payments.
    'return' => url('checkout/' . $order->order_id . '/payment/return/' . $order->data['payment_redirect_key'], array(
      'absolute' => TRUE,
    )),
    // Url to get POST result of payment.
    'merchant_url' => url('sermepa/callback/' . $order->order_id, array(
      'absolute' => TRUE,
    )),
    // Specify the current payment method instance ID in the notify_url.
    'payment_method' => $payment_method['instance_id'],
  );
  return commerce_sermepa_order_form($form, $form_state, $order, $payment_method['settings'] + $settings);
}

/**
 * Build the order form for the sermepa.
 *
 * @param commerce_order $order
 *   The fully loaded order being paid for.
 * @param array $settings
 *   An array of the current settings.
 *
 * @return array
 *   Form elements that should be submitted to the redirected
 *   payment service.
 */
function commerce_sermepa_order_form($form, &$form_state, $order, $settings) {

  // Create a sermepa instance.
  if (!($gateway = commerce_sermepa_library_initialize($settings))) {
    return FALSE;
  }

  // Prepare transaction data.
  // Set Authorization trasaction type.
  // See \CommerceRedsys\Payment\Sermepa::getAvailableTransactionTypes() for full list.
  $gateway
    ->setTransactionType('0')
    ->setOrder(substr(date('ymdHis') . $order->order_id, -12, 12))
    ->setAmount($order->commerce_order_total[LANGUAGE_NONE][0]['amount'])
    ->setCurrency($settings['currency'])
    ->setMerchantURL($settings['merchant_url'])
    ->setUrlOK($settings['return'])
    ->setUrlKO($settings['cancel_return'])
    ->setConsumerLanguage($settings['Ds_Merchant_ConsumerLanguage'])
    ->setMerchantData($order->order_id);

  // Set payment methods.
  if (!empty($settings['Ds_Merchant_PayMethods'])) {
    $gateway
      ->setPaymentMethod(implode('', $settings['Ds_Merchant_PayMethods']));
  }

  // Allow user to make changes in the gateway configuration data.
  rules_invoke_all('commerce_sermepa_gateway', $gateway, $order);
  $form['#action'] = $gateway
    ->getEnvironment();

  // Force rebuild the form.
  $form_sate['rebuild'] = TRUE;

  // Create hidden fields.
  $parameters = $gateway
    ->composeMerchantParameters();
  if ($parameters) {
    $form['Ds_SignatureVersion'] = array(
      '#type' => 'hidden',
      '#value' => $gateway
        ->getSignatureVersion(),
    );
    $form['Ds_MerchantParameters'] = array(
      '#type' => 'hidden',
      '#value' => $parameters,
    );
    $form['Ds_Signature'] = array(
      '#type' => 'hidden',
      '#value' => $gateway
        ->composeMerchantSignature(),
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Redirect to Redsys platform'),
  );
  return $form;
}

/**
 * Implements hook_redirect_form_validate().
 *
 * @param commerce_order $order
 *   The order object.
 * @param array $payment_method
 *   The payment method array.
 *
 * @return boolean
 *   TRUE if the customer should proceed to checkout completion or FALSE to go
 *   back one step in the checkout process.
 */
function commerce_sermepa_redirect_form_validate($order, $payment_method) {
  commerce_sermepa_process_callback($order, $payment_method);
}

/**
 * Process callback information from Sermepa.
 *
 * This can either be through a redirect after payment,
 * or a Direct HTTP server-to-server request.
 *
 * @param commerce_order $order
 *   The order object.
 * @param array $payment_method
 *   The payment method array.
 *
 * @return boolean
 *   TRUE if is valid, otherwise FALSE.
 */
function commerce_sermepa_process_callback($order, $payment_method) {

  // Create a sermepa instance.
  if (!($gateway = commerce_sermepa_library_initialize($payment_method['settings']))) {
    return FALSE;
  }

  // Get response data.
  if (!($feedback = $gateway
    ->getFeedback())) {
    watchdog('commerce_sermepa', "Bad feedback response data.", array(), WATCHDOG_WARNING);
    return FALSE;
  }

  // Validate feedback values.
  if (!$gateway
    ->validSignatures($feedback)) {
    watchdog('commerce_sermepa', "Bad feedback response, signatures don't match.", array(), WATCHDOG_WARNING);
    return FALSE;
  }
  $parameters = $gateway
    ->decodeMerchantParameters($feedback['Ds_MerchantParameters']);
  commerce_sermepa_process_transaction($order, $payment_method, $parameters);
  return TRUE;
}

/**
 * Get transaction with a specific Sermepa Ds_AuthorisationCode.
 *
 * @param string $authorisation_code
 *   The authorisation code received from Sermepa regarding the payment.
 */
function commerce_sermepa_get_payment_transaction($authorisation_code) {
  $query = new EntityFieldQuery();
  $result = $query
    ->entityCondition('entity_type', 'commerce_payment_transaction')
    ->propertyCondition('payment_method', 'commerce_sermepa')
    ->propertyCondition('remote_id', $authorisation_code)
    ->execute();
  if (!empty($result['commerce_payment_transaction']) && count($result['commerce_payment_transaction']) > 0) {
    $transaction = array_pop($result['commerce_payment_transaction']);
    return $transaction->transaction_id;
  }
  return FALSE;
}

/**
 * Save the payment transaction for the order.
 *
 * @param commerce_order $order
 *   The loaded order that is being processed.
 * @param array $payment_method
 *   The payment method settings.
 * @param array $feedback_parameters
 *   An associative array of feedback merchant parameters values:
 *   - Ds_Date: Transaction date (dd/mm/yyyy).
 *   - Ds_Hour: Transaction time (HH:mm).
 *   - Ds_Amount: Same of the transaction.
 *   - Ds_Currency: Same of the transaction.
 *   - Ds_Order: Same of the transaction.
 *   - Ds_MerchantCode: Same of the transaction.
 *   - Ds_Terminal: Assigned by Sermepa.
 *   - Ds_Response: Response values, see $this->handleResponse.
 *   - Ds_MerchantData: Optional sended from commerce form.
 *   - Ds_SecurePayment: 0 for no secure payment, 1 for secure.
 *   - Ds_TrasactionType: Trasaction type sended from commerce form.
 *   - Ds_Card_Country: (Optional) Country of issuance of the card that has
 *       tried to make the payment..
 *   - Ds_AuthorisationCode: (Optional) Assigned authorisation code.
 *   - Ds_ConsumerLanguage: (Optional) 0 indicates that has not been
 *       determined the customer's language..
 *   - Ds_Card_Type: (Optional) C for credit, D for debit.
 * @param boolean $redirect
 *   Specifies whether to call redirect functions or not.
 */
function commerce_sermepa_process_transaction($order, $payment_method, $feedback_parameters, $redirect = TRUE) {
  $authorisation_code = $feedback_parameters['Ds_AuthorisationCode'];
  $transaction_id = commerce_sermepa_get_payment_transaction($authorisation_code);
  if (!$transaction_id) {
    $transaction = commerce_payment_transaction_new('commerce_sermepa', $order->order_id);
  }
  else {
    $transaction = commerce_payment_transaction_load($transaction_id);
  }

  // Handle the response of the bank.
  if (!class_exists('Sermepa')) {
    libraries_load('sermepa');
  }
  $response_code = $feedback_parameters['Ds_Response'];
  $transaction_status = array(
    'code' => $response_code <= 99 ? COMMERCE_PAYMENT_STATUS_SUCCESS : COMMERCE_PAYMENT_STATUS_FAILURE,
    'message' => Sermepa::handleResponse($response_code),
  );
  $transaction->instance_id = $payment_method['instance_id'];
  $transaction->amount = $order->commerce_order_total[LANGUAGE_NONE][0]['amount'];
  $transaction->currency_code = $order->commerce_order_total[LANGUAGE_NONE][0]['currency_code'];
  $transaction->remote_id = $authorisation_code;
  $transaction->remote_status = $feedback_parameters['Ds_Response'];
  $transaction->status = $transaction_status['code'];
  if ($transaction_status['code'] == COMMERCE_PAYMENT_STATUS_SUCCESS) {
    $transaction->message = 'Transaction accepted with id @transaction_id';
  }
  elseif ($transaction_status['code'] == COMMERCE_PAYMENT_STATUS_FAILURE) {
    $transaction->message = 'Error for the transaction with id @transaction_id: ' . $transaction_status['message'];
  }
  $transaction->message_variables = array(
    '@transaction_id' => $authorisation_code,
  );
  commerce_payment_transaction_save($transaction);
  if ($redirect) {
    if ($transaction_status['code'] == COMMERCE_PAYMENT_STATUS_FAILURE) {
      commerce_payment_redirect_pane_previous_page($order);
    }
    else {
      commerce_payment_redirect_pane_next_page($order);
    }
  }
}

/**
 * Payment method callback: checkout form.
 *
 * @param array $payment_method
 *   An array of the current settings.
 * @param array $pane_values
 *   The current values of the pane.
 * @param array $checkout_pane
 *   The checkout pane array. The checkout pane will be NULL if the payment is
 *   being added through the administration form.
 * @param commerce_order $order
 *   The order object.
 *
 * @return array
 *   A form snippet for the checkout pane.
 */
function commerce_sermepa_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
  $form = array();
  if (!empty($payment_method['settings']['description'])) {
    $form['sermepa_description'] = array(
      '#type' => 'item',
      '#title' => t('Payment instructions'),
      '#markup' => '<p class="sermepa-description">' . $payment_method['settings']['description'] . '</p>',
    );
  }
  if (is_null($checkout_pane)) {

    // The authorisationCode must be required as it is used to check if the
    // transaction already exists.
    // @see commerce_sermepa_process_transaction();
    // @see commerce_sermepa_get_payment_transaction();
    $form['Ds_AuthorisationCode'] = array(
      '#type' => 'textfield',
      '#title' => t('Ds_AuthorisationCode'),
      '#description' => t('Assigned authorisation code.'),
      '#required' => TRUE,
    );
  }
  return $form;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function commerce_sermepa_form_commerce_checkout_form_alter(&$form, &$form_state) {

  // If this checkout form contains the payment method radios...
  if (!empty($form['commerce_payment']['payment_method']['#options'])) {

    // Loop over its options array looking for a Commerce Sermepa options.
    foreach (array_keys($form['commerce_payment']['payment_method']['#options']) as $key) {
      list($method_id, $rule_name) = explode('|', $key);

      // If we find Commerce Sermepa, include its CSS on the form and exit the
      // loop.
      if ($method_id == 'commerce_sermepa') {
        $form['commerce_payment']['payment_method']['#attached']['css'][] = drupal_get_path('module', 'commerce_sermepa') . '/theme/commerce_sermepa.theme.css';
        break;
      }
    }
  }
}

/**
 * Payment method callback: submit form submission.
 */
function commerce_sermepa_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) {

  // We need to discern if the payment is being added through the administration
  // form to create the transaction. We haven't found a better way than checking
  // that the form element "Ds_AuthorisationCode" is present.
  if (!empty($pane_form['Ds_AuthorisationCode'])) {
    commerce_sermepa_process_transaction($order, $payment_method, $pane_values);
  }
}

Functions

Namesort descending Description
commerce_sermepa_callback Get POST response from sermepa.
commerce_sermepa_commerce_payment_method_info Implements hook_commerce_payment_method_info().
commerce_sermepa_form_commerce_checkout_form_alter Implements hook_form_FORM_ID_alter().
commerce_sermepa_get_icons Returns an array of Sermepa payment method icon img elements.
commerce_sermepa_get_payment_transaction Get transaction with a specific Sermepa Ds_AuthorisationCode.
commerce_sermepa_hook_info Implements hook_hook_info().
commerce_sermepa_libraries_info Implements hook_libraries_info().
commerce_sermepa_library_initialize Helper function to create a Sermepa instance.
commerce_sermepa_menu Implements hook_menu().
commerce_sermepa_order_form Build the order form for the sermepa.
commerce_sermepa_process_callback Process callback information from Sermepa.
commerce_sermepa_process_transaction Save the payment transaction for the order.
commerce_sermepa_redirect_form Payment method callback: redirect form to sermepa gateway.
commerce_sermepa_redirect_form_validate Implements hook_redirect_form_validate().
commerce_sermepa_settings_form Payment method callback: settings form.
commerce_sermepa_settings_override_url_validate Form element validation handler for override bank connect url.
commerce_sermepa_submit_form Payment method callback: checkout form.
commerce_sermepa_submit_form_submit Payment method callback: submit form submission.