Версия: 6.x
burger close
Инструкция по разработке модулей интеграции с платёжными сервисами

Рассмотрим пример создания модуля для вымышленного типа оплаты CustomPaymentType. Структура папок модуля рекомендуется следующая:

  • custompaymenttype //название модуля. Можно использовать любое английское имя
    • config //Здесь должны находиться конфигурационные классы модуля
      • file.inc.php //Конфигурационный файл модуля
      • handlers.inc.php //Файл с обработчиками событий, на которые подписан модуль
      • module.xml //Файл со сведениями о модуле и его настройках по-умолчанию
    • model //Модели модуля
      • paymenttype //Контейнер для классов оплаты
        • paymenttype.inc.php //класс типа оплаты

Непосредственно функциональность модуля оплаты реализована в классе PaymentType. Класс PaymentType должен быть унаследован от базового класса Shop::Model::PaymentType::AbstractType.

Класс, описывающий тип оплаты (paymenttype.inc.php)

Рассмотрим методы базового класса Shop::Model::PaymentType::AbstractType, которые необходимо реализовать/унаследовать в нашем классе PaymentType.

Основная информация о типе оплаты

/**
* Возвращает название расчетного модуля (типа оплаты)
*
* @return string
*/
abstract public function getTitle();
/**
* Возвращает описание типа оплаты. Возможен HTML
*
* @return string
*/
abstract public function getDescription();
/**
* Возвращает идентификатор данного типа оплаты. (только англ. буквы)
*
* @return string
*/
abstract public function getShortName();
/**
* Возвращает true, если данный тип поддерживает проведение платежа через интернет
*
* @return bool
*/
abstract public function canOnlinePay();
/**
* Возвращает true, если данный тип подразумевает наложенный платеж при оплате заказа
*
* @return bool
*/
public function cashOnDelivery()
{
return !$this->canOnlinePay();
}
/**
* Возвращает идентификатор, уникализирующий продавца в рамках типа оплаты
*
* @return string
*/
public function getTypeUnique(): string
{
return '';
}

Дополнительная форма для способа оплаты

/**
* Возвращает ORM объект для генерации формы в административной панели или null
*
* @return FormObject|void
*/
public function getFormObject()
{}

Пример готовой формы:

public function getFormObject()
{
$properties = new \RS\Orm\PropertyIterator([
'api_key' => new \RS\Orm\Type\Varchar([
'description' => t('API ключ'),
]),
...
]);
$form_object = new FormObject($properties);
$form_object->setParentObject($this);
$form_object->setParentParamMethod('Form');
return $form_object;
}

Методы для классов, поддерживающих печатные формы

Например: если ваш тип оплаты выставляет "счёт на оплату", то печатной формой будет являтся "счёт на оплату"

/**
* Возвращает список названий документов и ссылки, по которым можно открыть данные документы,
* генерируемых данным типом оплаты
*
* @return array
*/
public function getDocsName()
{
//Пример реализации
return [
'bill' => [
'title' => t('Счет')
],
//...тут возможны еще другие документы
];
//bill - это идентификатор документа, который будет передан в аргументе $dockey в метод getDocHtml
}
/**
* Возвращает html документа для печати пользователем
*
* @param mixed $dockey
*/
public function getDocHtml($dockey = null)
{
//Пример реализации
$view = new \RS\View\Engine();
// Счет по оплате заказа
if($this->order){
$view->assign('order', $this->order);
return $view->fetch('%shop%/payment/bill.tpl');
}
// Счет по оплате транзации пополнения счета
if($this->transaction){
$view->assign('transaction', $this->transaction);
return $view->fetch('%shop%/payment/bill_transaction.tpl');
}
throw new \Exception(t('Невозможно сформировать документ. Не передан ни объект заказа, ни объект транзакции'));
}

Методы для классов, поддерживающих операции с заказом

Например: тип оплаты "с лицевого счёта" при просмотре заказа в административной панели отображает кнопку "оплатить заказ с лицевого счёта"

/**
* Возвращает дополнительный HTML для админ части в заказе
*
* @param Order $order - объект заказа
* @return string
*/
public function getAdminHTML(Order $order)
{
return "";
}
/**
* Действие с запросами к заказу для исполнения определённой операции
*
* @param Order $order - объект заказа
*/
public function actionOrderQuery(Order $order)
{}

Методы для классов, поддерживающих оплату онлайн (интернет эквайринг)

Под "онлайн оплатой" подразумевается интеграция с сервисами, способными присылать мгновенные уведомления о статусе платежа. Например: оплта банковской картой.

