Разработка платежного шлюза для Magento 2
В данной статье я хочу поделиться опытом разработки пользовательского метода оплаты в magento 2, рассказать о самом процессе разработки, подводных камнях и методах работы с данной системой.
Оглавление
Окружение
Так как Magento довольно требовательна к окружению было принято решение развернуть систему на VPS, также наш хостинг провайдер предоставляет услугу предоставления VPS с уже установленной и подготовленной к работе Magento на борту.
Требования системы Magento 2:
- Операционная система: Linux distributions (RHEL, Ubuntu, CentOS, Debian)
- Последняя стабильная версия Composer
- Apache 2.2 or 2.4 (не забываем включить модуль mod_rewrite)
- PHP: 5.4.x (x is 11 или более новая); 5.5.x
- PHP расширения: PDO/MySQL, mcrypt, mbstring, mhash, curl, simplexml, gd2, ImageMagick 6.3.7, soap
- MySQL 5.6.x
- SMTP сервер MTA
После того как не с первого раза получилось вернуть систему, начали проверять ее работоспособность. Тут снова возникли проблемы, товары не создаются, страницы не сохраняются, конфигурационные файлы не обновляются и все это сопровождается одной ошибкой Request validation failed
– в общем на лицо признаки серьезной проблемы.
После долгого поиска проблемы, просмотра логов и общения с поддержкой хостинга которая разводила руками выяснилось, что проблема кроется в конфигурационных файлах php-fpm
, а если быть точнее то в настройке post_max_size
которая находится по пути /etc/php/{версия php}/fpm/conf.d/magento.ini
, данное значение желательно чтобы было не меньше 3 Мб в идеале больше 5 Мб. После изменения значения все прекрасно заработало и даже поддержка хостинга решила изменить настройки образа на будущее.
Информации о данном решении проблемы на просторах интернета к сожалению не нашлось и эта одна из основных проблем работы с системой, информации в сети очень мало, а та что есть может быть или не рабочей, или не подходить для вашей версии, поэтому приходится работать с системой методом проб и ошибок.
Структура
В Magento имеется аналог WordPress плагинов – модули, они устанавливаются исключительно через файловую систему (никакого графического интерфейса добавления модулей нет). Чтобы добавить кастомный модуль, нужно создать папку по пути \project-name\app\code\Magento\
, в созданной папке создать файл registration.php
в котором нужно добавить код регистрации модуля, пример:
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Custom_module',
__DIR__
);
А так же в той же папке создать папку etc и в ней файл module.xml с таким содержимым:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noTestSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Custom_module" setup_version="2.0.0" active="true">
</module>
</config>
После добавления модуля для его регистрации необходимо выполнить команду для обновления кодовой базы и конфигурационных файлов:
php bin/magento setup:upgrade
и для компиляции проекта:
php bin/magento setup:di:compile
если при выполнении второй команды процесс обрывается не дойдя до конца необходимо запустить php с большим объемом памяти:
php -d memory_limit=5G bin/magento setup:di:compile.
Чтобы проверить, загрузился ли модуль используйте команду, которая отображает список рабочих модулей и список включенных:
php bin/magento module:status
Самое неприятное в работе с системой, что при внесении критических изменений в код модуля нужно выполнять эти 2 команды для применения этих изменений.
В целом структура работы с модулями привязана к MVC паттерну, что позволяет расширять классы и переопределять методы, с одной стороны это удобно и делает систему довольно гибкой, но с другой стороны сложная структура и невероятно длинные цепочки вызовов сильно усложняют разработку модулей.
Структура
Для регистрации шлюза как платежного шлюза в качестве метода оплаты нужно создать файл (путь из корня вашего модуля) custom_module/etc/config.xml в котором будут прописаны основные настройки вашего шлюза, пример:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../Store/etc/config.xsd">
<default>
<payment>
<custompayment><!--ID шлюза-->
<active>1</active> <!--On\Off-->
<model>Magento\custom_module\Model\PaymentMethod</model><!--Namespace основного класса шлюза-->
<order_status>pending</order_status><!-- default order status-->
<title>Custom Payment</title><!--Заголовок шлюза-->
<payment_action>authorize</payment_action><!--Метод который будет вызываться в основном классе шлюза при оплате заказа-->
</custompayment>
</payment>
</default>
</config>
Для добавления полей с настройками вашего шлюза нужно создать файл (путь из корня вашего модуля) custom_module/etc/adminhtml/system.xml в котором будут зарегистрированы поля для вашего шлюза, пример:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="payment">
<group id="custompayment" translate="label" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> <!--Отдельная группа вашего шлюза-->
<label>Custom Payment</label> <!--Название шлюза-->
<field id="active" translate="label comment" sortOrder="1" type="select" showInDefault="1" showInWebsite="1" showInStore="0"> <!--Select вкл\выкл -->
<label>Enable</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="authorization_token" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <!--Text input токен платежного провайдера используемый для взаимодействия с API-->
<label>Authorization token</label>
</field>
<field id="test_mode" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" canRestore="1"> <!--Select тестовый режим вкл\выкл-->
<label>Test mode</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</system>
</config>
Пример с большим количеством полей можно посмотреть в модуле Payment по тому же пути который идет по умолчанию в Magento 2.
Если все правильно сделано, в панели администрирования должна появится отдельная вкладка с настройками вашего метода
Stores Configuration → Sales → Payment Methods → OTHER PAYMENT METHODS
Отображение
Для регистрации нового метода оплаты списке методов на странице оформления заказа, необходимо добавить файл и папки custom_module/view/frontend/layout/checkout_index_index.xml
со следующим содержимым:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="billing-step" xsi:type="array">
<item name="component" xsi:type="string">uiComponent</item>
<item name="children" xsi:type="array">
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="renders" xsi:type="array">
<!-- merge payment method renders here -->
<item name="children" xsi:type="array">
<item name="custompayment" xsi:type="array"> <!--ID шлюза-->
<item name="component" xsi:type="string">custom_module/js/view/payment/method-renderer</item> <!--Путь к подключаемому JS скрипту обработки-->
<item name="methods" xsi:type="array">
<item name="custompayment" xsi:type="array">
<item name="isBillingAddressRequired" xsi:type="boolean">true</item> <!--Обязательно ли поле Адреса оплаты-->
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
А так же добавить Html для вывода по следующему пути custom_module/view/frontend/web/template/payment/custompayment.html
со следующим содержимым:
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}">
<div class="payment-method-title field choice">
<input type="radio"
name="payment[method]"
class="radio"
data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
<label data-bind="attr: {'for': getCode()}" class="label"><span data-bind="text: getTitle()"></span></label>
</div>
<div class="payment-method-content">
<!-- ko foreach: getRegion('messages') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
<div class="payment-method-billing-address">
<!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
</div>
<p data-bind="html: getInstructions()"></p>
<div class="checkout-agreements-block">
<!-- ko foreach: $parent.getRegion('before-place-order') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
</div>
<div class="actions-toolbar">
<div class="primary">
<button class="action primary checkout"
type="submit"
data-bind="
click: placeOrder,
attr: {title: $t('Place Order')},
enable: (getCode() == isChecked()),
css: {disabled: !isPlaceOrderActionAllowed()}
"
disabled>
<span data-bind="i18n: 'Place Order'"></span>
</button>
</div>
</div>
</div>
</div>
В данном шаблоне можно кастомизировать отображение метода оплаты и кнопки оплаты.
Далее нужно создать JS скрипт которые при необходимости будет делать обработку, валидацию, отправку запросов и многое другое. Для добавления нужно создать файл по пути custom_module/view/frontend/web/js/view/payment/method-renderer.js
и указать в нем какой скрипт будет использоваться с данным платежным методом:
define([
'uiComponent',
'Magento_Checkout/js/model/payment/renderer-list'
],
function (
Component,
rendererList
) {
'use strict';
rendererList.push({
type: custompayment,
component: 'Custom_Payment/js/view/payment/method-renderer/custompayment /*Путь до скрипта*/
});
return Component.extend({});
});
Далее создаем сам компонент который будет расширять стандартный набор методов для данного шлюза или перезаписывать их custom_module/view/frontend/web/js/view/payment/method-renderer/custompayment.js
, ниже пример использования и расширения методов
define([
'Magento_Checkout/js/view/payment/default',
'jquery',
'mage/validation',
'mage/storage'
],
function (Component) {
'use strict';
return Component.extend({
defaults: {
template: 'Custom_Payment/payment/custompayment
},
/**
* Returns payment method instructions.
*
* @return {*}
*/
getInstructions: function () {
return window.checkoutConfig.payment.instructions[this.item.method];
},
afterPlaceOrder: function () {
return false;
},
/** @inheritdoc */
initObservable: function () {
this._super().observe('purchaseOrderNumber');
return this;
},
/**
* @return {jQuery}
*/
validate: function () {
var form = 'form[data-role=purchaseorder-form]';
return $(form).validation() && $(form).validation('isValid');
}
});
});
Здесь можно посмотреть некоторые методы которые можно использовать, но это далеко не все, весь список так и не удалось найти.
Обработка
Основной шаг в создании шлюза – это создание класса модели которая будет содержать основную информацию и методы данного шлюза, создается файл по пути custom_module/Model/PaymentMethod.php
.
Данный класс должен расширять класс \Magento\Payment\Model\Method\AbstractMethod
или \Magento\Payment\Model\Method\Cc
для ввода карты непосредственно на сайте.
В первую класс очередь должен содержать набор защищенных переменных которые определяют конфигурации и возможности нашего шлюза. Разберем несколько основных настроек:
protected $_code = 'custompayment'
– ID вашего шлюзаprotected $_isOffline = false
– Разрешена ли офлайн оплата данным методомprotected $_canAuthorize = true
– Можно ли использовать метод authorizeprotected $_canCapture = false
– Можно ли использовать метод captureprotected $_canRefund = false
– Можно ли сделать рефанд
Полный список переменных можно найти в родительском классе.
Основной метод обработки платежа является метод который мы указали в конфигурациях custom_module/etc/config.xml
в параметре payment_action
, в нашем случае это метод authorize
:
/**
* Authorize payment abstract method
*
* @param \Magento\Framework\DataObject|InfoInterface $payment
* @param float $amount
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @deprecated 100.2.0
*/
public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount)
{
return $this;
}
В нашем случае мы обращаемся к API платежного провайдера, чтобы получить ссылку на страницу ввода карты и оплаты используя данные авторизации введенные пользователем на странице настроек шлюза в панели администрирования.
Для получения поля authorization_token, используем метод $this->getConfigData('authorization_token')
, для проверки доступности данного метода для использования $this->canAuthorize()
.
Вот как выглядит метод в полном виде:
/**
* Authorize payment abstract method
*
* @param \Magento\Framework\DataObject|InfoInterface $payment
* @param float $amount
*
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @deprecated 100.2.0
*/
public function authorize(
\Magento\Payment\Model\InfoInterface $payment, $amount
) {
if ( ! $this->canAuthorize() ) {
throw new \Magento\Framework\Exception\LocalizedException( __( 'The authorize action is not available.' ) );
}
// Получаем значения полей из панели администратора
$token = $this->getConfigData( 'authorization_token' );
$test = $this->getConfigData( 'test_mode' );
if ( empty( $token ) ) {
throw new \Magento\Framework\Exception\LocalizedException( __( 'Gateway token is empty.' ) );
}
try {
$order = $payment->getOrder(); // Получаем объект заказа
$client = new Curl();
// Получаем ссылки на страницы успешной оплаты и ошибочной
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$successUrl = $objectManager->create( \Magento\Framework\UrlInterface::class )->getUrl( 'checkout/onepage/success' ) . '?gateway=custom';
$failureUrl = $objectManager->create( \Magento\Framework\UrlInterface::class )->getUrl( 'checkout / onepage / failure' )
$params = [
'amount' => round( floatval( $amount ), 2 ) * 100, // Сумма заказа
'currency_code' => $order->getBaseCurrencyCode(), // Валюта заказа
'success_url' => $successUrl, // Ссылка на страницу успешной оплаты
'cancel_url' => $failureUrl, // Ссылка на страницу ошибочной оплаты
'test' => $test == '1', // Маркер использовать ли режим тестирования
];
// Устанавливаем заголовки запроса
$client->addHeader( "Authorization", "Bearer " . $token );
$client->addHeader( "Content-Type", 'application / json' );
$client->addHeader( "Accept", 'application / json' );
// Делаем POST запрос
$client->post( 'https://custom.com/api/payment_intent', \Safe\json_encode( $params ) );
// Получаем ответ
$result = \Safe\json_decode( $client->getBody() );
// Выводим ошибку в случае получения ошибки из API
if ( ! empty( $result->message ) ) {
throw new \Magento\Framework\Exception\LocalizedException( __( $result->message ) );
}
// Записывает ID транзакции
if ( ! empty( $result->id ) ) {
$payment->setTransactionId( $result->id )->setIsTransactionClosed( 0 );
}
// Записывает полученную ссылку на страницу оплаты
if ( ! empty( $result->redirect_url ) ) {
$payment->setAdditionalInformation( 'custom_url', $result->redirect_url );
}
// Устанавливаем статус заказа
$orderState = Order::STATE_PENDING_PAYMENT;
$order->setState( $orderState )->setStatus( $orderState );
$order->save();
//$payment->save();
} catch ( \Exception $e ) {
throw new \Magento\Framework\Exception\LocalizedException( __( 'Payment capturing error.' . $e->getMessage() ) );
}
return $this;
}
В результате мы получили ID транзакции, установили статус заказа “Ожидание оплаты” и URL страницы оплаты, но самое интересное начинается дальше, так как сделать переадресацию на страницу оплаты из этого метода или даже через JS компонент не получится по причине запрета CORS политиками на редирект из Fetch запроса.
Чтобы обойти данное ограничение, пришлось идти по стандартному сценарию и делать редирект на страницу успешной оплаты, но перехватив запрос до начала обработки страницы сделать редирект уже на нужную нам страницу оплаты на сайте платежного провайдера.
Завершение заказа
Для того чтобы перехватить обработку страницы успешной оплаты создаем новый файл custom_module/view/frontend/layout/checkout_onepage_success.xml
, в котором генерируется данная страница и в качестве первого блока указываем кастомный блок который создадим позже:
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<title>Success Page</title>
</head>
<body>
<referenceBlock name="page.main.title">
<block class="Magento\custom_module\Block\Success" name="checkout.success.print.button" template="Magento_Checkout::button.phtml"/> <!--В параметре class прописываем namespace нашего блока-->
<action method="setPageTitle">
<argument translate="true" name="title" xsi:type="string">Thank you for your purchase!</argument>
</action>
</referenceBlock>
<referenceContainer name="content">
<block class="Magento\Checkout\Block\Success" name="checkout.success" template="Magento_Checkout::success.phtml" cacheable="false">
<container name="order.success.additional.info" label="Order Success Additional Info"/>
</block>
<block class="Magento\Checkout\Block\Registration" name="checkout.registration" template="Magento_Checkout::registration.phtml" cacheable="false"/>
</referenceContainer>
</body>
</page>
Далее создаем сам блок custom_module/Block/Success.php в котором и будем производить все манипуляции:
namespace Magento\Ziina\Block;
use Magento\Sales\Model\Order;
class Success extends \Magento\Framework\View\Element\Template {
/**
* @var \Magento\Sales\Model\OrderFactory
*/
protected $_orderFactory;
/**
* @param \Magento\Framework\View\Element\Template\Context $context
* @param \Magento\Sales\Model\OrderFactory $orderFactory
* @param array $data
*
* @codeCoverageIgnore
*
*/
public function __construct(
\Magento\Framework\View\Element\Template\Context $context,
\Magento\Sales\Model\OrderFactory $orderFactory,
\Magento\Checkout\Model\Session $checkoutSession,
array $data = []
) {
$this->_orderFactory = $orderFactory;
parent::__construct( $context, $data );
// Получаем объект нашего заказа
$order = $checkoutSession->getLastRealOrder();
// Получаем объект платежа в котором у нас записана ссылка на страницу оплаты
$payment = $order->getPayment();
// Получаем ссылку на страницу оплаты
$url = $payment->getAdditionalInformation( 'ziina_url' );
if ( ! empty( $url ) ) { // Если ссылка есть, значит пользователь еще не оплатил заказ и необходимо его отправить на оплату
// Удаляем ссылку из объекта платежа чтобы при следующем заходе сработал второй сценарий
$payment->setAdditionalInformation( 'ziina_url', false );
$payment->save();
// Устанавливаем статус заказа
$orderState = Order::STATE_PENDING_PAYMENT;
$order->setState( $orderState )->setStatus( $orderState );
$order->save();
// Перезаписываем данные сессии для возможности зайти на этой страницу повторно в случае успешной оплаты
$checkoutSession->setQuoteId( $order->getQouteId() );
$checkoutSession->setLastQuoteId( $order->getQuoteId() );
$checkoutSession->setLastSuccessQuoteId( $order->getQuoteId() );
$checkoutSession->setLastOrderId( $order->getEntityId() );
// Делаем редирект
header( "Location: " . $url );
exit();
} else { // Второй сценарий когда пользователь успешно оплатил заказ и нужно показать ему страницу успешной оплаты
if ( ! empty( $_GET['gateway'] ) && $_GET['gateway'] == 'ziina' ) {
// Устанавливаем статус заказа Processing
$orderState = Order::STATE_PROCESSING;
$order->setState( $orderState )->setStatus( $orderState );
$order->save();
// Обновляем данные сессии
$checkoutSession->setQuoteId( $order->getQouteId() );
$checkoutSession->setLastQuoteId( $order->getQuoteId() );
$checkoutSession->setLastSuccessQuoteId( $order->getQuoteId() );
$checkoutSession->setLastOrderId( $order->getEntityId() );
}
}
}
/**
* @return int
*/
public function getRealOrderId() {
/** @var \Magento\Sales\Model\Order $order */
$order = $this->_orderFactory->create()->load( $this->getLastOrderId() );
return $order->getIncrementId();
}
}
Как видите, в данном блоке в конструкторе класса прописывается вся логика работы. Установлено два сценария:
- Пользователь еще не оплатил – мы получаем ссылку из объекта оплаты которую записали ранее, удаляем ее оттуда чтобы данный сценарий не повторился и делаем редирект на нее.
- Пользователь заходит на данную страницу уже после успешной оплаты – видит страницу успешной оплаты.
Самое неприятное, что система не позволяет заходить на страницу успешной оплаты дважды очищая сессию, чтобы этого избежать нужно перезаписать данные в сессии чтобы система позволила зайти сюда еще раз.
Заключение
В данной системе чтобы вмешаться в стандартную логику работы приходится изобретать костыли, которые позволят сделать ее более гибкой. Возможно если бы на просторах интернета было больше документации или хотя бы дельных советов с системой было бы работать гораздо проще, но пока этого нет рекомендую выбирать более распространенные и адаптированные под изменения системы, по крайней мере для разработки платежного шлюза
Комментирование этой и других статей доступно в нашем Телеграм канале