Тема: Как создать свой модуль оплаты в ReadyScript? Пример Interkassa
Всем привет.
Довольно часто, приходится слышать от украинских покупателей, что хотелось бы кроме Robokassa ещё видеть у себя в способах оплаты Interkass-у. Недавно мы это реализовали и сделали отдельным модулем. Но в большинстве случаев люди немножко не понимают, как сделать им тоже самое для своего сайта, если заказчик им заказал интеграцию с каким-либо сервисом. Ну вот сегодня настал тот день, когда я раскрою все тайны и всё станет я думаю яснее.
Тем кому лень всё изучать даю прямую ссылку на готовый модуль
Скачать готовый модуль интеграции интеркассы для ReadyScript - распаковать в папку /modules/. В Веб-сайт->Настройка модулей сбросить кэш, перезагрузить страницу и установить модуль.
Задача сделать свой собственный дополнительный модуль, который позволит проводить платежи через Interkassa.
Вот как это можно сделать?
Смотрим документацию здесь по этому сервису. Всё довольно просто.
Предварительно для разработки нужно будет включить ошибки. Смотрим сюда.
Шаг 1. Сделаем свой собственный модуль.
В папке modules создаём папку с названием Вашего модуля(interkassa в нашем случае) и в ней создаём папку config
В этой папке создаём файл file.inc.php с данными описывающими модуль и пишем следующее содержимое:
<?php
namespace Interkassa\Config;
use \RS\Orm\Type;
/**
* Конфигурационный файл модуля
*/
class File extends \RS\Orm\ConfigObject
{
/**
* Возвращает значения свойств модуля по-умолчанию
*
* @return array
*/
public static function getDefaultValues()
{
return array(
'name' => t('Интеркасса'), //Название нашего модуля
'description' => t('Платёжная система - Интеркасса'), //Описание модуля
'version' => '1.0.0.0', //Версия вашего модуля
'author' => 'Сведения об авторе', //Сведения об авторе
);
}
}
После чего заходим в админ панель в "Веб-сайт" -> "Настройка модулей" и на этой странице сбрасываем кэш.
Появится наш модуль с названием "Интеркасса". Заходим в наш модуль. И нажимаем "установить модуль".
Всё модуль в системе зарегистрирован.
Шаг 2. Создание файла с хуком для добавления своего типа оплаты
В системе существует так называемая система хуков, которая позволяет встраиваться в различные места системы и выполнять свои функции. Документация по хукам находится здесь. Всё основано на том, что нужно указать в файле handlers свой нужный символьный идентификатор хука и потом создать public static function c таким же названием, только вырезав из него символы "." (точка) и "-" тире. Идём на страницу этой документации и спускаемся в низ до таблицы. Для нашего случая нам нужен хук payment.gettypes.
Т.е. нам нужно пропиcать bind этого алиаса хука и создать функцию с таким же названием вырезав лишнее.
Итак алиас будет выглядеть так:
function init()
{
$this
->bind('payment.gettypes');
}
и функция соотвественно:
/**
* Добавляем новый вид оплаты - Интеркасса
*
* @param array $list - массив уже существующих типов оплаты
* @return array
*/
public static function paymentGetTypes($list)
Почему $list и как узнать что передаётся в хук? Смотрим внимательно в документацию по хукам в колонку "Тип параметра" напротив строки с "payment.gettypes".
В функцию paymentGetTypes поступает массив с типами оплаты со всей системы, а мы его просто дополним, добавив новый элемент в массив. Элементом массива будет класс который мы создадим и именно он будет обрабатывать все запросы от Interkassa.
Итак cоздаём файл handlers.inc.php в папке config. Пишем в него следующее содержимое:
<?php
namespace Interkassa\Config;
use \RS\Orm\Type as OrmType;
/**
* Класс предназначен для объявления событий, которые будет прослушивать данный модуль и обработчиков этих событий.
*/
class Handlers extends \RS\Event\HandlerAbstract
{
function init()
{
$this
->bind('payment.gettypes');
}
/**
* Добавляем новый вид оплаты - Интеркасса
*
* @param array $list - массив уже существующих типов оплаты, который собирается со всей системы
* @return array
*/
public static function paymentGetTypes($list)
{
$list[] = new \Interkassa\Model\PaymentType\Interkassa(); //Интеркасса. Класс который мы создадим и будет обрабатывать все запросы к интеркассе.
return $list;
}
}
Шаг 3. Создаём класс для интеркассы, который хранит все сведения.
Все классы модулей оплат вшитые в дистрибутив находятся в /modules/shop/model/paymenttype/.
Проще всего создавать свой класс оплаты на основе уже имеющихся в этой папке. Например для interkassa я переделывал из robokassa, т.к. алгоритм примерно схож.
Итак создаём файл в папке с нашим модулем по пути:
/modules/interkassa/model/paymenttype/interkassa.inc.php
Имя класса будет Interkassa, которая будет наследником абстрактного класса оплат \Shop\Model\PaymentType\AbstractType
<?php
namespace Interkassa\Model\PaymentType;
use \RS\Orm\Type;
use \Shop\Model\Orm\Transaction;
/**
* Способ оплаты - Interkassa
*/
class Interkassa extends \Shop\Model\PaymentType\AbstractType
Пропишем в соответствии с документацией interkassa наши константы
const
API_URL = "https://sci.interkassa.com/", //URL Api для взаимодействия
PAYWAYS_URL = "https://api.interkassa.com/v1/paysystem-input-payway", //URL Api для получения полниго списка типов оплат
IP_DIAPOZON = "85.10.225."; //Диапозон IP адресов интеркассы с которых должны приходить запросы
Разберём следующие стандартные функции создающего нами класса:
Функция getTitle() должна возвращать имя Вашего способа оплаты в Вашем модуле.
/**
* Возвращает название расчетного модуля (типа доставки)
*
* @return string
*/
function getTitle()
{
return t('Интеркасса');
}
Функция getDescription() должна возвращать описание вашего типа оплаты
/**
* Возвращает описание типа оплаты. Возможен HTML
*
* @return string
*/
function getDescription()
{
return t('Оплата через агрегатор платежей "Интеркасса"');
}
Функция getShortName() должна возвращать короткое имя (Именно на латиницей!) Вашего типа оплаты. Это нужно для того чтобы переключатся между типами оплаты и именно это название пойдёт как идентификатор Вашего типа оплаты внутри системы. Должно быть уникально по сравнению с теми что уже есть в системе.
/**
* Возвращает идентификатор данного типа оплаты. (только англ. буквы)
*
* @return string
*/
function getShortName()
{
return 'interkassa';
}
Функция isPostQuery() должна возвращать булевое значение. Если true, то пользователь будет перенаправлен на систему оплаты POST запросом. Если эту функцию не указывать, то пользователь будет переходить GET запросом на страницу оплаты.
Зачем это нужно? Дело вот в чем.
Когда пользователь совершает оплату и переходит на последнюю страницу (Завершение заказа), то ему предоставляется кнопка, которая либо возвращает пользователя на главную страницу либо даёт сформированную ссылку на систему оплаты. Эта функция отвечает как раз каким способом(GET или POST запросом) пользователь перейдёт на сайт системы оплаты.
В нашем случаем нам нужно POST поэтому:
/**
* Отправка данных с помощью POST?
*
*/
function isPostQuery()
{
return true;
}
Функция getFormObject() должна возвращать дополнительную форму для способа оплаты. Дело в том, что в способе оплаты по умолчанию уже есть набор постоянных полей. Но в соответствии с документациями часто требуется указать дополнительные параметры которые должен заполнить. Например Api ключ и Api пароль как в нашем случае. Поэтому при выборе класса оплаты будет ajax запросом подгружена форма с полями, которые мы укажем.
А также бывает часто нужно вывести дополнительную информацию для пользователя, чтобы он её вставил в настройках. Например для нашего случая в настройках интеркассы нужно ввести url которые будут обрабатывать приходящие запросы(будет рассмотрено ниже).
В этой функции реализуется возврат массива ORM объектов полей. Подробнее об этих полях можно почитать в документации.
Итак на основе документации по интеркассе получим следующее:
/**
* Возвращает ORM объект для генерации формы или null
*
* @return \RS\Orm\FormObject | null
*/
function getFormObject()
{
$properties = new \RS\Orm\PropertyIterator(array(
'ik_co_id' => new Type\String(array(
'maxLength' => 255,
'description' => t('Checkout ID - индетификатор кассы'),
)),
'secret_key' => new Type\String(array(
'description' => t('Секретный ключ'),
'hint' => t('Указан на странице Вашей кассы'),
'template' => '%interkassa%/form/payment/interkassa/secret_key.tpl'
)),
'test_key' => new Type\String(array(
'description' => t('Тестовый ключ'),
'hint' => t('Указан на странице Вашей кассы')
)),
'language' => new Type\String(array(
'maxLength' => 5,
'description' => t('Язык интерфейса'),
'listFromArray' => array(array(
0 => t('Определяется Интеркассой'),
'ru' => t('Русский'),
'ua' => t('Украинский'),
'en' => t('Английский'),
))
)),
'ik_pw_via' => new Type\String(array(
'maxLength' => 255,
'description' => t('Тип оплаты:'),
'default' => 0,
'list' => array(array($this,'getPayways')),
)),
'__help__' => new Type\Mixed(array(
'description' => t(''),
'visible' => true,
'template' => '%interkassa%/form/payment/interkassa/help.tpl'
)),
));
return new \RS\Orm\FormObject($properties);
}
Поле __help__ как раз служит для получения отрендеренного шаблона с данными для вставки в настройки системы оплаты (Шаблон в ключе 'template').
%interkassa% - означает, что путь для поиска шаблона будет строится из папки с шаблонами модуля interkassa, а именно - /modules/interkassa/view/
Также приведём код в файлах шаблонов которые нам понадобятся для полей где указан ключ 'template'.
В этом шаблоне будут указаны 3 ссылки для вставки в настройки интеркассы. Url надо формировать в соотвествии с маршрутами в системе для оплаты.
Маршруты для этих урлов можно посмотреть в /modules/shop/config/handlers.inc.php в методе getRoutes.
В шаблоне сгенерировать урл можно конструкцией
{$router->getUrl('shop-front-onlinepay', [Act=>result, PaymentType=>$payment_type->getShortName()], true)}
//в итоге будет
//http://ВАШДОМЕН/onlinepay/interkassa/result/
//т.е. http://ВАШДОМЕН/onlinepay/Ваш класс/метод/
/modules/interkassa/view/form/payment/interkassa/help.tpl:
<h3>Настройка аккаунта Интеркасса</h3>
<p>Укажите алгоритм подписи у Вашей кассы - <b>MD5</b></p>
<p>Укажите эти URL в настройках Вашей кассы:</p>
<b>URL ожидания проведения платежа: </b><br>
<a target="_blank" href="{$router->getUrl('shop-front-onlinepay', [Act=>result, PaymentType=>$payment_type->getShortName()], true)}">
{$router->getUrl('shop-front-onlinepay', [Act=>result, PaymentType=>$payment_type->getShortName()], true)}
</a>
<br><br>
<b>URL взаимодействия платежа: </b><br>
<a target="_blank" href="{$router->getUrl('shop-front-onlinepay', [Act=>result, PaymentType=>$payment_type->getShortName()], true)}">
{$router->getUrl('shop-front-onlinepay', [Act=>result, PaymentType=>$payment_type->getShortName()], true)}
</a>
<br><br>
<b>URL успешной оплаты: </b><br>
<a target="_blank" href="{$router->getUrl('shop-front-onlinepay', [Act=>success, PaymentType=>$payment_type->getShortName()], true)}">
{$router->getUrl('shop-front-onlinepay', [Act=>success, PaymentType=>$payment_type->getShortName()], true)}
</a>
<br><br>
<b>URL неуспешной оплаты: </b><br>
<a target="_blank" href="{$router->getUrl('shop-front-onlinepay', [Act=>fail, PaymentType=>$payment_type->getShortName()], true)}">
{$router->getUrl('shop-front-onlinepay', [Act=>fail, PaymentType=>$payment_type->getShortName()], true)}
</a>
<p>Укажите методы для всех URL - <b>POST</b></p>
/modules/interkassa/view/form/payment/interkassa/secret_key.tpl:
Это шаблон интересен тем, что здесь используется POST AJAX запрос в зависимости значения поля с секретным ключом и возвращаемый результат вставляется в соответствующие поле(выпадающий список).
Зачем? Дело в том, что в соответсвии с документацией пользователь при построеннии запроса на оплату может указывать канал оплаты, либо не указывать его и тогда ему будет доступен выбоор всех каналов оплат в интеркассе.
Для построения такого запроса нам нужно сформированить запрос на тотже url где мы находимся в админке но с параметром ?do=userAct.
Получить такой url можно так:
{$router->getAdminUrl('userAct')}
Также надо передать следующие параметры:
userAct - Метод который будет выполнятся в нашем классе и вернёт информацию (В нашем случае staticGetPaywaysByCheckoutID)
paymentObj - название латиницей класса типа оплаты в системе
interkassa - название латиницей папки вашего модуля
params - объект с параметрами дополнительными для передачи в наш статический метод
Обработка приходящего запроса будет в методе actionUserAct() в файле:
/modules/shop/controller/admin/paymentctrl.inc.php
В итоге выглядит так:
data : {
'userAct' : 'staticGetPaywaysByCheckoutID',
'paymentObj' : 'interkassa',
'module' : 'interkassa',
'params' :{
'checkout_id' : $(".formbox input[name='data[ik_co_id]']").val(),
'secret_key' : val
}
}
Полный текст файла
<div id="interkassaSecretKey" data-url="{$router->getAdminUrl('userAct')}" data-default-title="{t('-Все типы оплат-')}">
{include file=$field->getOriginalTemplate()}
</div>
<script type="text/javascript">
$.allReady(function() {
var aj;
/**
* Назначаем действие на текстовое поле с id кассы, если будет введен текст,
* то шлём запрос на сервер, чтобы он нам вернул нам доступные пути оплаты через интеркассу
*
*/
$("#interkassaSecretKey input[name='data[secret_key]']").off('keyup').on('keyup',function(){
var val = $(this).val();
var select = $("select[name='data[ik_pw_via]']");
if (val.length<16){ //Если символов не хватаем в списке будет пункт "Все"
select.empty().append('<option value="0">'+ $("#interkassaSecretKey").data('defaultTitle')+'</option>');
}else{ //Если есть ключ пробуем запросить пути оплаты для данной кассы
if (typeof(aj)=='object'){
aj.abort();
}
aj = $.ajaxQuery({
type : "POST",
url : $("#interkassaSecretKey").data('url'),
data : {
'userAct' : 'staticGetPaywaysByCheckoutID',
'paymentObj' : 'interkassa',
'module' : 'interkassa',
'params' :{
'checkout_id' : $(".formbox input[name='data[ik_co_id]']").val(),
'secret_key' : val
}
},
dataType : "json",
success : function(responce){
select.empty();
if (responce['data']['list'].length>0){
//Добавим возможные значения
$(responce['data']['list']).each(function(i){
select.append('<option value="'+responce['data']['list'][i]['key']+'">'+responce['data']['list'][i]['value']+'</option>');
});
}
}
});
}
});
});
</script>
А метод в нашем классе будет таким:
/**
* Получает возможные пути оплаты для привязанной к вам кассы
*
* @param array|string $data - id кассы или массив параметров, среди которых, должен быть ключ "checkout_id"
*/
public static function staticGetPaywaysByCheckoutID($data)
{
$_this = new self();
$_this->setOption('secret_key',$data['secret_key']);
$_this->setOption('ik_co_id',$data['checkout_id']);
$data = $_this->getPayways();
$payways = array();
foreach ($data as $key=>$value){
$payways[] = array(
'key' => $key,
'value' => $value,
);
}
return array('list' => $payways);
}
Функция canOnlinePay() должна возвращать булевое значение. Если true, то значит что это тип оплаты через внешнюю систему оплаты и должен на последнем шаге заказа оправить пользователя на url оплаты через систему оплаты. Если false, то этот тип оплаты не перенаправляет пользователя на систему оплаты, а перенаправляет на главную станицу. При этом наш класс оплаты формирует документы для обработки или отправки. Примерами служит тип оплаты "Счёт" и "Квитанция ПД4" для оплаты. Тем кому нужна реализация именно таких типов оплат без перехода на страницу внешних сервисов смотрите файлы:
/modules/shop/model/paymenttype/bill.inc.php и /modules/shop/model/paymenttype/formpd4.inc.php
/**
* Возвращает true, если данный тип поддерживает проведение платежа через интернет
*
* @return bool
*/
function canOnlinePay()
{
return true;
}
Шаг 4. Создаём основные функции в нашем классе для обмена информации.
Есть 5 основных функции для нашего класса, которые являются обязательными. Дело в том, что в процессе оплаты через онлайн сервисы оплаты требуется обмен информацией с системой оплаты и Вашим сайтом. Обмен происходит по определённым url в системе. Всего из 3:
1. url успешной оплаты
2. url не успешной оплаты
3. url который нужен для обмена информацией с системой онлайн оплаты и сайтом. Например, сведения, что оплата прошла успешно и нужно зарегистрировать изменения или нужно подтверждение от нашего сайта, которое скажет что оплатить данный счёт можно. Всё зависит от API системы онлайн оплаты к которой подключаемся.
В процессе обмена информацией между сайтом и системой онлайн оплаты обязательно должен передаваться id транзакции, который система онлайн оплаты возвращает. Он может быть передан как дополнительным параметром от системы Вашему сайту, либо одном из основным, в зависимости от API системы онлайн оплаты к которой подключаемся. А мы в своём случае должны получить данный id транзакции чтобы понять, какая сейчас транзакции происходит.
Идём по порядку по функциям:
1. getTransactionIdFromRequest(\RS\Http\Request $request)
При создании транзакции на онлайн оплату(происходит всегда, когда пользователь переходит на сайт оплаты нажимая кнопку завершить заказ). Транзакции присваивается уникальный идентификатор. Как его получить будет рассмотрено ниже. Удалять транзакции из базы вручную нельзя категорически, т.к. каждая новая последующая транзакция обладает своей уникальной подписью, которая основывается на всех предыдущих подписях транзакций. Если всё же вы так сделали, то придётся удалить все транзакции из системы в соответствующей таблице.
Эта функция(getTransactionIdFromRequest) служит для того, чтобы указать в каком параметре во входящих запросах от системы онлайн оплаты нам пришёл id транзации на оплату. В нашем случае придёт запрос со значением параметра с именем "ik_pm_no".
/**
* Возвращает ID заказа исходя из REQUEST-параметров соотвествующего типа оплаты
* Используется только для Online-платежей
*
* @return mixed
*/
function getTransactionIdFromRequest(\RS\Http\Request $request)
{
return $request->request('ik_pm_no', TYPE_INTEGER, false);
}
2. getPayUrl(\Shop\Model\Orm\Transaction $transaction)
Эта функция подготавливает ссылку с параметрами, по которой перейдёт пользователь на систему оплаты для совершения платежа. Ссылка будет присвоена кнопке "Завершить заказ" на последней стадии оформления заказа.
В общем главная функция этой функции это составить правильный урл с параметрами для перенаправления пользователя на url оплаты.
Получить id транзакции можно так:
$inv_id = $transaction->id; //Получаем id транзакции
Также для получения параметров из дополнительных полей, которые мы создавали в функции getFormObject() можно делать так:
$field = $this->getOption('Имя поля',Значение поля по умолчанию, если не задано(не обязательный параметр));
//Т.е. получение значения поля формы с именем "ik_co_id"
$params['ik_co_id'] = $this->getOption('ik_co_id');
//Или с параметром по умолчанию
$way = $this->getOption('ik_pw_via',0); //Тип канала оплаты
Если нам нужен GET запрос, то нужно сформировать просто строку с url. Если это POST запрос для системы, то мы возвращаем url API системы онлайн оплаты. И с помощью метода $this->addPostParams($params); добавим параметры для POST запроса.
В итоге для нашего POST запроса будет так:
/**
* Возвращает URL для перехода на сайт сервиса оплаты
*
* @param \Shop\Model\Orm\Transaction $transaction - ORM объект транзакции
* @return string
*/
function getPayUrl(\Shop\Model\Orm\Transaction $transaction)
{
$order = $transaction->getOrder(); //Данные о заказе
/**
* @var mixed
*/
$user = $order->getUser(); //Пользователь который должен оплатить
$inv_id = $transaction->id;
$out_summ = round($transaction->cost, 2);
$in_cur = $this->getPaymentCurrency();
$way = $this->getOption('ik_pw_via',0); //Тип канала оплаты
$params = array();
$params['ik_co_id'] = $this->getOption('ik_co_id'); //ID кассы
$params['ik_pm_no'] = $inv_id;
$params['ik_cur'] = $in_cur;
$params['ik_am'] = $out_summ;
$params['ik_am_t'] = "invoice"; //Выбор способа оплаты будет на стороне интеркассы
$params['ik_desc'] = t("Оплата заказа №").$order['order_num'];
if ( $language = $this->getLanguage() ) {
$params['ik_loc'] = $language;
}
$params['ik_cli'] = $user['e_mail']; //Контакты покупателя
//принудительно указываем метод post и url
$router = \RS\Router\Manager::obj();
//Обработать process|payway|payways|payways_calc
$params['ik_act'] = 'payways';
if ($way){ //Если нужно отобразить все типы оплат
$params['ik_act'] = 'payway';
$params['ik_pw_via'] = $way;
}
$params['ik_int'] = 'web'; //Формат ответов json|web
$params['ik_sign'] = $this->getParamsSign($params);
$this->addPostParams($params); //Добавляем параметры для POST запроса
return self::API_URL; //url пост запроса
}
3. onResult(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
Эта фукция самая пожалуй главная, на неё должны приходить все запросы на обработку от системы. Например в нашем случае мы при помощи неё проверяем пришедшие данные из интеркассы, подтверждаем воможность оплаты данного платежа и проверяем статус оплаты. См. документацию интекассы
В итоге имеем:
/**
* Обработка запросов от интеркассы
*
* @param \Shop\Model\Orm\Transaction $transaction - объект транзакции
* @param \RS\Http\Request $request - объект запросов
* @return string
*/
function onResult(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
{
if (!$this->checkIPsDiapozon()){
$exception = new \Shop\Model\PaymentType\ResultException(t('Неправильный диапозон IP адресов'));
$exception->setResponse('Wrong IPs');
throw $exception;
}
if (!$this->checkMainParams($transaction, $request)){
$exception = new \Shop\Model\PaymentType\ResultException(t('Главные параметры указаные в настройках интеркассы не прошли проверку'));
$exception->setResponse('Wrong main params');
throw $exception;
}
//Смотрим текущий статус
$status = $request->request('ik_inv_st',TYPE_STRING,0);
switch($status){
case "success":
return 'OK'.$transaction->id;
break;
case "process":
$exception = new \Shop\Model\PaymentType\ResultException(t('Неудачно совершённый платёж'));
$exception->setResponse('Payment status progress');
$exception->setUpdateTransaction(false);
throw $exception;
break;
case "waitAccept": //Если долгое ожидание проведения платежа, то пользователь перенаправляется к нам
\RS\Application\Application::getInstance()->headers->addHeader('Location',$request->getSelfAbsoluteHost());
break;
case "fail":
default:
$exception = new \Shop\Model\PaymentType\ResultException(t('Неудачно совершённый платёж'));
$exception->setResponse('Payment failed');
throw $exception;
break;
}
return 'OK'.$transaction->id;
}
В методе присутвует вызов исключений \Shop\Model\PaymentType\ResultException он очень важен т.к. он позволяет генерировать исключения, чтобы они сохранялись в системе, а также отдавали информацию для платёжной системе об итоге обработки их запроса.
Рассмотрим на прямом примере:
//Создаём класс исключения и в параметре передаём какой текст сохранится в системе у этой транзакции
$exception = new \Shop\Model\PaymentType\ResultException(t('Неудачно совершённый платёж'));
//Генерируем ответ на запрос для интеркассы
$exception->setResponse('Payment status progress');
//Устанавливаем флаг того, что статус транзакции не обновлять при сбошенном обновлении иначе платёж не удастся
$exception->setUpdateTransaction(false);
//Бросаем исключение
throw $exception;
Теперь поясню по результатам, которые надо возвращать с помощью этой функции(onResult):
1. В этом методе если будет брошено исключение, то статус транзакции установится в fail(неуспешно) и для нового платежа надо будет генерировать новое обращение к системе. Нужно, чтобы поставить статус что оплата провалилась. В интеркассу улетит сообщение $exception->setResponse
2. Если будет брошено исключение, но при этом будет использоваться функция $exception->setUpdateTransaction(false); и в аргумете будет false, то будет брошено исключение не влияющее на транзакцию, повторное обращение к сайту с этим платежом будет возможно и статус у транзакции не изменится. Например нужно, чтобы просто вернуть информацию о платеже системе онлайн оплаты, но статус транзакции при этом ещё не должен быть изменён.
3. Если исключение не будет брошено, а просто будет возвращён текст:
//Например
return 'OK'.$transaction->id;
То в систему онлайн оплаты улетит OKid транзакции и статут транзакции установится в успешный.
4. onFail(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
Эта функция не обязательна, т.к. имеется в родительском классе. По сути она показывает страницу с не успешным статусом оплаты на Вашем сайте. Для нашего случая внутри функции устанавливается ещё и статус транзакции.
/**
* Вызывается при открытии страницы неуспешного проведения платежа
* Используется только для Online-платежей
*
* @param \Shop\Model\Orm\Transaction $transaction
* @param \RS\Http\Request $request
* @return void
*/
function onFail(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
{
$transaction['status'] = $transaction::STATUS_FAIL;
$transaction->update();
}
5. onSuccess(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
Эта функция не обязательна, т.к. имеется в родительском классе. По сути она показывает страницу с успешным статусом оплаты на Вашем сайте.
Итого получаем такой файл:
<?php
namespace Interkassa\Model\PaymentType;
use \RS\Orm\Type;
use \Shop\Model\Orm\Transaction;
/**
* Способ оплаты - Interkassa
*/
class Interkassa extends \Shop\Model\PaymentType\AbstractType
{
const
API_URL = "https://sci.interkassa.com/", //URL Api для взаимодействия
PAYWAYS_URL = "https://api.interkassa.com/v1/paysystem-input-payway", //URL Api для получения полниго списка типов оплат
IP_DIAPOZON = "85.10.225."; //Диапозон IP адресов интеркассы с которых должны приходить запросы
/**
* Возвращает название расчетного модуля (типа доставки)
*
* @return string
*/
function getTitle()
{
return t('Интеркасса');
}
/**
* Возвращает описание типа оплаты. Возможен HTML
*
* @return string
*/
function getDescription()
{
return t('Оплата через агрегатор платежей "Интеркасса"');
}
/**
* Возвращает идентификатор данного типа оплаты. (только англ. буквы)
*
* @return string
*/
function getShortName()
{
return 'interkassa';
}
/**
* Отправка данных с помощью POST?
*
*/
function isPostQuery()
{
return true;
}
/**
* Возвращает ORM объект для генерации формы или null
*
* @return \RS\Orm\FormObject | null
*/
function getFormObject()
{
$properties = new \RS\Orm\PropertyIterator(array(
'ik_co_id' => new Type\String(array(
'maxLength' => 255,
'description' => t('Checkout ID - индетификатор кассы'),
)),
'secret_key' => new Type\String(array(
'description' => t('Секретный ключ'),
'hint' => t('Указан на странице Вашей кассы'),
'template' => '%interkassa%/form/payment/interkassa/secret_key.tpl'
)),
'test_key' => new Type\String(array(
'description' => t('Тестовый ключ'),
'hint' => t('Указан на странице Вашей кассы')
)),
'language' => new Type\String(array(
'maxLength' => 5,
'description' => t('Язык интерфейса'),
'listFromArray' => array(array(
0 => t('Определяется Интеркассой'),
'ru' => t('Русский'),
'ua' => t('Украинский'),
'en' => t('Английский'),
))
)),
'ik_pw_via' => new Type\String(array(
'maxLength' => 255,
'description' => t('Тип оплаты:'),
'default' => 0,
'list' => array(array($this,'getPayways')),
)),
'__help__' => new Type\Mixed(array(
'description' => t(''),
'visible' => true,
'template' => '%interkassa%/form/payment/interkassa/help.tpl'
)),
));
return new \RS\Orm\FormObject($properties);
}
/**
* Возвращает true, если данный тип поддерживает проведение платежа через интернет
*
* @return bool
*/
function canOnlinePay()
{
return true;
}
/**
* Возвращает URL для перехода на сайт сервиса оплаты
*
* @param Transaction $transaction - ORM объект транзакции
* @return string
*/
function getPayUrl(\Shop\Model\Orm\Transaction $transaction)
{
$order = $transaction->getOrder(); //Данные о заказе
/**
* @var mixed
*/
$user = $order->getUser(); //Пользователь который должен оплатить
$inv_id = $transaction->id;
$out_summ = round($transaction->cost, 2);
$in_cur = $this->getPaymentCurrency();
$way = $this->getOption('ik_pw_via',0); //Тип канала оплаты
$params = array();
$params['ik_co_id'] = $this->getOption('ik_co_id'); //ID кассы
$params['ik_pm_no'] = $inv_id;
$params['ik_cur'] = $in_cur;
$params['ik_am'] = $out_summ;
$params['ik_am_t'] = "invoice"; //Выбор способа оплаты будет на стороне интеркассы
$params['ik_desc'] = t("Оплата заказа №").$order['order_num'];
if ( $language = $this->getLanguage() ) {
$params['ik_loc'] = $language;
}
$params['ik_cli'] = $user['e_mail']; //Контакты покупателя
//принудительно указываем метод post и url
$router = \RS\Router\Manager::obj();
//Обработать process|payway|payways|payways_calc
$params['ik_act'] = 'payways';
if ($way){ //Если нужно отобразить все типы оплат
$params['ik_act'] = 'payway';
$params['ik_pw_via'] = $way;
}
$params['ik_int'] = 'web'; //Формат ответов json|web
$params['ik_sign'] = $this->getParamsSign($params);
$this->addPostParams($params); //Добавляем параметры для POST запроса
return self::API_URL; //url пост запроса
}
/**
* Получает все варианты оплаты
*
* @param string $checkout_id - id кассы
*/
function getPayways()
{
$params = array();
$params['ik_co_id'] = $this->getOption('ik_co_id','');
$params['ik_pm_no'] = 'PAYWAYS_1';
$params['ik_am'] = 1;
$params['ik_desc'] = 'Look up my payways';
$params['ik_act'] = 'payways';
$params['ik_int'] = 'json';
$params['ik_sign'] = $this->getParamsSign($params);
// Create a stream
$opts = array(
'http'=>array(
'method'=>"GET",
)
);
$context = stream_context_create($opts);
$params = http_build_query($params);
// Получим оплаты, которые привязаны к кассе
$data = json_decode(file_get_contents(self::API_URL."?".$params, false, $context));
// Подготовим массив
$payways = array(0 => t('-Все типы оплат-'));
if (isset($data->resultMsg) && $data->resultMsg=="Success"){ //Если всё прошло успешно и мы получили типы путей оплаты
foreach($data->resultData->paywaySet as $way){
$payways[$way->als] = mb_strtoupper($way->ser)." - ".mb_strtoupper($way->curAls);
}
}
asort($payways);
return $payways;
}
/**
* Получает возможные пути оплаты для привязанной к вам кассы
*
* @param array|string $data - id кассы или массив параметров, среди которых, должен быть ключ "checkout_id"
*/
public static function staticGetPaywaysByCheckoutID($data)
{
$_this = new self();
$_this->setOption('secret_key',$data['secret_key']);
$_this->setOption('ik_co_id',$data['checkout_id']);
$data = $_this->getPayways();
$payways = array();
foreach ($data as $key=>$value){
$payways[] = array(
'key' => $key,
'value' => $value,
);
}
return array('list' => $payways);
}
/**
* Получает нужную подпись для разных режимов
*
* @param boolean $test - флаг, что нужно использовать тестовый ключ
*/
private function getRightSign($test = false)
{
if ($test && ($this->getOption('ik_pw_via',false) == "test_interkassa_test_xts")){
file_put_contents(__DIR__.'/file.txt',date('Y-m-d H:i:s')."\n"."345435\n",FILE_APPEND);
return $this->getOption('test_key','');
}
return $this->getOption('secret_key','');
}
/**
* Получает подпись для платежа формируемая по правилам Интеркассы
*
* @param array $params - массив параметров
* @param boolean $test - флаг, что нужно использовать тестовый ключ
*/
private function getParamsSign( $params, $test = false )
{
unset($params['ik_sign']); //Удаляем из данных строку подписи
ksort($params, SORT_STRING); //Сортируем по ключам в алфовитном порядке
array_push($params, $this->getRightSign($test)); //Конкатинируем символом ":"
return base64_encode(md5(implode($params, ":"), true)); //MD5 в бинарном виде в кодированном base64
}
/**
* Получает язык в котором будет представлен интерфейс Интеркассы
*/
private function getLanguage()
{
return $this->getOption('language',0) ? $this->getOption('language',0) : false;
}
/**
* Проверяет диапозон IP адреса. Проверяет IP адреса интеркассы
*
* @return boolean
*/
private function checkIPsDiapozon()
{
$ip = $_SERVER['REMOTE_ADDR'];
if (stripos($ip, self::IP_DIAPOZON)===false){
return false;
}
return true;
}
/**
* Получает трех символьный код базовой валюты в которой ведётся оплата
*
*/
private function getPaymentCurrency()
{
/**
* @var \Catalog\Model\Orm\Currency
*/
$currency = \RS\Orm\Request::make()
->from(new \Catalog\Model\Orm\Currency())
->where(array(
'public' => 1,
'is_base' => 1,
))
->object();
return $currency ? $currency->title : false;
}
/**
* Проверяем подпись запроса
*
* @param string $sign - подпись запроса
*/
private function checkSign($sign, $test = false)
{
$ik = array();
foreach ($_REQUEST as $key=>$value){
if (stripos('ik_')!==false){
$ik[$key] = $value;
}
}
$my_sign = $this->getParamsSign($ik, $test); //Получаем нами сформированную подпись
// Проверка корректности подписи
return $my_sign == $sign;
}
/**
* Проверяет основные параметры приходящие от интеркассы сравнивая с теми, что уснавлены в настройках системы
*
* @param \Shop\Model\Orm\Transaction $transaction - объект транзакции
* @param \RS\Http\Request $request - объект запросов
*/
private function checkMainParams(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
{
$ik_co_id = $request->request('ik_co_id',TYPE_STRING,'');
$ik_am = $request->request('ik_am',TYPE_STRING,'');
$ik_inv_st = $request->request('ik_inv_st',TYPE_STRING,'');
$ik_sign = $request->request('ik_sign',TYPE_STRING,'');
if ( $ik_co_id != $this->getOption('ik_co_id','') ) {
return 'ID кассы';
}
if ( $ik_am != round($transaction->cost, 2) ) {
return 'Сумма платежа';
}
if (!in_array($ik_inv_st,array('process','success','fail','waitAccept'))) {
return 'параметр ik_inv_st='.$ik_inv_st;
}
$ik = array();
foreach ($_REQUEST as $key=>$value){
if (stripos('ik_')!==false){
$ik[$key] = $value;
}
}
$test = ($request->request('ik_pw_via',TYPE_STRING,'') == "test_interkassa_test_xts");
if (!$this->checkSign($ik_sign, $test)){
return 'Неправильная подпись. Параметр ik_sign='.$ik_sign." ".$this->getParamsSign($ik);
}
return true;
}
/**
* Возвращает ID заказа исходя из REQUEST-параметров соотвествующего типа оплаты
* Используется только для Online-платежей
*
* @return mixed
*/
function getTransactionIdFromRequest(\RS\Http\Request $request)
{
return $request->request('ik_pm_no', TYPE_INTEGER, false);
}
/**
* Обработка запросов от интеркассы
*
* @param \Shop\Model\Orm\Transaction $transaction - объект транзакции
* @param \RS\Http\Request $request - объект запросов
* @return string
*/
function onResult(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
{
if (!$this->checkIPsDiapozon()){
$exception = new \Shop\Model\PaymentType\ResultException(t('Неправильный диапозон IP адресов'));
$exception->setResponse('Wrong IPs');
throw $exception;
}
if (!$this->checkMainParams($transaction, $request)){
$exception = new \Shop\Model\PaymentType\ResultException(t('Главные параметры указаные в настройках интеркассы не прошли проверку'));
$exception->setResponse('Wrong main params');
throw $exception;
}
//Смотрим текущий статус
$status = $request->request('ik_inv_st',TYPE_STRING,0);
switch($status){
case "success":
return 'OK'.$transaction->id;
break;
case "process":
$exception = new \Shop\Model\PaymentType\ResultException(t('Неудачно совершённый платёж'));
$exception->setResponse('Payment status progress');
$exception->setUpdateTransaction(false);
throw $exception;
break;
case "waitAccept": //Если долгое ожидание проведения платежа, то пользователь перенаправляется к нам
\RS\Application\Application::getInstance()->headers->addHeader('Location',$request->getSelfAbsoluteHost());
break;
case "fail":
default:
$exception = new \Shop\Model\PaymentType\ResultException(t('Неудачно совершённый платёж'));
$exception->setResponse('Payment failed');
throw $exception;
break;
}
return 'OK'.$transaction->id;
}
/**
* Вызывается при открытии страницы неуспешного проведения платежа
* Используется только для Online-платежей
*
* @param \Shop\Model\Orm\Transaction $transaction
* @param \RS\Http\Request $request
* @return void
*/
function onFail(\Shop\Model\Orm\Transaction $transaction, \RS\Http\Request $request)
{
$transaction['status'] = $transaction::STATUS_FAIL;
$transaction->update();
}
}
Как отлаживать всё это дело?
Я лично все запросы которые пришли на url пишу в файл. Например так:
//Пишет в текущую папку в файл info.txt весь массив запроса
file_put_contents(__DIR__."/info.txt","\n".var_export($_REQUEST,true),FILE_APPEND);