Разработка плагина для способа оплаты

С версии Moguta.CMS 10.9.0 способы оплаты являются плагинами.
Прежде чем приступить к разработке оплаты, рекомендуем ознакомиться с инструкцией по разработке плагинов.


  1. Убедитесь, что у вас на сайте способы оплаты работают из плагинов. Как включить новую систему оплат?
  2. Ознакомьтесь с таблицей оплат в базе данных (Таблица #PREFIX#payment).
    Это та же таблица, что используется и в старой системе, но в ней добавлены несколько новых полей (code, plugin, icon) и убрано устаревшее поле "add_security".

     
    НазваниеТипОписание
    namevarchar(1024) Название оплаты для менеджера (Отображается в административной панели). По умолчанию - пустая строка;
    public_namevarchar(1024) Название оплаты для клиента (Отображается в публичной части сайта). По умолчанию - пустая строка;
    activityint(1) Флаг активности оплаты. По умолчанию - 0;
    paramArraytextJSON массив параметров оплаты;
    urlArrayvarchar(1024) JSON массив ссылок оплаты (отображаются в настройках оплаты);
    ratedoubleСкидка/Наценка на способ оплаты. Коэффициент изменения стоимости, т. е. значение 0.1 означает наценку в 10%, а значение -0.05 - скидку в 5%;
    sortint(11) Сортировка оплаты. Чем выше число - тем выше по списку отображается оплата;
    permissionvarchar(5)Кому доступна оплата. all - всем, fiz - только физическим лицам, yur - только юридическим;
    codevarchar(5)UNIQUE Уникальный текстовый идентификатор оплаты;
    pluginvarchar(255)Название папки плагина оплаты;
    iconvarchar(511)Ссылка на иконку оплаты (Отображается в публичной части сайта и в разделе заказов административной панели);
    logstinyint(1)Флаг поддержки логирования. 1 - оплата поддерживает логирование, 0 - не поддерживает.

    И новые и старые оплаты хранятся в этой таблице одновременно, но у старых оплат code всегда имеет вид "old#(идентификатор оплаты)", а у новых задаётся из плагина оплаты (как правило это название папки плагина) или имеет вид "custom#(идентификатор оплаты)", если это пользовательская оплата.

  3. Ознакомьтесь с документацией к модели оплаты. Большая часть методов этой модели используются в системе, а для разработки плагина понадобятся всего несколько:
    • Метод для добавления способа оплаты, нужно использовать при активации плагина оплаты через mgActivateThisPlugin.
    • Метод отрисовки формы оплаты. Сам метод напрямую вызывать не нужно, он создаёт важный хук отрисовки формы оплаты, уникальный для каждого плагина. Если пользователь оформляет заказ с оплатой, которая работает с плагином "payment-example", то, во время отрисовки формы оплаты, создастся хук "payment-example". Плагин должен обработать этот хук и отдать html с формой, которая отобразиться после оформления заказа.
    • Метод перехвата запросов в контроллер оплат. Аналогично методу выше, вызывать его напрямую не нужно, он тоже создаёт хук, который нужно обработать в плагине - удостовериться, что запрос предназначен плагину,  проверить платёж, сделать заказ оплаченным через метод actionWhenPayment, если для этого соблюдены все условия. Как правило, этот метод срабатывает когда приходит нотификация от банка или пользователь возвращается на сайт после оплаты.
    • Метод для получения настроек оплаты. Возвращает удобный, для использования в коде, массив со всеми настройками оплаты, которые были заданы во время создания оплаты и заполнены пользователем в административной панели.
    • Методы для получения самой оплаты, такие как getPaymentByCodegetPaymentByPlugin, реже getPaymentById или getPayment.
    • Метод для записи логов. Такие логи защищены от просмотра 3-ми лицами, структурированы, их можно включить или выключить в настройках оплаты, а, также, скачать или удалить там же. Рекомендуем использовать этот метод на разных этапах работы плагина для сбора отладочной информации на случай, если у пользователя возникнут какие-либо проблемы с оплатой. Особенно в местах, в которых происходят запросы к банкам или сбор данных для отрисовки формы оплаты.
  4. Используйте шаблон плагина способа оплаты или пример плагина способа оплаты Qiwi для создания плагина.
Рассмотрим работу плагина оплаты на примере плагина оплаты Qiwi.

test

  1. Структура плагина:
    • index.php - файл с основным программным кодом плагина и его метаданными;
    • views/form.php - файл с вёрсткой формы оплаты. Рекомендуется выносить отдельно, чтобы в коробочных версиях было проще доработать внешний вид формы или её содержимое;
    • src/icon.png - иконка оплаты, её мы сразу укажем во время создания оплаты для вывода в публичной части сайта и странице заказов административной.

  2.  Метаданные плагина, такие же как и в любом другом плагине, с одним важным отличием, в конце должно быть указано "Edititon: PAYMENT". Именно по этому параметру движок сможет понять, что плагин является плагином оплаты и будет выводить его в разделе настроек оплат, а не в разделе плагины.

  3. Конструктор плагина добавляет обработку 3-х важных хуков.
    • При активации плагина будет вызван метод "activate", в котором создаётся оплата;
    • На событие "payment-qiwi" будет вызван метод "getOrderForm", который возвращает html форму оплаты;
    • На событие "Models_Payment_handleRequest" будет вызван метод "webhookIntercept", который перехватывает нотификацию от банка и, при соблюдении всех условий, делает заказ оплаченным.

    new PaymentQiwi;
    
    class PaymentQiwi {
        public static $pluginName = 'payment-qiwi';
    
        public function __construct() {
            mgActivateThisPlugin(__FILE__, [__CLASS__, 'activate']);
    
            mgAddAction(self::$pluginName, [__CLASS__, 'getOrderForm'], 1);
    
            mgAddAction('Models_Payment_handleRequest', [__CLASS__, 'webhookIntercept'], 1);
        }
    ...
  4. Метод activate проверяет есть ли оплата и если нет, то создаёт её используя метод Models_Payment::addPayment.

    public static function activate() {
        $currentPayment = Models_Payment::getPaymentByPlugin(self::$pluginName);
        if (!$currentPayment) {
            $name = 'Qiwi';
    
            $icon = SITE.'/mg-plugins/'.self::$pluginName.'/src/icon.png';
    
            $defaultParams = [
                [
                    'name' => 'publicKey',
                    'title' => 'Публичный ключ',
                    'type' => 'text',
                    'value' => '',
                ],
                [
                    'name' => 'secretKey',
                    'title' => 'Секретный ключ',
                    'type' => 'crypt',
                    'value' => '',
                ],
            ];
    
            $urls = [
                [
                    'type' => 'info',
                    'title' => 'Result URL',
                    'link' => SITE.'/payment?payment=qiwi',
                ],
            ];
    
            Models_Payment::addPayment(
                self::$pluginName,
                $name,
                $name,
                self::$pluginName,
                $defaultParams,
                $icon,
                0,
                $urls
            );
        }
    }
  5. Метод getOrderForm подготавливает переменные, такие как $uniqOrderId, $publicKey, $orderAmount, $currency, $phone, $email, $successUrl и отдаёт html форму оплаты, полученную через буферизацию вывода файла views/form.php,  в котором эти переменные используются.
    public static function getOrderForm($args) {
        $result = $args['result'];
        $orderId = $args['args'][1];
        $orderModel = new Models_Order();
        $orders = $orderModel->getOrder('`id` = '.DB::quoteInt($orderId));
        $order = $orders[$orderId];
    
        $uniqOrderId = $orderId.'-'.time();
    
        $options = Models_Payment::getPaymentParams(self::$pluginName, true);
        $publicKey = $options['publicKey'];
    
        // Отменяем предыдущий неоплаченный заказ
        if (!empty($_SESSION['qiwiApi']['orderID'])) {
            $lastOrderId = $_SESSION['qiwiApi']['orderID'];
    
            $secretKey = $options['secretKey'];
    
            $url = 'https://api.qiwi.com/partner/bill/v1/bills/'.$lastOrderId.'/reject';
    
            $headers = [
                'Accept: application/json',
                'Content-Type: application/json',
                'Authorization: Bearer '.$secretKey,
            ];
    
            $curlHandler = curl_init($url);
            $curlOptions = [
                CURLOPT_POST => true,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER => $headers,
            ];
            curl_setopt_array($curlHandler, $curlOptions);
            curl_exec($curlHandler);
            curl_close($curlHandler);
        }
    
        $orderAmount = round($order['summ'] + $order['delivery_cost'], 2);
    
        $currency = MG::getSetting('shopCurrencyIso');
        if ($currency === 'RUR') {
            $currency = 'RUB';
        }
        
        $phone = '';
        if (!empty($order['phone'])) {
            $phone = str_replace([' ', '(', ')', '-'], '', $order['phone']);
            $phone = str_replace('+7', '8', $phone);
        }
    
        $email = $order['user_email'];
        if (empty($email)) {
            $email = $order['contact_email'];
        }
    
        $successUrl = SITE.'/payment?payment=qiwi';
    
        $_SESSION['qiwiApi']['orderID'] = $uniqOrderId;
    
        ob_start();
        require('views/form.php');
        $result = ob_get_contents();
        ob_end_clean();
    
        return $result;
    }
    <style>
      #qiwiSendForm {
        background: rgb(255, 140, 0);
        color: white;
        border-radius: 24px;
        font-weight: 500;
        padding: 15px 50px;
      }
    
      #qiwiSendForm:hover {
        background: rgb(255, 130, 0);
      }
    style>
    
    <form method="get" action="https://oplata.qiwi.com/create" accept-charset="UTF-8">
      <input type="hidden" name="publicKey" value="" />
      <input type="hidden" name="billId" value="" />
      <input type="hidden" name="amount" value="" />
      <input type="hidden" name="phone" value="" />
      <input type="hidden" name="email" value="" />
      <input type="hidden" name="successUrl" value="" />
      <input type="submit" value="Оплатить" id="qiwiSendForm">
    form>
  6. Метод webhookIntercept перехватывает запрос в контроллер оплат, удостоверяется, что этот запрос именно для оплаты этого плагина и пришли все необходимые данные. Проверяет, что заказ оплачен в банке и делает его оплаченным в магазине через метод Controllers_Payment::actionWhenPayment.
    public static function webhookIntercept($args) {
        $result = $args['result'];
        if ($_GET['payment'] !== 'qiwi') {
            return $result;
        }
    
        $payment = Models_Payment::getPaymentByCode(self::$pluginName, true);
        $options = $payment['paramArray'];
    
        $secretKey = $options['secretKey'];
    
        $lastOrderId = $_SESSION['qiwiApi']['orderID'];
    
        $url = 'https://api.qiwi.com/partner/bill/v1/bills/' . $lastOrderId;
        $headers = [
            'Accept: application/json',
            'Content-Type: application/json',
            'Authorization: Bearer ' . $secretKey,
        ];
        $curlOptions = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
        ];
    
        $curlHandler = curl_init($url);
        curl_setopt_array($curlHandler, $curlOptions);
    
        $response = curl_exec($curlHandler);
        curl_close($curlHandler);
    
        $response = json_decode($response, true);
        $status = $response['status']['value'];
    
        if ($status === 'PAID') {
            $orderId = preg_replace('/-.*/is', '', $response['billId']);
            $orderAmount = $response['amount']['value'];
    
            $orderModel = new Models_Order();
            $orders = $orderModel->getOrder(
                '`id` => ' . DB::quoteInt($orderId) . ' AND ' .
                'ROUND(`summ` + `delivery_cost`) = ' . DB::quoteFloat($orderAmount) . ' AND ' .
                '`payment_id` = ' . DB::quoteInt($payment['id'])
            );
    
            if (empty($orders)) {
                $result = [
                    'status' => 'fail',
                    'message' => 'ERR: Заказ был изменен! Была произведена оплата ' . $orderAmount . ' ' . $response['amount']['currency'] . ' по некорректному счету!',
                ];
                return $result;
            } else {
                $order = array_shift($orders);
                $result = [
                    'status' => 'success',
                    'message' => 'Вы успешно оплатили заказ. Спасибо!',
                ];
                if (intval($order['status_id']) === 0 || intval($order['status_id']) === 1) {
                    Controllers_Payment::actionWhenPayment([
                        'paymentOrderId' => $orderId,
                        'paymentAmount' => $orderAmount,
                        'paymentID' => $payment['id']
                    ]);
                }
            }
        } else {
            $result = [
                'status' => 'fail',
                'message' => 'Возникла ошибка в оплате заказа.',
            ];
        }
    
        return $result;
    }

Примечание: В системе предусмотрены стандартные страницы для успешной или неудачной оплаты, куда можно пренаправить пользователя после оплаты. Это #SITE#/payment?payStatus=success и #SITE#/payment?payStatus=fail соответственно. Добавьте их в список ссылок при создании оплаты через метод Models_Payment::addPayment, если их можно указать в настройках перенаправления в банке.