commerce_robokassa.module in Commerce robokassa 7.2
Same filename and directory in other branches
Drupal Commerce Robokassa payment method.
This module contains basic integration with Robokassa for Drupal Commerce.
File
commerce_robokassa.moduleView 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
Name | 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. |