04.07.2019

Покупка в один клик

Одна из распространенных задач - это покупка в один клик. Очень часто на сайтах ее реализуют не правильно. И дело не во внешнем виде или количестве полей.

Благодаря множеству исследований в области интернет-маркетинга, стало известно, что чем меньшее количество кликов необходимо сделать пользователю в процессе совершения транзакции, тем больший их процент будет успешным.Если в двух словах – при снижении числа необходимых кликов для совершения покупки уменьшается показатель отказов в процессе оформления заказа. Именно поэтому в ряде интернет-магазинов вместо классической формы заказа или в качестве дополнения к ней внедряется такая возможность, как «Покупка в один клик».

В интернет-магазинах, в том числе сделанных на готовых решениях 1C-Битрикс, часто неправильно реализован функционал быстрого заказа «Купить в 1 клик». Речь пойдет не о количестве полей в форме, не о цвете кнопки (об этом есть другая статья), а о технической реализации – том, как полученные заказы сохраняются на сайте и как менеджер интернет-магазина ведет с ними дальнейшую работу.

Если кратко, то:

Если у вас на сайте есть кнопка «Купить в 1 клик», то заказы, поступившие от покупателей, воспользовавшихся этой кнопкой, должны падать в общий список заказов. Туда же, куда сохраняются заказы, оформленные обычным способом.

Часто вижу что покупка в один клик добавляет заявки в инфоблок или веб форму. Так же видел вариант с простой отправкой письма или добавлением данных в свою ORM таблицу. Минусы такого подхода:

  • Сумма заказа не будет учитываться при расчете накопительной скидки
  • Заказ не «приклеивается» к профилю пользователя
  • Пользователь не увидит в Личном кабинете свои заказы, сделанные «в 1 клик»
  • Не отправляются уведомления пользователя при смене статуса их заказа
  • Заказы, хранящиеся в инфоблоке, не будут синхронизированы с 1С.
  • Заказы которые были добавлены таким способом менеджеру придётся добавлять вручную
  • Менеджеру неудобно работать с заказами, хранящимися в инфоблоках

Для покупки в один клик, я использую вот такую заготовку.

  • ajax popup из этой статьи
  • /ajax/oneClickBuy.php - вызов компонента
  • /local/components/gricuk/oneClickBuy/ - Компонент покупки в один клик.

Для ajax popup создаю кнопку покупки, например так:

<span class="btn btn-default js-ajax-popup" data-url="/ajax/oneClickBuy.php?product_id=<?=$arItem["ID"]?>">Купить в один клик</a>

Создаем в папке /ajax/ файл oneClickBuy.php. В нём размещаем следующий код:

<?
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

/** @var CMain $APPLICATION */

use Bitrix\Main\Context;

$request = Context::getCurrent()->getRequest();

if (is_null($request->get('product_id'))) {
    return;
}

$APPLICATION->IncludeComponent(
    'gricuk:oneClickBuy',
    '',
    [
        "MAX_ORDER_COUNT" => 10,
        "STATUS_ID" => "OC",
        "USER_GROUP_ID" => array(2, 5),
        "PAY_SYSTEM" => 1,
        "DELIVERY" => 3,
        'AJAX_MODE' => 'Y', 
        'AJAX_OPTION_SHADOW' => 'N', 
        'AJAX_OPTION_JUMP' => 'N', 
        'OFFERS_CART_PROPERTIES' => ["COLOR", "VOLUME"]
    ]
);

Теперь о параметрах компонента gricuk:oneClickBuy

  • MAX_ORDER_COUNT - максимальное количество не обработанных заказов пользователя. Своеобразная защита от спама.
  • STATUS_ID - Статус в котором будет создаваться заказ. Я предпочитаю создать отдельный статус заказа для такого варианта покупки, чтобы менеджеру было сразу видно такие заказы
  • USER_GROUP_ID - массив ID групп пользователя, в которые будет добавлен пользователь, в случае регистрации.
  • PAY_SYSTEM - ID платежной системы
  • DELIVERY - ID службы доставки
  • OFFERS_CART_PROPERTIES - свойства торговых предложений которые будут добавлены в корзину
  • AJAX_MODE, AJAX_OPTION_SHADOW, AJAX_OPTION_JUMP - стандартные параметры Bitrix для работы компонента в режиме ajax

