
Привет, хабр!
Хочу рассказать про прием WebMoney без перехода на сайт мерчанта Webmoney (merchant.webmoney.ru). Данный метод приема платежей может использоваться, и используется в оффлайн магазинах, небраузерных играх.
Интересно? Добро пожаловать под кат. Будет много php кода)
Как будет выглядеть со стороны клиента:
Для оплаты, ваш клиент должен будет ввести номер мобильного телефона\WMID\email. Далее ему одним из 3-х способов нужно будет подтвердить оплату:
- Подтверждение SMS-кодом, присланным на мобильный телефон клиента
- Подтверждение USSD-запросом, присланным на мобильный телефон клиента
- Оплатой WM-счета, выписанного клиенту. В данный момент оплата ВМ-счетов возможна во всех мобильных (и вообще любых) приложениях по управлению кошельками WebMoney Transfer.
После оплаты нужно будет подтвердить факт оплаты на вашем сайте.
Для оффлайн магазинов наиболее интересны 1-й и 2-й способ подтверждения.
Подготовка:
Для начала выберем способ формирования подписи, их 3:
- Подпись с помощью WMSigner
- Подпись с помощью MD5
- Передача Secret Key
Я рассмотрю только первые 2 варианта, так как при использовании 3-го способа необходимо производить дополнительный проверки (проверка аутентичности соединения по https, во избежание подмены DNS):
1) WMSigner — модуль, формирующий подпись с использованием ключевого файла WM Keeper Classic. При его использовании, рекомендуется размещать конфиг модуля и ключевой файл выше директории сайта, во избежание кражи.
В архиве с модулем содержится README.rus, в котором все прекрасно описано, проблем у вас не должно возникнуть.
2) MD5
Для формирования подписи с помощью md5 предварительной подготовки не требуется, в большинстве языков программирования данная технология хэширования присутствует по умолчанию (в т.ч. и в php, на котором и будем программировать).
Данный метод является предпочтительным, если вы не используете других интерфейсов X, требующих для подписи только WMSigner.
Сама подпись будет представлять собой md5 хэш от склейки
wmid + lmi_payee_purse + lmi_payment_no + lmi_clientnumber + lmi_clientnumber_type + secret_key
WMID + кошелек_продавца + номер_платежа + номер_телефона\email\wmid_клиента + тип предыдущего_поля + секретный ключ
Кодим
В качестве языка программирования для осуществления взаимодействия я выбрал php. Посмотреть и скачать весь код (в одном файле) вы можете тут.
Форма платежа:
<html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> </head> <body> <form method="post" action=""> <select name="purse"> <option value="wmr">WMR</option> <option value="wmz">WMZ</option> </select><br /> Сумма платежа в WMR разделитель через точку:<br /> <input type="text" name="amount" VALUE="12.34"><br /> Примечание к платежу:<br /> <input type="text" name="desc" value="тестовый платеж"><br /> Платим по:<br /> <select name="number_type"> <option value="0">Телефон</option> <option value="1">WMID</option> <option value="2">Email</option> </select> <input type="text" name="number" value="79167777777"><br /> Подтвреждение по:<br /> <select name="confirmation_type"> <option value="1">SMS или оплатой счета</option> <option value="2" selected>USSD или оплатой счета</option> <option value="3">выбор автоматом/ по умолчанию смс</option> <option value="4">только счетом</option> </select><br /> <input type="submit" value="Инициировать платеж"> </form> </body> </html>
Для начала — 2 общие функции.
function wmsign($input) // Подпись с помощью WMSigner { global $_SETTINGS; // Открываем поток к WMSigner, на запись и чтение. $f=proc_open("./signer/wmsigner -i ./signer/wmsigner.ini -k ./signer/".$_SETTINGS['wmid'].".kwm", array( 0=>array("pipe", "r"), 1=>array("pipe", "w"), 2=>array("file", "/tmp/error-output.txt", "a")), $pipes); if(is_resource($f)) { $output=""; // Отсылаем вышесгенерированную строку и закрываем поток на запись. fwrite($pipes[0], $input); fclose($pipes[0]); // Читаем хэш из входного потока while(!feof($pipes['1'])) { $output .= fread($pipes[1], '1024'); } // Закрываем входной поток и закрываем поток WMSigner fclose($pipes['1']); proc_close($f); } return $output; } function post($post,$url="https://merchant.webmoney.ru/conf/xml/XMLTransRequest.asp") // Формирование post запроса { $ch=curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FAILONERROR, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 3); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); $result=curl_exec($ch); curl_close($ch); return $result; }
Проверяем входные параметры, составляем XML запрос к WebMoney, подписываем его и отправляем. Далее проверяем статус запроса, если все без ошибок — выдаем форму на подтверждение платежа. Если СМС — то спрашиваем еще и код, полученный по смс.
$_SETTINGS=array( 'wmid'=>'123456789012', //ваш wmid 'wmr'=>'R123456789012', // R кошелек 'wmz'=>'R123456789012', // Z кошелек 'amount'=>'123.45', // Сумма 'desc'=>'Тестирование X20', // Примечание 'sing_method'=>'sing', // Тип подписи, md5\sing (by default)\secret_key 'secret_key'=>'t4er43d#4' // Секретный ключ, из настроек магазина в мерчанте. ); if($_SERVER['REQUEST_METHOD']=="POST") { // Тут различные проверки входных данных. Если найдена ошибка - присваиваем переменной $error = True; switch($_REQUEST['purse']) // Указываем нужный кошелек, в зависимости от того, в какой валюте платит покупатель. По умолчанию - WMZ { case "wmr": $purse=$_SETTINGS['wmr']; break; default: $purse=$_SETTINGS['wmz']; } if(!(is_numeric($_REQUEST['number_type'])&&$_REQUEST['number_type']<3&&$_REQUEST['number_type']>=0)) // Проверяем тип оплатs { $error=True; $errors['number_type']="Неверный тип"; } if(!(is_numeric($_REQUEST['confirmation_type'])&&$_REQUEST['confirmation_type']<=4&&$_REQUEST['confirmation_type']>0)) // Проверяем тип оплаты { $error=True; $errors['confirmation_type']="Неверный тип"; } $amount=(is_numeric($_POST['amount']))?$_POST['amount']:$_SETTINGS['amount']; if(!$error) { $pay_no=time(); // Формируем запрос $xml=new DomDocument('1.0', 'utf-8'); $xml->formatOutput=true; // Формируем запрос $merchant=$xml->appendChild($xml->createElement('merchant.request')); $merchant->appendChild($xml->createElement('wmid'))->appendChild($xml->createTextNode($_SETTINGS['wmid'])); // WMID $merchant->appendChild($xml->createElement('lmi_payee_purse'))->appendChild($xml->createTextNode($purse)); // Кошелек продавца $merchant->appendChild($xml->createElement('lmi_payment_no'))->appendChild($xml->createTextNode($pay_no)); // Номер платежа в системе учета продавца $merchant->appendChild($xml->createElement('lmi_payment_amount'))->appendChild($xml->createTextNode($amount)); // Сумма платежа в валюте указаного выше кошелька, разделитель - точка. $merchant->appendChild($xml->createElement('lmi_payment_desc'))->appendChild($xml->createTextNode($_SETTINGS['desc'])); // Описание платежа, 255 символов. $merchant->appendChild($xml->createElement('lmi_clientnumber'))->appendChild($xml->createTextNode($_REQUEST['number'])); // Мобильный телефон (в формате 79167777777,380527777777), WMID, email. $merchant->appendChild($xml->createElement('lmi_clientnumber_type'))->appendChild($xml->createTextNode($_REQUEST['number_type'])); // Т��п данных, переданных в lmi_clientnumber (0-телефон, 1-WMID, 2-email) $merchant->appendChild($xml->createElement('lmi_sms_type'))->appendChild($xml->createTextNode($_REQUEST['confirmation_type'])); // Тип подтверждения транзакции (1-смс, 2-USSD, 3-автоматический выбор, из настроек сделанных самим клиентом и анализа предыдущих платежей и т.п., по умолчанию - смс, 4- только WM счет) $merchant->appendChild($xml->createElement('secret_key'))->appendChild($xml->createTextNode('')); // Тип подтверждения транзакции (1-смс, 2-USSD, 3-автоматический выбор, из настроек сделанных самим клиентом и анализа предыдущих платежей и т.п., по умолчанию - смс, 4- только WM счет) // Формируем запрос в соотвествии с выбранным методом подписи switch($_SETTINGS['sign_method']) { case "md5": // md5, тут все понятно, склеиваем поля в нужном порядке и вычисляем md5 хэш от полученной строки $sign = md5($_SETTINGS['wmid'].$purse.$pay_no.$_REQUEST['number'].$_REQUEST['number_type'].$_SETTINGS['secret_key']); $merchant->appendChild($xml->createElement('md5'))->appendChild($xml->createTextNode($sign)); break; case "sign": // Подпись с помощью WMSigner // Склеиваем данные в нужном порядке, из которых далее будем вычислять хэш. $sign = wmsign($_SETTINGS['wmid'].$purse.$pay_no.$_REQUEST['number'].$_REQUEST['number_type']); $merchant->appendChild($xml->createElement('sign'))->appendChild($xml->createTextNode($sign)); break; } // Отсылаем запрос запрос к WebMoney, методом POST $result=post($xml->saveXML(),"https://merchant.webmoney.ru/conf/xml/XMLTransRequest.asp"); // Используем библиотеку dom, т.к. на мы получили XML документ $dom=new domDocument; $dom->loadXML($result); if(!$dom) die("Ошибка парсинга XML"); $data=simplexml_import_dom($dom); // Если произошла ошибка (retval - код ошибки, 0 в случае отсутствия ошибки), то выводим ее описание для клиента. if($data->retval!=0) { echo $data->retval." Error: ".$data->userdesc."\n\n"; } else { ?> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> </head> <body> <form method="post" action=""> <input type="hidden" name="confirmation" value="1"> <?= ($data->realsmstype==1)?'Введите код, полученный в СМС<br />\n':'' ?><input type="<?= ($data->realsmstype==1)?'text':'hidden' ?>" name="code" value="<?= ($data->realsmstype==1)?'':0 ?>"> <input type="hidden" name="account" value="<?=$data->operation['wminvoiceid']?>"> <input type="hidden" name="purse" value="<?= $_REQUEST['purse'] ?>"> <input type="submit" value="Подтвердить оплату."> </form> </body> </html> <? } }
Клиент подтверждает оплату (если СМС — вводит код, пришедший ему, с остальными вариантами — достаточно просто нажать на кнопку), и мы должны уведомить о оплате WebMoney. Иначе денег нам не видать.
// Здесь валидация, можете посмотреть как реализовано в предыдущем примере. if(!$error) { $xml=new DomDocument('1.0','utf-8'); $xml->formatOutput = true; // Формируем запрос $merchant = $xml->appendChild($xml->createElement('merchant.request')); $merchant->appendChild($xml->createElement('wmid'))->appendChild($xml->createTextNode($_SETTINGS['wmid'])); // WMID $merchant->appendChild($xml->createElement('lmi_payee_purse'))->appendChild($xml->createTextNode($purse)); // Кошелек продавца $merchant->appendChild($xml->createElement('lmi_clientnumber_code'))->appendChild($xml->createTextNode($_REQUEST['code'])); // Цифровой код, полученный от клиента (WM счет\USSD = 0, -1 отмена счета) $merchant->appendChild($xml->createElement('lmi_wminvoiceid'))->appendChild($xml->createTextNode($_REQUEST['account'])); // Номер счета, полученный из предыдущей операции switch($_SETTINGS['sign_method']) { case "md5": // md5, тут все понятно, склеиваем поля в нужном порядке и вычисляем md5 хэш от полученной строки $sign=md5($_SETTINGS['wmid'].$purse.$_REQUEST['account'].$_REQUEST['code'].$_SETTINGS['secret_key']); $merchant->appendChild($xml->createElement('md5'))->appendChild($xml->createTextNode($sign)); break; case "sign": // Подпись с помощью WMSigner // Склеиваем данные в нужном порядке, из которых далее будем вычислять хэш. $sign=wmsign($_SETTINGS['wmid'].$purse.$_REQUEST['account'].$_REQUEST['code']); $merchant->appendChild($xml->createElement('sign'))->appendChild($xml->createTextNode($sign)); break; } // Отсылаем запрос запрос к WebMoney, методом POST $result=post($xml->saveXML(),"https://merchant.webmoney.ru/conf/xml/XMLTransConfirm.asp"); // Используем библиотеку dom, т.к. на мы получили XML документ $dom=new domDocument; $dom->loadXML($result); if(!$dom) die("Ошибка парсинга XML"); $data=simplexml_import_dom($dom); // Если произошла ошибка (retval - код ошибки, 0 в случае отсутствия ошибки), то выводим ее описание для клиента. if($data->retval!=0) { echo "Error№".$data->retval.": ".$data->userdesc."\n\n"; ?> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> </head> <body> <form method="post" action=""> <input type="hidden" name="confirmation" value="1"> <?=(!$_POST['code'])?'Введите код, полученный в СМС<br />':''?> <input type="<?=(!$_POST['code'])?'text':'hidden'?>" name="code" value="<?=$_POST['code'];?>"> <input type="hidden" name="account" value="<?=$_POST['account']?>"> <input type="hidden" name="purse" value="<?= $_REQUEST['purse'] ?>"> <input type="submit" value="Подтвердить оплату."> </form> </body> </html> <? } else { // Оплата скорей всего была принята. echo "Оплата была принята"; } }
Результаты тестирования
Найден указанный Вами ВМ-идентификатор, но на его кошельке нет достаточного для оплаты количества средств
Сообщение от WebMoney Transfer: в данный момент Ваш продавец приостановил прием платежей, попробуйте пожалуйста позднее.
Проблемы решены. Причина в фиксированной комиссии, невозможности оплачивать с кошельков, зарегистрированных в мерчанте и не совсем верной логике. Если выбрать способ оплаты — WM-счетом, то будет проверяться наличие на кошельке суммы платежа+0,8% комиссии+фиксированной комиссии. Хотя фиксированная комиссия на WM-счет не распространяется.
Ссылки по теме
Описание интерфейса X20
WMSigner
Ролик, демонстрирующий оплату через X20
Ролик, демонстрирующий оплату через X20 в оффлайн магазинах
Пример реализации на сайте мерчанта
UPD При подтверждении оплаты через USSD\SMS взимается дополнительная фиксированная комиссия — 0.9 WMR, 0.04 WMZ, 0.03 WME, 0.25 WMU.