/**
* Возвращает true, если необходимо использовать
* POST запрос для открытия страницы платежного сервиса
*
* @return bool
*/
public function isPostQuery()
{
return false;
}
/**
* Возвращает URL для перехода на сайт сервиса оплаты для совершения платежа
* Используется только для Online-платежей
*
* @param Transaction $transaction
* @return string
*/
public function getPayUrl(Transaction $transaction)
{}
/**
* Возвращает ID заказа исходя из REQUEST-параметров соотвествующего типа оплаты
* Используется только для Online-платежей
*
* @param HttpRequest $request - входящий запрос
* @return mixed
*/
public function getTransactionIdFromRequest(HttpRequest $request)
{
return false;
}
/**
* Вызывается при оплате сервером платежной системы.
* Возвращает строку - ответ серверу платежной системы.
* В случае неверной подписи бросает исключение
* Используется только для Online-платежей
*
* @param Transaction $transaction - транзакция
* @param HttpRequest $request - входящий запрос
* @return \Shop\Model\ChangeTransaction|string
*/
public function onResult(Transaction $transaction, HttpRequest $request)
{}
/**
* Собирает результаты обработки нескольких платежей в один ответ
* (используется только у оплат, которые в одном уведомлении отправляют информацию по нескольким платежам)
*
* @param string[] $result_array - результаты обработки платежей
* @return string
*/
public function wrapOnResultArray($result_array)
{}

Методы для классов, поддерживающих действия с транзакциями

/**
* Возвращает список возможных действий с транзакцией
*
* @param Transaction $transaction - транзакция
* @return TransactionAction[]
*/
public function getAvailableTransactionActions(Transaction $transaction): array
{
return [];
}
/**
* Исполняет действие с транзакцией
* При успехе - возвращает текст сообщения для администратора, при неудаче - бросает исключение
*
* @param Transaction $transaction - транзакция
* @param string $action - идентификатор исполняемого действия
* @return string
* @throws RSException
*/
public function executeTransactionAction(Transaction $transaction, string $action): string
{
throw new RSException(t('Данный тип оплаты не поддерживает действий с транзакциями'));
}
Заметки
Изменения в транзакцию следует вносить при помощи объекта Shop::Model::ChangeTransaction

Пример:

$change = new \Shop\Model\ChangeTransaction($transaction);
$change->setNewStatus(Transaction::STATUS_SUCCESS);
$change->setChangelog(t('Оплата успешно завершена'));
$change->applyChanges();

Справочники

/**
* Справочник кодов НДС
* Ключи справочника должны соответствовать списку кодов НДС в TaxApi
*
* @return string[]
*/
protected static function handbookNds()
{
static $nds = [
TaxApi::TAX_NDS_NONE => TaxApi::TAX_NDS_NONE,
TaxApi::TAX_NDS_0 => TaxApi::TAX_NDS_0,
TaxApi::TAX_NDS_10 => TaxApi::TAX_NDS_10,
TaxApi::TAX_NDS_20 => TaxApi::TAX_NDS_20,
TaxApi::TAX_NDS_110 => TaxApi::TAX_NDS_110,
TaxApi::TAX_NDS_120 => TaxApi::TAX_NDS_120,
];
return $nds;
}

Регистрация нового типа оплаты в системе

Для регистрации в системе нового типа оплаты, необходимо обработать событие payment.gettypes. Согласно формату моделй в ReadyScript, регистрация обработчика событий производится в файле /config/handlers.inc.php.

Пример регистрации нового модуля-адаптера для телефонии CustomPaymentType.

<?php
namespace CustomPaymentType\Config;
/**
* Класс содержит обработчики событий, на которые подписан модуль
*/
class Handlers extends \RS\Event\HandlerAbstract
{
/**
* Добавляет подписку на события
*
* @return void
*/
function init()
{
$this->bind('payment.gettypes');
}
/**
* Регистрирует в системе класс модуля-адаптера для телефонии
*
* @param $list
* @return array
*/
public static function paymentGetTypes($list)
{
$list[] = new \CustomPaymentType\Model\PaymentType\PaymentType();
return $list;
}
}

Холдирование платежей

Когда платёж холдируется следует перевести транзакцию в статус Transaction::STATUS_HOLDING

Для завершения/отмены холдирования в административной панели, необходимо реализовать следующие методы:

