Частичная оплата заказа с помощью Robokassa

Частичная оплата заказа с помощью Robokassa

При разработке одного интернет-магазина, нам необходимо было дать пользователям частично оплатить заказ, при этом, сумма первого платежа определялась системой согласно бизнес-логике заказчика. После обработки заказа менеджером магазина пользователь мог оплатить оставшуюся часть заказа тем же способом, что и первую часть. В этом посте мы покажем как можно реализовать частичную оплату заказа в интернет-магазине на 1С-Битрикс с помощью платежной системы Robokassa.

API Robokassa

Robokassa имеет достаточно обширный API и первым делом при выполнении этой задачи мы заглянули туда. В документации имеется описание совершения платежа пользователем произвольной суммы:

Оплата произвольной суммы

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

Частичная оплата Robokassa

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

Возникает еще одна проблема. Ответ платежной системы будет приходить на ту же страницу-обработчик результата, на которую приходят ответы на заказы, что оплачиваются пользователями полностью. Нужно научиться как-то различать ответы частичных и полных оплат и по-разному их обрабатывать. В этом нам также поможет документация Robokassa. API Robokassa позволяет отправлять системе произвольные параметры вместе с обязательными полями запроса (логин, контрольная сумма и т.д.). Суть этих произвольных параметров в том, что они никак не обрабатываются Робокассой, но они приходят в ответе обратно в магазин. Вот цитата из документации:

Дополнительные пользовательские параметры

Они также относятся к необязательным параметрам, но несут совершенно другую смысловую нагрузку. Это такие параметры, которые ROBOKASSA никак не обрабатывает, но всегда возвращает магазину в ответных вызовах.

Если:
  • Вы собираетесь создавать магазин, в котором предусмотрено большое количество товаров, разделов и типов товара.
  • Ваш сайт будет предоставлять разнообразные услуги, не похожие друг на друга.
  • На одном сайте работают несколько ресурсов.
  • И самое распространённое — Вам требуется использовать дополнительную идентификацию Ваших клиентов, например, знать его ID или Логин у Вас на сайте.

То при старте операции оплаты Вы можете передавать всю эту информацию. При завершении операции оплаты, мы будем возвращать Вам эти дополнительные параметры.

Вся необходимая информация собрана и можно перейти к реализации.

Реализация

В описанном ниже варианте реализации мы добавили на финальную страницу оформления заказа блок с информацией о возможности оплаты заказа частично. Естественно, этот блок появляется при выполнении условий, заданных бизнес-логикой. Вот как выглядит этот блок:

Кнопка частичной олпаты заказа

После клика на кнопку "Оплатить часть заказа" и прохождения этапов оплаты, система сохранит данные о том, что было оплачено 200 руб. из всей суммы заказа. Этот платеж так же можно увидеть во вкладке «История изменений», где сохранено подтверждение того, что часть заказа была оплачена. После этих действий на странице оплаты заказа уже не будет кнопки «Оплатить часть заказа», а система предложит оплатить только остаток от суммы:

Результать оплаты части заказа

Далее рассмотрим что происходит внутри.

Под капотом

Основной код для реализации частичной оплаты организован в виде PHP-класса (RobokassaPayment), который будет описан ниже. Но для начала мы посмотрим на финальную страницу оформления заказа - confirm.php. В месте, где подключается обработчик платежной системы для оплаты заказа покупателем, мы внедряем свою логику для осуществления частичной оплаты заказа. Здесь, сразу после подключения стандартного обработчика платежной системы, мы проверяем, что платежная система заказа равна Робокассе и что заказ еще не был оплачен частями. Затем выводим текст про возможность оплаты частями и вызываем метод, который возвращает нам нужные поля для отправки в платежную систему и вставляем их в форму с кнопкой. Перед формой так же проверяется может ли пользователь сделать оплату частями в зависимости от состава заказа, но на этом останавливаться подробнее не будем.


Файл home/путь_к_шаблонам_компонентов/sale.order.ajax/confirm.php
...

if (strlen($arResult["PAY_SYSTEM"]["PATH_TO_ACTION"])>0)
{
    include($arResult["PAY_SYSTEM"]["PATH_TO_ACTION"]);

    if(DVS\Order\RobokassaPayment::isRobokassa($arResult["ORDER"]["PAY_SYSTEM_ID"])
        && $arResult["ORDER"]["SUM_PAID"] == "0.00")
    {
        $obKassa = new DVS\Order\RobokassaPayment($_REQUEST["ORDER_ID"]);
        $arPartPayForm = $obKassa->getFormData();
        ?>
        <?if(($arPartPayForm["PART_PAY_SUM"] !== floatval($arResult["ORDER"]["PRICE"]))
            && $arPartPayForm["PART_PAY_SUM"] !== 0):?>
            <hr />
            <p>Вы так же можете оплатить часть заказа на <b>сумму <?=$arPartPayForm["PART_PAY_SUM"]?> руб.</b>, равной сумме стоимостей штучных товаров в заказе</p>
            <form action="<?=$arPartPayForm["ACTION"]?>" method="POST">
                <?=$arPartPayForm["HIDDEN"]?>
                <input type="submit" value="Оплатить часть заказа">
            </form>
        <?endif;?>
    <?}
}