Сам код компонента.
class.php

<?

use Bitrix\Main\Application;
use Bitrix\Main\Loader;
use Bitrix\Sale;

if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) die();

class OneClickBuy extends CBitrixComponent
{
    public function onPrepareComponentParams($arParams)
    {
        $request = Application::getInstance()->getContext()->getRequest();

        $arParams["FIELDS"] = array(
            "NAME" => array(
                "REQUIRED" => true,
                "NAME" => "Имя",
                "VALIDATOR" => "validName",
                "FROM_USER_PROFILE" => "Y"
            ),
            "PERSONAL_PHONE" => array(
                "REQUIRED" => true,
                "NAME" => "Телефон",
                "VALIDATOR" => "validPhone",
                "FROM_USER_PROFILE" => "Y",
                "DESCRIPTION" => "Мы свяжемся с Вами по указанному номеру."
            ),

            "EMAIL" => array(
                "REQUIRED" => true,
                "NAME" => "Email",
                "VALIDATOR" => "validEmail",
                "FROM_USER_PROFILE" => "Y",
                "DESCRIPTION" => "Мы свяжемся с Вами по указанному номеру."
            ),
        );

        return $arParams;
    }

    public function executeComponent()
    {
        $request = Application::getInstance()->getContext()->getRequest();
        $product = $request->get("product_id");
        $action = $request->get("action");

        $this->arResult["FORM"]["ACTION"] = POST_FORM_ACTION_URI;
        $this->arResult["FIELDS"] = $this->arParams["FIELDS"];
        $this->arResult["USER"] = $this->getUser();

        if ((intval($this->arResult["USER"]["ID"]) > 0) && (($countExistOrders = $this->getCountExistOrders()) > $this->arParams["MAX_ORDER_COUNT"])) {
            $this->arResult["MESSAGES"][] = array(
                "TYPE" => "ERROR",
                "MESSAGE" => "Вами оформлено {$countExistOrders} заказов  в 1 клик, дождите окончания обработки всех текущих заказов или пройдите полную процедуру оформления закза"
            );

            $this->arResult["DONT_SHOW_FORM"] = true;
        }

        if (empty($this->arResult["MESSAGES"])) {
            if ($product != "") {
                if ($action != "addOrder") {
                    if ($this->arResult["USER"]) {
                        foreach ($this->arResult["FIELDS"] as $code => $field) {
                            if (($field["FROM_USER_PROFILE"] == "Y") && (isset($this->arResult["USER"][$code]))) {
                                $this->arResult["FIELDS"][$code]["VALUE"] = $this->arResult["USER"][$code];
                            }
                        }
                    }
                } else {
                    $allFieldValid = true;
                    foreach ($this->arParams["FIELDS"] as $code => $field) {
                        $fieldValue = $request->getPost($code);
                        $this->arResult["FIELDS"][$code]["VALUE"] = $fieldValue;
                        $isValid = ($fieldValue != "") || ($field["REQUIRED"] != true);

                        if (isset($field["VALIDATOR"])) {
                            $isValid &= call_user_func(array($this, $field["VALIDATOR"]), $fieldValue);
                        }

                        if (!$isValid) {
                            $allFieldValid = false;
                            $this->arResult["MESSAGES"][] = array(
                                "TYPE" => "ERROR",
                                "MESSAGE" => "Не верный формат поля " . $field["NAME"]
                            );;
                        }

                    }
                    if ($allFieldValid) {
                        $this->addOrder($product);
                    }

                }
            } else {
                $this->arResult["MESSAGES"][] = array(
                    "TYPE" => "ERROR",
                    "MESSAGE" => "Не передан ID товара"
                );;
            }
        }


        $this->includeComponentTemplate();
    }

