
Привет, хабр!
Хочу рассказать про прием 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.