...

Нажимая кнопку "Оплатить часть заказа" пользователь отправляется в платежную систему и делает оплату. Платежная система отправляет ответ с результатами на страницу-обработчик, указанную в ее настройках. На этой странице мы должны проверить произвольный параметр, который мы отправили при запросе на частичную оплату, для того, чтоб определить тип заказа, по которому система присылает ответ. Если в ответе мы получаем произвольный параметр (в нашем случае это SHP_PART_PAY), то мы подключаем обработку частичной оплаты с помощью написанного класса, иначе - подключаем стандартный компонент sale.order.payment.receive. Вот как выглядит код этой страницы:


Файл home/payment/result.php
...

if(isset($_POST["SHP_PART_PAY"]) && $_POST["SHP_PART_PAY"] == "Y")
{
    $obKassa = new DVS\Order\RobokassaPayment($_POST["InvId"]);
    $obKassa->initPartPayment($_POST);
}
else
{
    $APPLICATION->IncludeComponent(
        "bitrix:sale.order.payment.receive",
        "",
        Array(
            "PAY_SYSTEM_ID" => "10",
            "PERSON_TYPE_ID" => "1"
        )
    );

}

...

Ниже предоставляем исходный код класса, и описание ключевых его методов.

  • __construct() - Конструктор принимает в параметре ID заказа. Сохраняет в свойствах объекта настройки платежной системы и номер заказа.
  • isRobokassa() - Принимает в параметре ID платежной системы и проверяет является ли эта платежная система Робокассой.
  • getFormData() - Возвращает массив данных для построения формы частичной оплаты. В нем возвращаются ссылка куда отправлять запрос, сумма частичной оплаты, и html-код скрытых полей с данными для робокассы.
  • initPartPayment() - принимает в параметре массив $_POST, выполняет расшифровку ответа от Робокассы и заносит данные об оплате в заказ.
  • getPartPaySum() - Возвращает сумму для частичной оплаты заказа. Если заказ нельзя оплатить частично, то должна вернуть 0


<?php
namespace DVS\Order;

use \Bitrix\Main\Loader;

/**
 * Class for Robokassa API integration
 *
 * Class RobokassaPayment
 * @package DVS\Order
 */
class RobokassaPayment 
{
    const PAY_SYSTEM_ID = 4;
    const PERSON_TYPE_ID = 1;

    public $paySystemParams = array();
    private $merchantLogin = "";
    private $pass1 = "";
    private $pass2 = "";
    private $orderId = 0;

    public function __construct($orderId)
    {
        if(Loader::includeModule("sale"))
        {
            $this->paySystemParams = $this->getPaySystemConfig();

            if(count($this->paySystemParams) > 0)
            {
                $this->merchantLogin = $this->paySystemParams["ShopLogin"];
                $this->pass1 = $this->paySystemParams["ShopPassword"];
                $this->pass2 = $this->paySystemParams["ShopPassword2"];
            }

            $this->orderId = intval($orderId);
        }
    }

    /**
     * Returns pay system settings stored in DB
     * @return array|mixed
     */
    private function getPaySystemConfig()
    {
        $arSelect = array("PARAMS");
        $arFilter = array("PAY_SYSTEM_ID" => self::PAY_SYSTEM_ID, "PERSON_TYPE_ID" => self::PERSON_TYPE_ID);

        $res = \CSalePaySystemAction::GetList(
            array(),
            $arFilter,
            false,
            false,
            $arSelect
        );
        if ($arFields = $res->GetNext())
        {
            return unserialize($arFields["~PARAMS"]);
        }

        return array();
    }

    /**
     * Returns hash of required parameters including flag SHP_PART_PAY which means that only part of order price will
     * be payed
     *
     * @param $def_sum
     * @return string
     */
    public function getPartPaySignature($def_sum)
    {
        $login = $this->getLogin();
        $pass = $this->getPass1();

        return md5("$login:$def_sum:$this->orderId:$pass:SHP_PART_PAY=Y");
    }

    /**
     * Returns hash for comparison with hash that pay system posts
     *
     * @param $def_sum
     * @return string
     */
    public function getPartPayCheckSignature($def_sum)
    {
        $pass2 = $this->getPass2();
        $hash = md5("$def_sum:$this->orderId:$pass2:SHP_PART_PAY=Y");

        return strtoupper($hash);
    }