    private function getCountExistOrders()
    {
        Loader::includeModule('sale');
        $dbOrders = \Bitrix\Sale\Order::getList(array(
            "select" => array("*"),
            "filter" => array(
                "USER_ID" => $this->arResult["USER"]["ID"],
                "STATUS_ID" => $this->arParams["STATUS_ID"]
            )
        ));


        return $dbOrders->getSelectedRowsCount();
    }

    private function addOrder($product)
    {
        Loader::includeModule('sale');
        Loader::includeModule('catalog');

        if (!$this->arResult["USER"]) {
            $email = $this->arResult["FIELDS"]["EMAIL"]["VALUE"];
            $dbUser = \Bitrix\Main\UserTable::getList(array(
                "select" => array("*"),
                "filter" => array(
                    "EMAIL" => $this->arResult["FIELDS"]["EMAIL"]["VALUE"]
                ),
            ));
            if ($arUser = $dbUser->fetch()) {
                $this->arResult["USER"] = $arUser;
            } else {
                $user = new CUser;
                $PASSWORD_LENGTH = 8;
                $pass = substr(str_shuffle(strtolower(sha1(rand() . time() . "Lorem i1psum dolor sit amet, consectetur 9adipiscing elit8, sed do eiusmod tempor incididunt ut labosr3e et dolore magna aliqua. Ut enim ad minims ve2niam, quis n678osdtrud exercitation 456ulla2mco laboris nisi ut aliquip ex ea commodo con7sequat. Dauis aute irure dolo3r in raeprehenderit in volupta4te velit esse cil5aslum dolor4e eu fugiat naulla pariatur. Excepteur sint occaecat cupidatat no4n proi1dent, sunt in culpa qui o9fficia deseru0nt mollit anim id est laborum."))), 0, $PASSWORD_LENGTH);

                $arFields = Array(
                    "NAME" => $this->arResult["FIELDS"]["NAME"]["VALUE"],
                    "EMAIL" => $email,
                    "LOGIN" => $email,
                    "LID" => SID,
                    "ACTIVE" => "Y",
                    "GROUP_ID" => $this->arParams["USER_GROUP_ID"],
                    "PASSWORD" => $pass,
                    "CONFIRM_PASSWORD" => $pass,
                    "PERSONAL_PHONE" => $this->arResult["FIELDS"]["PERSONAL_PHONE"]["VALUE"]
                );

                $ID = $user->Add($arFields);
                if (intval($ID) > 0) {
                    $arFields["ID"] = $ID;
                    $this->arResult["USER"] = $arFields;
                } else {
                    $this->arResult["MESSAGES"][] = array(
                        "TYPE" => "ERROR",
                        "MESSAGE" => "Ошибка при создании пользователя: " . $user->LAST_ERROR
                    );
                }
            }
        }

        if ((intval($this->arResult["USER"]["ID"]) > 0) && (($countExistOrders = $this->getCountExistOrders()) > $this->arParams["MAX_ORDER_COUNT"])) {
            $this->arResult["MESSAGES"][] = array(
                "TYPE" => "ERROR",
                "MESSAGE" => "Вами оформлено {$countExistOrders} заказов  в 1 клик, дождите окончания обработки всех текущих заказов или пройдите полную процедуру оформления закза"
            );

            $this->arResult["DONT_SHOW_FORM"] = true;
        } elseif (intval($this->arResult["USER"]["ID"]) > 0) {
            $siteId = Bitrix\Main\Context::getCurrent()->getSite();

            //////корзина
            $basket = Sale\Basket::create($siteId);
            $item = $basket->createItem('catalog', $product);

            $item->setFields(array(
                'QUANTITY' => 1,
                'CURRENCY' => Bitrix\Currency\CurrencyManager::getBaseCurrency(),
                'LID' => Bitrix\Main\Context::getCurrent()->getSite(),
                'PRODUCT_PROVIDER_CLASS' => 'CCatalogProductProvider',
            ));

            $arProduct = \Bitrix\Catalog\ProductTable::getById($product)->fetch();
            if ($arProduct["TYPE"] == \Bitrix\Catalog\ProductTable::TYPE_OFFER) {
                $productProperties = [];
                $parentProduct = \CCatalogSku::GetProductInfo($product);
                $productProperties = CIBlockPriceTools::GetOfferProperties(
                    $product,
                    $parentProduct["IBLOCK_ID"],
                    $this->arParams["OFFERS_CART_PROPERTIES"],
                    []
                );
                if (!empty($productProperties)) {
                    $basketItemPropertyCollection = $item->getPropertyCollection();
                    $basketItemPropertyCollection->setProperty($productProperties);
                }
            }


            $basket->save();


            ////////заказ
            $order = Bitrix\Sale\Order::create($siteId, $this->arResult["USER"]["ID"]);
            $order->setPersonTypeId(1);
            $order->setBasket($basket);
            $order->setField("STATUS_ID", $this->arParams["STATUS_ID"]);

            $orderProps = $order->getPropertyCollection();
            if ($phoneProp = $orderProps->getPhone()) {
                $phoneProp->setValue($this->arResult["FIELDS"]["PERSONAL_PHONE"]["VALUE"]);
            }
            if ($emailProp = $orderProps->getUserEmail()) {
                $emailProp->setValue($this->arResult["FIELDS"]["EMAIL"]["VALUE"]);
            }

            /////////оплата

            $shipmentCollection = $order->getShipmentCollection();
            $shipment = $shipmentCollection->createItem(
                Bitrix\Sale\Delivery\Services\Manager::getObjectById($this->arParams["DELIVERY"])
            );

            $shipmentItemCollection = $shipment->getShipmentItemCollection();

            foreach ($basket as $basketItem) {
                $item = $shipmentItemCollection->createItem($basketItem);
                $item->setQuantity($basketItem->getQuantity());
            }

            /////////////отгрузка
            $paymentCollection = $order->getPaymentCollection();
            $payment = $paymentCollection->createItem(
                Bitrix\Sale\PaySystem\Manager::getObjectById($this->arParams["PAY_SYSTEM"])
            );

            /** @var Bitrix\Sale\Payment $payment */
            $payment->setField("SUM", $order->getPrice());
            $payment->setField("CURRENCY", $order->getCurrency());


            $result = $order->save();
            if (!$result->isSuccess()) {
                $this->arResult["MESSAGES"][] = array(
                    "TYPE" => "ERROR",
                    "MESSAGE" => "Ошибка при создании заказа: " . implode(", ", $result->getErrorMessages())
                );
            }

            if ($result->isSuccess()) {
                $this->arResult["ORDER"] = $order;
                $this->arResult["SUCCESS_ADD"] = "Y";
                $this->arResult["MESSAGES"][] = array(
                    "TYPE" => "OK",
                    "MESSAGE" => isset($this->arParams["OK_MESSAGE"]) ? $this->arParams["OK_MESSAGE"] : "Заказ успешно оформлен"
                );
            } else {
                $this->arResult["MESSAGES"][] = array(
                    "TYPE" => "ERROR",
                    "MESSAGE" => "Ошибка при создании заказа: " . $result->getErrors()
                );
            }
        }
    }