/**
* Возвращает список возможных действий с транзакцией
*
* @param Transaction $transaction
* @return TransactionAction[]
*/
public function getAvailableTransactionActions(Transaction $transaction): array
{
$result = [];
if ($transaction['status'] == Transaction::STATUS_HOLD) {
$result[] = (new TransactionAction($transaction, 'close_hold', t('Завершить оплату')))
->setConfirmText(t('Вы действительно хотите завершить оплату?'))
->setCssClass(' btn-primary');
}
return $result;
}
/**
* Исполняет действие с транзакцией
* При успехе - возвращает текст сообщения для администратора, при неудаче - бросает исключение
*
* @param Transaction $transaction - транзакция
* @param string $action - идентификатор исполняемого действия
* @return string
* @throws RSException
*/
public function executeTransactionAction(Transaction $transaction, string $action): string
{
...
switch ($action) {
case 'close_hold':
...
Производим необходимые действия
...
return t('Оплата успешно завершена');
default:
throw new RSException(t('Вызванное действие не поддерживается данным типом оплаты'));
}
}

Рекуррентные платежи

Рекуррентные платежи подразумавают получение и сохранение в системе идентификатора способа платежа, после его ввода покупателем на стороне платежной системы. Далее, сохраненный способ платежа можно использовать при последующих оплатах для списания средств без ввода данных по способу платежа (например, карты).

Чтобы способ оплаты поддерживал рекуррентные платежи, он должен имплементировать интерфейс Shop::Model::PaymentType::InterfaceRecurringPayments и использовать трейт Shop::Model::PaymentType::TraitInterfaceRecurringPayments. Трейт создан для упрощения разработки, в нем уже реализованы некоторые методы интерфейса, но при необходимости вы сможете их перегрузить в вашем классе платежной системы.

Чтобы в административной панели у способа оплаты появились соответствующие опции, необходимо в вашем расчетном классе оплаты в методе getFormObject предусмотреть добавление настроек, связанных с рекуррентными платежами.

Пример кода:

<?php
use RS\Orm\Type;
/**
* Способ оплаты - ЮKassa (ранее Яндекс.Касса)
*/
class YandexKassaApi extends AbstractType implements InterfaceRecurringPayments
{
//... пропущен код
/**
* Возвращает ORM объект для генерации формы или null
*
* @return FormObject | null
*/
public function getFormObject()
{
//Собственные настройки расчетного класса оплаты
$properties = [
'__help__' => (new Type\MixedType())
->setDescription(t(''))
->setVisible(true)
->setTemplate('%shop%/form/payment/yandexkassaapi/help.tpl'),
'shop_id' => (new Type\Integer())
->setDescription(t('Идентификатор магазина (shopId)'))
->setHint(t('Можно узнать в личном кабинете ЮКассы'))
->setChecker(Type\Checker::CHECK_EMPTY, t('Не указан "Идентификатор магазина (shopId)"')),
'key_secret' => (new Type\Varchar())
->setDescription(t('Секретный ключ'))
->setHint(t('Можно узнать в личном кабинете ЮКассы'))
->setChecker(Type\Checker::CHECK_EMPTY, t('Не указан "Секретный ключ"')),
'is_holding' => (new Type\Integer())
->setDescription(t('Холдирование платежей'))
->setMaxLength(1)
->setDefault(0)
->setCheckboxView(1, 0),
'enable_log' => (new Type\Integer())
->setDescription(t('Вести лог запросов?'))
->setMaxLength(1)
->setDefault(0)
->setCheckboxView(1, 0),
];
//Подключаем стандартные настройки рекуррентных оплат
$properties += $this->getFormCommonProperties();
//Создаем объект формы, возвращаем его
$form_object = new FormObject(new PropertyIterator($properties));
$form_object->setParentObject($this);
$form_object->setParentParamMethod('Form');
return $form_object;
}
}

В этом случае у вашего расчетного класса автоматически появятся следующие опции:

  • Режим работы "рекуррентных платежей": Рекуррентные платежи не доступны, Сохранять способ платежа, Только привязывать способ платежа
  • Не отправлять чек при привязке способа платежа

Обычно, платежные системы возвращают идентификатор способа платежа в уведомлении об успешной оплате. Соответственно необходимо принимать его там и сохранять в БД, с помощью объекта класса Shop::Model::Orm::SavedPaymentMethod. Ниже предлагаем схематично посмотреть, как мог бы выглядеть код, который выпоняет сохранение способа платежа. Различные платежные системы могут совершенно по-разному возвращать данные о способе платежа, поэтому мы показываем на примере ЮКассы.