    /**
     * Initiates partial payment of order. $post must contain pay system result response
     * Checks pay system signature from $post, updates order's SUM_PAID field and makes new record in order's change log
     *
     * @param $post
     */
    public function initPartPayment($post)
    {
        if($this->checkSignature($post["SignatureValue"], $post["OutSum"]))
        {
            $db_sales = \CSaleOrder::GetList(
                array("DATE_INSERT" => "ASC"),
                array("ID" => $this->orderId),
                false,
                false,
                array("PRICE", "SUM_PAID")
            );
            if ($ar_sales = $db_sales->Fetch())
            {
                $arFields = array(
                    "SUM_PAID" => $ar_sales["SUM_PAID"] + $post["OutSum"],
                    "STATUS_ID" => "H"
                );
                if(\CSaleOrder::Update($this->orderId, $arFields))
                {
                    $arChangeFields = array(
                        "ORDER_ID" => $this->orderId,
                        "TYPE" => "ORDER_PAYED",
                        "DATA" => serialize(array('PAYED' => 'Y')),
                        "DATE_CREATE" => ConvertTimeStamp(time(), "FULL"),
                        "DATE_MODIFY" => ConvertTimeStamp(time(), "FULL"),
                        "USER_ID" => 1
                    );
                    \CSaleOrderChange::Add($arChangeFields);
                }
            }
        }
    }

    /**
     * Returns true if $signature posted by pay system for current order and partial invoice to the amount of $def_sum
     * equals expected signature. Else returns false
     *
     * @param $signature
     * @param $def_sum
     * @return bool
     */
    private function checkSignature($signature, $def_sum)
    {
        if(strlen($signature) == 32)
        {
            $expectedSignature = $this->getPartPayCheckSignature($def_sum);

            if($expectedSignature == $signature)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }

    /**
     * Checks is $paySystemId equals Robokassa pay system id
     *
     * @param $paySystemId
     * @return bool
     */
    public static function isRobokassa($paySystemId)
    {
        if(intval($paySystemId) == self::PAY_SYSTEM_ID)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * Returns array of partial form data.
     * Keys:
     *  ACTION - form action value
     *  PART_PAY_SUM - partial pay invoice sum
     *  HIDDEN - hidden values for the pay system
     *
     * @return array
     */
    public function getFormData()
    {
        $arResult = array();

        $mrh_login = $this->getLogin();
        $inv_id = $this->orderId;
        $inv_desc = "Частичная оплата заказа №".$this->orderId;
        $def_sum = $this->getPartPaySum();
        $crc = $this->getPartPaySignature($def_sum);

        $arResult["ACTION"] = $this->getFormAction();
        $arResult["PART_PAY_SUM"] = $def_sum;

        ob_start();
        ?>
        <input type="hidden" name="MrchLogin" value="<?=$mrh_login?>">
        <input type="hidden" name="OutSum" value="<?=$def_sum?>">
        <input type="hidden" name="InvId" value="<?=$inv_id?>">
        <input type="hidden" name="Desc" value="<?=$inv_desc?>">
        <input type="hidden" name="SignatureValue" value="<?=$crc?>">
        <input type="hidden" name="SHP_PART_PAY" value="Y">
        <?
        $arResult["HIDDEN"] = ob_get_clean();

        return $arResult;
    }

    /**
     * Returns true if pay system test mode enabled or return false if not
     * @return bool
     */
    private function isTestMode()
    {
        if($this->paySystemParams["IS_TEST"]["VALUE"] == "Y")
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * Returns url for queries to pay system in addiction of test mode
     *
     * @return string
     */
    private function getFormAction()
    {
        if($this->isTestMode())
        {
            return "http://test.robokassa.ru/Index.aspx";
        }
        else
        {
            return "https://merchant.roboxchange.com/Index.aspx";
        }
    }

    /**
     * Counts piece products in order and returns its total price.
     * If there are no piece products in order - returns 0
     * @return int
     */
    private function getPartPaySum()
    {
        //ваш код для определения суммы частичной оплаты. Номер заказа лежит в $this->orderId.
    }

    public function getLogin()
    {
        return $this->merchantLogin["VALUE"];
    }

    public function getPass1()
    {
        return $this->pass1["VALUE"];
    }

    public function getPass2()
    {
        return $this->pass2["VALUE"];
    }

    public function getOrderId()
    {
        return $this->orderId;
    }
}

Ссылки по теме

Комментарии

Тут без вас никак. Поделитесь с нами вашими мыслями

  • как это связка работает при выгрузке заказов в 1С
  • Точно так же, как если бы клиент оплатил часть заказа другим способом. Например, из внутреннего счета Битрикс
Горячие вакансии