    /** Получение информации о текущем пользователе.
     * @return array
     */
    private function getUser()
    {
        global $USER;
        return CUser::GetByID($USER->GetID())->Fetch();
    }

    /*
     * Валидатор телефона
     *
     * @return bool
     */
    private function validPhone($phone)
    {
        $re = '/^((8|\+7)[\- ]?)?(\(?\d{3}\)?[\- ]?)?([\d\- ]{7,10})$/';
        $str = $phone;
        preg_match_all($re, $str, $matches, PREG_SET_ORDER, 0);
        return count($matches) > 0;
    }

    /*
     * Валидатор email
     *
     * @return bool
     */
    private function validEmail($email)
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    /*
     * Валидатор имени
     *
     * @return bool
     */
    private function validName($name)
    {
        return (bool)preg_match('/^[a-zA-Zа-яА-Я ]+$/ui', $name);
    }
}

template.php

<? use Bitrix\Main\Localization\Loc;

if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) die(); ?>
<?
/** @var array $arResult */

?>
<script>
    BX = top.BX;
    $ = top.$;
</script>
<div class="popup-wrap">
    <? if ($arResult["SUCCESS_ADD"] != "Y"): ?>
        <header class="popup-header"><?= Loc::getMessage("ONE_CLICK_BUY_POPUP_TITLE") ?></header>
    <? endif ?>
    <? if (isset($arResult["ORDER"])): ?>
        <?
        /** @var Bitrix\Sale\Order $order */
        $order = $arResult["ORDER"];
        ?>
        <header class="popup-header">
            Заказ # <?= $order->getId() ?>
            от <?= $order->getDateInsert()->format("d.m.Y") ?></header>
    <? endif ?>
    <div class="popup-content">
        <div class="popup-form form">
            <? if (isset($arResult["MESSAGES"]) && ($arResult["SUCCESS_ADD"] != "Y")): ?>
                <? foreach ($arResult["MESSAGES"] as $message): ?>
                    <? ShowMessage($message) ?>
                <? endforeach ?>
            <? endif ?>
            <? if (($arResult["SUCCESS_ADD"] != "Y") && !($arResult["DONT_SHOW_FORM"] === true)): ?>
                <form name="oneClickBuyForm" action="<?= $arResult["FORM"]["ACTION"] ?>" method="POST">
                    <input type="hidden" value="addOrder" name="action">
                    <? foreach ($arResult["FIELDS"] as $code => $field): ?>

                        <div class="form-group">
                            <label>
                                <?= $field["NAME"] ?>
                                <? if ($field["REQUIRED"]): ?>
                                    <span class="starrequired">*</span>
                                <? endif ?>
                            </label>

                            <input type="text" name="<?= $code ?>"
                                   <? if ($field["REQUIRED"]): ?>required=""<? endif ?>
                                   <? if ($field["VALUE"]): ?>value="<?= $field["VALUE"] ?>"<? endif ?>
                                   class="form-control">
                        </div>
                    <? endforeach ?>
                    <div class="form-group">
                        <button type="submit" name="button"
                                class="btn btn-primary">
                            <?= Loc::getMessage("ONE_CLICK_BUY_POPUP_ORDER_BTN") ?>
                        </button>
                    </div>
                </form>
                <script type="text/javascript">

                    function refreshInputs() {
                        $("input[name=PERSONAL_PHONE]").mask("+7 999 999 99 99");
                    }

                    setTimeout($.proxy(refreshInputs, top), 500);
                </script>
            <? else: ?>
                <div class="form-row">
                    <div style="max-width: 300px; min-width: 230px;">
                        Наши менеджеры свяжутся с Вами для уточнения удобных для Вас способов оплаты и доставки.
                        Вы можете следить за выполнением своего заказа в
                        <a class="link-green" href="/personal/">персональном разделе сайта</a>
                    </div>
                    <div class="form-row__btn text-center">
                        <button onclick="BX.PopupWindowManager.getCurrentPopup().close(); return false;"
                                name="button"
                                class="btn-green size-l btn btn-submit">
                            <?= Loc::getMessage("POPUP_CLOSE_BTN") ?>
                        </button>
                    </div>
                </div>
                <script type="text/javascript">
                    $(document).trigger("resize");
                </script>
            <? endif ?>
        </div>
    </div>
</div>

В итоге может получиться что то в этом духе: Покупка в один клик битрикс, one click buy bitrix