<?php
use Shop\Model\Exception as ShopException;
use RS\Http\Request as HttpRequest;
/**
* Способ оплаты - ЮKassa (ранее Яндекс.Касса)
*/
class YooKassaApi extends AbstractType implements InterfaceRecurringPayments
{
const TRANSACTION_EXTRA_KEY_PAYMENT_ID = 'payment_id';
//..... пропущен код
/**
* Вызывается при оплате сервером платежной системы.
* Возвращает строку - ответ серверу платежной системы.
* В случае неверной подписи бросает исключение
* Используется только для Online-платежей
*
* @param Transaction $transaction
* @param HttpRequest $request
* @return string
* @throws ResultException
* @throws ShopException
*/
public function onResult(Transaction $transaction, HttpRequest $request)
{
//..... пропущен код
//Ниже пример кода сохранения способа платежа в системе
$payment_id = $transaction->getExtra(self::TRANSACTION_EXTRA_KEY_PAYMENT_ID);
if ($payment_id) {
//Проверяем реальный статус платежа, запросом к платежному сервису. Так нужно для ЮКассы.
//В некоторых других плтежных системах, где входящие запросы подписаны вся информация по платежу есть сразу.
$response = $this->apiRequest("payments/$payment_id", 'GET');
if (!empty($response['payment_method']['saved'])) {
$payment_method_data = $response['payment_method'];
//Пытаемся понять, был ли привязан данный способ платежа раньше. Если да, то загружаем его.
$saved_method = SavedPaymentMethod::loadByWhere(['external_id' => $payment_method_data['id']]);
if (empty($saved_method['id'])) {
//Если это новый способ платежа, то сохраняем его
$payment_method = new SavedPaymentMethod();
//string external_id - Внешний идентификатор способа платежа (используется для рекуррентной оплаты)
$payment_method['external_id'] = $payment_method_data['id'];
//string type - Тип способа платежа (например, SavedPaymentMethod::TYPE_CARD)
$payment_method['type'] = SavedPaymentMethod::TYPE_CARD;
//string subtype - Подтип способа платежа
$payment_method['subtype'] = $payment_method_data['card']['card_type'];
//string title - Имя способа платежа
$payment_method['title'] = "*{$payment_method_data['card']['last4']}";
//int user_id' - id пользователя, к которому привязан способ платежа
$payment_method['user_id'] = $transaction->getUser()['id'];
//string payment_type - Класс типа оплаты (получается при помощи вызова метода getShortName() у способа оплаты)
$payment_method['payment_type'] = $this->getShortName();
//string payment_type_unique - Идентификатор в рамках класса (получается при помощи вызова метода getTypeUnique у способа оплаты)
$payment_method['payment_type_unique'] = $this->getTypeUnique();
//array data - Произвольные данные способа платежа
$payment_method['data'] = $payment_method_data['card'];
if ($payment_method->insert()) {
//Сохраняем сведения о том, что транзакция связана с привязкой карты
$transaction->setExtra(InterfaceRecurringPayments::TRANSACTION_EXTRA_KEY_SAVED_METHOD, $payment_method['id']);
$transaction->update();
return true;
}
}
}
} else {
throw (new ResultException(t('Транзакция не содержит идентификатор платежа')))
->setUpdateTransaction(false); //Не устанавливаем статус FAIL транзакции
}
}
}

Рассмотрим методы интерфейса Shop::Model::PaymentType::InterfaceRecurringPayments, базовая реализация которых отсутствует в трейте, а соответственно их необходимо реализовать:

/**
* Производит "рекуррентную" оплату заказа
* Автоматически списывает с $saved_payment_method средства за заказ $order
* @param Order $order - заказ
* @param SavedPaymentMethod $saved_payment_method - сохранённый способ платежа
* @return void
*/
public function recurringPayOrder(Order $order, SavedPaymentMethod $saved_payment_method): void;
/**
* Производит "рекуррентное" пополнение лицевого счёта
* Автоматически списывает с $saved_payment_method средства и увеличивает баланс лицевого счета $user на сумму $cost
*
* @param User $user - пользователь
* @param float $cost - сумма пополнения
* @param SavedPaymentMethod $saved_payment_method - сохранённый спосб платежа
* @return void
*/
public function recurringPayBalanceFounds(User $user, float $cost, SavedPaymentMethod $saved_payment_method): void;
/**
* Производит возврат транзакции, привязывающей новый способ платежа
* Создает транзакцию RS на списание суммы, указанной в $transaction и выполняет запрос на полный возврат
* средств к платежной системе.
*
* @param Transaction $transaction - транзакция
* @return void
*/
public function refundBindingTransaction(Transaction $transaction): void;
/**
* Удаляет сохранённый способ платежа
*
* @param SavedPaymentMethod $saved_payment_method - Сохранённый способ платежа
* @return void
* @throws ShopException
*/
public function deleteSavedPaymentMethod(SavedPaymentMethod $saved_payment_method): void;

Готовый пример реализации поддержки рекуррентных платежей можно найти в классе Shop::Model::PaymentType::YandexKassaApi.