You are here

commerce_robokassa.module in Commerce robokassa 7.2

Same filename and directory in other branches
  1. 7 commerce_robokassa.module

Drupal Commerce Robokassa payment method.

This module contains basic integration with Robokassa for Drupal Commerce.

File

commerce_robokassa.module
View source
<?php

/**
 * @file
 * Drupal Commerce Robokassa payment method.
 *
 * This module contains basic integration with Robokassa
 * for Drupal Commerce.
 */

/**
 * Implements hook_commerce_payment_method_info().
 */
function commerce_robokassa_commerce_payment_method_info() {
  $payment_methods = array();
  $payment_methods['commerce_robokassa'] = array(
    'title' => t('Robokassa'),
    'description' => t('Provides the payment method for robokassa service.'),
    'active' => TRUE,
    'terminal' => FALSE,
    'offsite' => TRUE,
    'offsite_autoredirect' => TRUE,
  );
  return $payment_methods;
}

/**
 * Implements hook_menu().
 */
function commerce_robokassa_menu() {
  $items['commerce_robokassa/%commerce_robokassa_pm/%'] = array(
    'title' => 'Payment status',
    'page callback' => 'commerce_robokassa_status',
    'page arguments' => array(
      1,
      2,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  $items['commerce_robokassa/%commerce_robokassa_pm/result'] = array(
    'title' => 'Result of payment through robokassa Merchant',
    'page callback' => 'commerce_robokassa_result',
    'page arguments' => array(
      1,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Payment method loader.
 *
 * Load payment method with settings from url arg to support multiple payment
 * method instance if need.
 *
 * @param string $instance_id
 *   Variable part of payment method instance.
 *
 * @return bool
 *   Payment method instance.
 */
function commerce_robokassa_pm_load($instance_id) {
  $instance_id = 'commerce_robokassa|commerce_payment_' . $instance_id;
  return commerce_payment_method_instance_load($instance_id);
}

/**
 * Page callback: commerce_robokassa/%commerce_robokassa_pm/%.
 *
 * Fallback call result menu callback because Robokassa call result callback
 * only on success payment.
 *
 * @param mixed $payment_method
 *   Drupal commerce payment method instance passed via url param.
 * @param string $status
 *   Payment status (fail/success allowed).
 */
function commerce_robokassa_status($payment_method, $status) {
  $allowed_statuses = array(
    'success',
    'fail',
  );
  if (!in_array($status, $allowed_statuses)) {
    drupal_exit();
  }
  $data = $_POST;
  if ($status != 'success') {
    commerce_robokassa_result($payment_method, FALSE);
  }
  if (isset($data['InvId'])) {

    // This parameter availability is flag for 7.x.2.x transactions.
    $shp_trx_id = isset($data['shp_trx_id']) ? $data['shp_trx_id'] : FALSE;
    $transaction = _commerce_robokassa_transaction_load($data['InvId'], $shp_trx_id);
    if ($transaction) {
      $order = commerce_order_load($transaction->order_id);
      if ($order) {
        if ($status == 'fail') {
          drupal_set_message(t('Payment failed at the payment server. Please review your information and try again.'), 'error');
        }
        drupal_goto(commerce_checkout_order_uri($order));
      }
    }
  }
  drupal_exit();
}

/**
 * Payment transaction load helper.
 *
 * Commerce robokassa 7.x-2.x send transaction GUID as shp_trx_id
 * custom parameter.
 *
 * @param int $inv_id
 *   Robokassa InvId value.
 * @param int $shp_trx_id
 *   Commerce transaction GUID for 7.x-2.x, empty for 7.x-1.x.
 * @param float $amount
 *   Payed order amount for 7.x-1.x compatibility.
 * @param object $payment_method
 *   Payment method object user on transaction creation for
 *   7.x-1.x compatibility.
 *
 * @return object|bool
 *   Commerce payment transaction object or FALSE otherwise.
 */
function _commerce_robokassa_transaction_load($inv_id, $shp_trx_id = FALSE, $amount = 0, $payment_method = FALSE) {
  if (!is_numeric($inv_id)) {
    return FALSE;
  }

  // Transaction key 'order_id' used for 7.x-1.x transactions.
  // Transaction key 'remote_id' used for 7.x-2.x transactions.
  $transaction_key = $shp_trx_id ? 'remote_id' : 'order_id';
  $transaction_key_val = $shp_trx_id ? $shp_trx_id : $inv_id;
  $query = new EntityFieldQuery();
  $query
    ->entityCondition('entity_type', 'commerce_payment_transaction')
    ->propertyCondition($transaction_key, $transaction_key_val)
    ->propertyCondition('payment_method', 'commerce_robokassa')
    ->range(0, 1);
  $result = $query
    ->execute();
  if (!isset($result['commerce_payment_transaction'])) {
    if (!$shp_trx_id) {
      $order = commerce_order_load($inv_id);
      $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
      $currency_code = $order_wrapper->commerce_order_total->currency_code
        ->value();
      $amount = commerce_currency_decimal_to_amount($amount, $currency_code);
      $charge = array(
        'amount' => $amount,
        'currency_code' => $currency_code,
      );
      return commerce_robokassa_transaction($payment_method, $order, $charge);
    }
    return FALSE;
  }
  $transaction_ids = array_keys($result['commerce_payment_transaction']);
  $entities = entity_load('commerce_payment_transaction', $transaction_ids);
  return reset($entities);
}

/**
 * Helper to validate robokassa $_POST data.
 *
 * @param mixed $data
 *   $_POST to be validated.
 * @param mixed $payment_method
 *   Drupal commerce payment method instance passed via url param.
 * @param bool $is_interaction
 *   Fallback call flag.
 *
 * @return bool|mixed
 *   Transaction according to POST data or due.
 */
function _commerce_robokassa_validate_post($data, $payment_method = FALSE, $is_interaction = TRUE) {

  // Exit now if the $_POST was empty.
  if (empty($data)) {
    watchdog('commerce_robokassa', 'Interaction URL accessed with no POST data submitted.', array(), WATCHDOG_WARNING);
    print 'bad data';
    drupal_exit();
  }

  // Exit now if any required keys are not exists in $_POST.
  $required_keys = array(
    'OutSum',
    'InvId',
  );
  if ($is_interaction) {
    $required_keys[] = 'SignatureValue';
  }
  $unavailable_required_keys = array_diff_key(array_flip($required_keys), $data);
  if (!empty($unavailable_required_keys)) {
    watchdog('commerce_robokassa', 'Missing POST keys. POST data: <pre>!data</pre>', array(
      '!data' => print_r($unavailable_required_keys, TRUE),
    ), WATCHDOG_WARNING);
    print "bad data";
    drupal_exit();
  }
  $settings = isset($payment_method['settings']) ? $payment_method['settings'] : commerce_robokassa_default_settings();

  // Exit now if missing Checkout ID.
  if (empty($settings['MrchLogin'])) {
    $info = array(
      '!settings' => print_r($payment_method, 1),
      '!data' => print_r($data, TRUE),
    );
    watchdog('commerce_robokassa !data', 'Missing merchant ID.  POST data: <pre>!data</pre> <pre>!settings</pre>', $info, WATCHDOG_WARNING);
    print 'bad data';
    drupal_exit();
  }
  if ($is_interaction) {
    if ($payment_method) {

      // Robokassa Signature.
      $robo_sign = $data['SignatureValue'];

      // Create own Signature.
      $signature_data = array(
        $data['OutSum'],
        $data['InvId'],
        $settings['pass2'],
      );
      if (isset($data['shp_trx_id'])) {
        $signature_data[] = 'shp_trx_id=' . $data['shp_trx_id'];
      }
      $sign = hash($settings['hash_type'], implode(':', $signature_data));

      // Exit now if missing Signature.
      if (drupal_strtoupper($robo_sign) != drupal_strtoupper($sign)) {
        watchdog('commerce_robokassa', 'Missing Signature.  POST data: !data', array(
          '!data' => print_r($data, TRUE),
        ), WATCHDOG_WARNING);
        print "bad sign";
        drupal_exit();
      }
    }
  }

  // This parameter availability is flag for 7.x.2.x transactions.
  $shp_trx_id = isset($data['shp_trx_id']) ? $data['shp_trx_id'] : FALSE;
  $transaction = _commerce_robokassa_transaction_load($data['InvId'], $shp_trx_id, $data['OutSum'], $payment_method);
  if (!$transaction) {
    watchdog('commerce_robokassa', 'Missing transaction id.  POST data: !data', array(
      '!data' => print_r($data, TRUE),
    ), WATCHDOG_WARNING);
    print 'bad data';
    drupal_exit();
  }
  $amount = commerce_currency_amount_to_decimal($transaction->amount, $transaction->currency_code);
  if ($amount != $data['OutSum']) {
    watchdog('commerce_robokassa', 'Missing transaction id amount.  POST data: !data', array(
      '!data' => print_r($data, TRUE),
    ), WATCHDOG_WARNING);
    print 'bad data';
    drupal_exit();
  }
  return $transaction;
}

/**
 * Page callback: commerce_robokassa/%commerce_robokassa_pm/result.
 *
 * I do not use default payment method redirect_validate  with return urls
 * checkout/' . $order->order_id . '/payment/return/' .
 *       $order->data['payment_redirect_key']
 * and checkout/ ' . $order->order_id . '/payment/back/' .
 *       $order->data['payment_redirect_key']
 * that calls CALLBACK_redirect_validate payment transaction
 * processing and user redirect are combined and robokassa does not support
 * dynamic return urls.
 * Also payment processing for all payment systems require change
 * order\transaction statuses only in interaction\process\ipn callbacks
 * not in success\fail\panding page callbacks.
 *
 * @param mixed $payment_method
 *   Drupal payment method passed via url arg.
 * @param bool $is_result
 *   Fallback call flag.
 */
function commerce_robokassa_result($payment_method, $is_result = TRUE) {

  // Retrieve the SCI from $_POST if the caller did not supply an SCI array.
  // Note that Drupal has already run stripslashes() on the contents of the
  // $_POST array at this point, so we don't need to worry about them.
  $data = $_POST;
  $transaction = _commerce_robokassa_validate_post($data, $payment_method, $is_result);
  $transaction->message_variables = array();
  $transaction->remote_status = '';
  $transaction->payload = $data;
  $order = commerce_order_load($transaction->order_id);
  if ($is_result) {
    $remote_payment_method = isset($data['PaymentMethod']) ? $data['PaymentMethod'] : 'Robokassa';
    $transaction->message_variables = array(
      '!remote_payment_method' => $remote_payment_method,
    );
    $transaction->message = t('Success payment via !remote_payment_method');
    $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
    commerce_payment_redirect_pane_next_page($order);
  }
  else {
    $transaction->message = t('Fail payment via robokassa');
    $transaction->status = COMMERCE_PAYMENT_STATUS_FAILURE;
    commerce_payment_redirect_pane_previous_page($order);
  }
  commerce_payment_transaction_save($transaction);
  if ($is_result) {
    print 'OK' . $transaction->transaction_id;
    drupal_exit();
  }
}

/**
 * Default Robokassa settings getter.
 *
 * @return array
 *   Default settings array.
 */
function commerce_robokassa_default_settings() {
  return array(
    'MrchLogin' => '',
    'pass1' => '',
    'pass2' => '',
    'server' => 'test',
    'hash_type' => 'md5',
    'show_robokassa_fee_message' => TRUE,
    'allowed_currencies' => array(),
  );
}

/**
 * Payment method callback: settings form.
 *
 * @param mixed $settings
 *   Payment method instance settings.
 *
 * @return array
 *   Settings form array.
 */
function commerce_robokassa_settings_form($settings = NULL) {
  $form = array();
  $settings = (array) $settings + commerce_robokassa_default_settings();
  $form['MrchLogin'] = array(
    '#type' => 'textfield',
    '#title' => t('login'),
    '#description' => t('Your robokassa login'),
    '#default_value' => $settings['MrchLogin'],
    '#required' => TRUE,
  );
  $form['pass1'] = array(
    '#type' => 'textfield',
    '#title' => t('First password'),
    '#description' => t('Password 1'),
    '#default_value' => $settings['pass1'],
    '#required' => TRUE,
  );
  $form['pass2'] = array(
    '#type' => 'textfield',
    '#title' => t('Second password'),
    '#description' => t('Password 2'),
    '#default_value' => $settings['pass2'],
    '#required' => TRUE,
  );
  $form['server'] = array(
    '#type' => 'radios',
    '#title' => t('Robokassa server'),
    '#options' => array(
      'test' => t('Test - use for testing.'),
      'live' => t('Live - use for processing real transactions'),
    ),
    '#default_value' => $settings['server'],
    '#required' => TRUE,
  );
  $form['hash_type'] = array(
    '#type' => 'radios',
    '#title' => t('Hash type'),
    '#options' => array(
      'md5' => 'md5',
      'ripemd160' => 'ripemd160',
      'sha1' => 'sha1',
      'sha256' => 'sha256',
      'sha384' => 'sha384',
      'sha512' => 'sha512',
    ),
    '#default_value' => $settings['hash_type'],
    '#required' => TRUE,
  );
  $form['allowed_currencies'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Currencies'),
    '#options' => commerce_robokassa_payment_methods_list($settings),
    '#default_value' => $settings['allowed_currencies'],
  );
  $form['show_robokassa_fee_message'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show robokassa fee message'),
    '#default_value' => $settings['show_robokassa_fee_message'],
  );
  return $form;
}

/**
 * Payment method callback: adds a message and CSS to the submission form.
 */
function commerce_robokassa_submit_form($payment_method, $pane_values, $checkout_pane, $order) {

  // @todo add payways list and show payment gateway price per payways
  $logo_path = drupal_get_path('module', 'commerce_robokassa') . '/images/logo.png';
  $image = array(
    '#theme' => 'image',
    '#path' => $logo_path,
    '#alt' => t('Robokassa'),
    '#title' => t('Robokassa'),
  );
  $form['robokassa_logo'] = array(
    '#markup' => '<div class="commerce-robokassa-logo">' . render($image) . '</div>',
  );
  $selected_currs = !empty($payment_method['settings']['allowed_currencies']) ? array_filter($payment_method['settings']['allowed_currencies']) : array();
  if (!empty($selected_currs)) {
    $currs = commerce_robokassa_payment_methods_list($payment_method['settings']);
    $form['IncCurrLabel'] = array(
      '#type' => 'radios',
      '#title' => t('Pay via'),
      '#options' => array_intersect_key($currs, $selected_currs),
    );
  }
  if ($payment_method['settings']['show_robokassa_fee_message']) {
    $form['robokassa_fee_message'] = array(
      '#markup' => '<div class="commerce-robokassa-fee-message">' . t('In addition to the order amount robokassa fee can be charged.') . '</div>',
    );
  }
  return $form;
}

/**
 * Payment method callback: redirect form, building a robokassa form.
 */
function commerce_robokassa_redirect_form($form, &$form_state, $order, $payment_method) {
  $settings = $payment_method['settings'];

  // Return an error if the enabling action's settings haven't been configured.
  if (empty($settings['MrchLogin']) || empty($settings['pass1']) || empty($settings['pass2'])) {
    drupal_set_message(t('Robokassa is not configured for use.'), 'error');
    return array();
  }
  return commerce_robokassa_order_form($form, $form_state, $order, $payment_method['settings']);
}

/**
 * Builds a robokassa form from an order object.
 */
function commerce_robokassa_order_form($form, &$form_state, $order, $settings) {
  $form['#action'] = commerce_robokassa_server_url($settings['server']);
  $form['#attributes'] = array(
    'name' => 'payment',
    'accept-charset' => 'UTF-8',
  );
  $form['#method'] = 'post';
  $transaction = $order->data['active_transaction'];
  $amount = commerce_currency_amount_to_decimal($transaction->amount, $transaction->currency_code);
  $settings["OutSum"] = $amount;
  $settings["InvId"] = $order->order_id;
  $settings["shp_trx_id"] = $transaction->remote_id;

  // For test transactions.
  if ($settings['server'] == 'test') {
    $settings['IsTest'] = '1';
  }
  $signature_data = array(
    $settings['MrchLogin'],
    $amount,
    $settings["InvId"],
    $settings['pass1'],
    'shp_trx_id=' . $settings["shp_trx_id"],
  );

  // Calculate signature.
  $settings['SignatureValue'] = hash($settings['hash_type'], implode(':', $signature_data));
  $inv_desc_params = array(
    '!orderid' => $order->order_id,
    '!mail' => $order->mail,
  );
  $inv_desc = t('Order ID: !orderid, User mail: !mail', $inv_desc_params);
  $settings['InvDesc'] = truncate_utf8($inv_desc, 100);
  $skiped_settings = array(
    'server',
    'show_robokassa_fee_message',
    'pass1',
    'pass2',
    'hash_type',
    'allowed_currencies',
  );
  if (isset($order->data['commerce_robokassa']['IncCurrLabel'])) {
    $settings['IncCurrLabel'] = $order->data['commerce_robokassa']['IncCurrLabel'];
  }
  foreach ($settings as $name => $value) {
    if (empty($value) || in_array($name, $skiped_settings)) {
      continue;
    }
    $form[$name] = array(
      '#type' => 'hidden',
      '#value' => $value,
    );
  }
  $form['process'] = array(
    '#type' => 'submit',
    '#value' => t('Proceed to robokassa'),
  );
  return $form;
}

/**
 * Payment method callback: submit form submission.
 *
 * Pass transaction to redirect form as value for InvId key and track
 * https://www.drupal.org/project/commerce_partial_payment
 *
 * There is no transaction object in redirect form callback so pass
 * it via $order->data property for proper Robokassa InvId using. Why
 * all payment method callbacks gets only $order object and not active
 * transaction.
 */
function commerce_robokassa_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) {
  $order->data['commerce_robokassa'] = $pane_values;
  $order->data['active_transaction'] = commerce_robokassa_transaction($payment_method, $order, $charge);
}

/**
 * Creates an payment transaction for the specified charge amount.
 *
 * @param mixed $payment_method
 *   Drupal commerce payment method instance.
 * @param object $order
 *   Drupal commerce order object.
 * @param mixed $charge
 *   Amount to be charged.
 *
 * @return mixed
 *   Created transaction.
 */
function commerce_robokassa_transaction($payment_method, $order, $charge) {
  $transaction = commerce_payment_transaction_new('commerce_robokassa', $order->order_id);
  $transaction->instance_id = $payment_method['instance_id'];
  $transaction->amount = $charge['amount'];
  $transaction->currency_code = $charge['currency_code'];
  $transaction->status = COMMERCE_PAYMENT_STATUS_PENDING;
  $transaction->message = 'User redirected to robokassa';

  // Robokassa does not support remote ID in response so populate
  // transaction->remote_id with random int. This value will be used as
  // Robokassa InvId request parameter.
  // We can't use transaction id as value for InvId because possible
  // CSRF attack. Also we can't use hash or any non int value for InvId
  // because documentation requirements.
  $guid = sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535));
  $transaction->remote_id = $guid;
  commerce_payment_transaction_save($transaction);
  return $transaction;
}

/**
 * Returns the URL to the specified Robokassa server.
 *
 * @param string $server
 *   Either test or live indicating which server to get the URL for.
 *
 * @return string
 *   The URL to use to submit requests to the Robokassa server.
 */
function commerce_robokassa_server_url($server) {
  $servers = array(
    'test' => 'https://auth.robokassa.ru/Merchant/Index.aspx',
    'live' => 'https://auth.robokassa.ru/Merchant/Index.aspx',
  );
  return $servers[$server];
}
function commerce_robokassa_payment_methods_list($settings) {
  $url = 'https://auth.robokassa.ru/Merchant/WebService/Service.asmx/GetCurrencies';
  global $language;
  $lang = $language->language == 'ru' ? 'ru' : 'en';
  $data = array(
    'MerchantLogin' => $settings['MrchLogin'],
    'Language' => $lang,
  );
  $full_url = url($url, array(
    'query' => $data,
  ));
  $result = drupal_http_request($full_url);
  $xmlstring = $result->data;
  $xml = simplexml_load_string($xmlstring, "SimpleXMLElement", LIBXML_NOCDATA);
  $json = json_encode($xml);
  $array = json_decode($json, TRUE);
  $ret = array();
  foreach ($array['Groups'] as $groups) {
    foreach ($groups as $group) {
      foreach ($group['Items'] as $item) {
        if (isset($item['@attributes'])) {
          $item = array(
            $item,
          );
        }
        foreach ($item as $currency) {
          $ret[$currency['@attributes']['Label']] = $currency['@attributes']['Name'];
        }
      }
    }
  }
  return $ret;
}

Functions

Namesort descending Description
commerce_robokassa_commerce_payment_method_info Implements hook_commerce_payment_method_info().
commerce_robokassa_default_settings Default Robokassa settings getter.
commerce_robokassa_menu Implements hook_menu().
commerce_robokassa_order_form Builds a robokassa form from an order object.
commerce_robokassa_payment_methods_list
commerce_robokassa_pm_load Payment method loader.
commerce_robokassa_redirect_form Payment method callback: redirect form, building a robokassa form.
commerce_robokassa_result Page callback: commerce_robokassa/%commerce_robokassa_pm/result.
commerce_robokassa_server_url Returns the URL to the specified Robokassa server.
commerce_robokassa_settings_form Payment method callback: settings form.
commerce_robokassa_status Page callback: commerce_robokassa/%commerce_robokassa_pm/%.
commerce_robokassa_submit_form Payment method callback: adds a message and CSS to the submission form.
commerce_robokassa_submit_form_submit Payment method callback: submit form submission.
commerce_robokassa_transaction Creates an payment transaction for the specified charge amount.
_commerce_robokassa_transaction_load Payment transaction load helper.
_commerce_robokassa_validate_post Helper to validate robokassa $_POST data.