Оформление заказа

Что делать если не хватает функционала?

Если вы считаете, что не хватает какого-то общего функционала, то вы можете написать его самостоятельно и отправить pull request. Кратко для тех кто не сталкивался с опенсорсом. Вы делаете форк репозитория, в своем форке добавляете функционал и отправляете pull request из форка в оригинал.

Если вы хотите написать какой то функционал под конкретный проект, то у вас есть две пути — события и наследование компонента.

Что можно улучшить?

Для понимания масштаба, весь фреймворк symfony в минимальной комплектации это 13к строк в php файлах. Папка с компонентом sale.order.ajax это 11к строк в php файлах и 18,5к строк в js файлах. Эти цифры не для того чтобы кого-то обвинить, совсем нет. По ним вы можете грубо прикинуть количество времени необходимое для изучения темы.

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

Если бегло пройтись по функционалу, то помимо самого оформления штатный компонент авторизует старых пользователей (да, это прямо в коде компонента), регистрирует новых, работает с профилями покупателей, применяет скидочные купоны. Это всё полито толстым таким слоем галочек из параметров, обработкой событий и набором аякс методов. Тщательно размешиваем и получаем идеальный хаос 🙂

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

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

Похожее:  Login page with jQuery and AJAX

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

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

Базис

У компонента есть три отдельных части, три кита на которых всё держится. Каждый из них отвечает за свою область и строго специализирован. Постарайтесь понять цель каждой отдельной части.

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

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

Дальнейшие планы

Скорее всего будет пример шаблона на vue. Я пробовал сделать пример на bx.vue, но без транспиляции кода и импорта я чувствую себя очень не комфортно. Многие готовые компоненты vue не получается добавить подключением из CDN. Поэтому пока что отложил эту задачу. Посмотрим как это лучше всего реализовать. Планирую написать отдельную статью по процедуре заказа на vue.

Сам компонент будет развиваться. Есть некоторые вещи которые я хочу улучшить. Кардинально ничего не поменяется, вы всё так же можете рассчитывать на объект заказа и коллекцию ошибок в шаблоне.

Демонстрационные шаблоны

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

Дефолтный шаблон

Скрин с дефолтной формой заказа я приводил в начале статьи. Там есть пять основных блоков:

1. Таблица со списком свойств. В шаблоне реализовано отображение свойств типа строка, чекбокс Да/Нет, выбор из списка значений, заполнение даты и поиск местоположений.

При наличии ошибок по какому либо свойству, например если свойство не заполнено, или заполнено неверно, текст ошибки отобразится отдельно прямо в строке со свойством.

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

3. Выбор платежной системы. Блок из обычных радио инпутов плюс вывод списка ошибок связанных с оплатой.

4. Таблица с товарами в корзине. Текстовая информация, можно посмотреть как именно правильно доставать различные цифры из корзины, как получить значение скидки, как работать со свойствами товаров корзине и отображать единицы измерения.

5. Итоговая информация по заказу. Опять же просто информационный текст, в этом блоке ничего не заполняется. Я показал как можно отобразить девять самых популярных цифр по заказу. Выводить их или нет в своем шаблоне — решать вам.

Добавление на страницу

Вне зависимости от того как вы установили модуль, через композер или через маркетплейс, в любом случае открываем какую-либо страницу в визуальном редакторе и размещаем компонент на странице:

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

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

Зачем нужен компонент?

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

Штатный компонент оформления заказа работает хорошо. Как бы кто к нему не относился, в целом сама процедура заказа со стороны покупателя довольно продумана и постоянно совершенствуется. Со стороны разработчика компонент можно более менее модифицировать в каких то рамках. В связи с этим возникает вопрос — а надо ли что-то менять?

Каждый сам должен ответить на этот вопрос. Ответ будет зависеть от конкретного проекта, от бюджета, от сложности доработок, от нужной процедуры оформления. Универсального ответа тут не существует.

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

Если штатный шаблон вас в целом устраивает, то вполне можно жить используя разные хаки — что-то придумывать в событиях компонента, писать js скрипты для правок поверх шаблона, жонглировать стилями и т.д.

Однако же если вам нужна простая и лаконичная форма заказа, или более сложная, или даже просто другая, то проще отказаться от штатного шаблона вообще. И возможно от штатного компонента тоже.

Как использовать?

Что вам нужно сделать как программисту для интеграции верстки? В самом простом случае вам всего лишь нужно сформировать форму (html тэг <form>) в шаблоне компонента, которая при отправке передаст на сервер пять переменных:

  1. person_type_id. Переменная которая содержит тип плательщика.
  2. properties[]. Массив переменных со свойствами заказа. Например, если у свойства символьный код — FIO, то атрибут name у инпута ставьте properties[FIO]. Если переменная множественная то ставьте name=properties[FIO][]
  3. delivery_id. В самом простом случае это просто input типа radio, у которого атрибут name=delivery_id
  4. pay_system_id. Так же как и с доставкой, просто radio инпут, только атрибут name=pay_system_id
  5. save. Если переменная save=y, то компонент сохранит заказ. Во всех остальных случаях компонент просто обновит данные в объекте заказа и отдаст шаблон.

Да, всё настолько просто. Формируете форму с этими пятью переменными и вы великолепны. Это далеко не всё что можно сделать с помощью опенсорсного компонента. Но даже в самых сложных шаблонах суть останется прежней. Оформление заказа это не магия, просто обычная форма в браузере, просто чуть больше полей чем в обратной связи.

Как установить?

Установка доступна с помощью композера, через маркетплейс битрикс и просто вручную, архивом.

Кристально ясное оформление

Битрикс развивается и довольно много удобных вещей уже реализовано. ООП архитектура интернет магазина на мой взгляд получилась крайне удачной. Люди привыкли работать с объектами в реальном мире, поэтому мне кажется довольно просто понять что такое объект заказа и держать его в голове.

Яблоки на прилавке — товар, яблоки в пакете по дороге домой — сохраненный объект заказа. Можно оптимизировать внутренности объекта заказа, делать его более понятным и функциональным, однако же в плане концептов тут уже нечего улучшать. У тебя есть условный пакет яблок и ты можешь делать с ним что захочешь.

А что люди реально хотят делать с объектом заказа? Вариантов миллион. Яблоко можно просто съесть, сделать компот, сделать повидло, фруктовый салат, можно достать семена и посадить яблоню. И если мы в самом компоненте будем формировать массив $arResult, который пригодится вообще во всех возможных случаях, то мы опять придем к тому, что компонент разрастется и перестанет быть доступным для понимания.

Это список новостей всегда будет выглядеть примерно одинаково, а в процедуре оформления заказа фантазия может разгуляться. Отсюда первый вывод — для универсальности мы должны полностью отказаться от массива $arResult в компоненте. Компонент на выходе должен отдавать только объект заказа.

Куда отправлять аякс запросы?

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

Аякс работает с помощью нового механизма аякс, о котором я писал в статье по ссылке. В простом случае никакие аякс запросы вам не понадобятся. Достаточно просто сформировать html форму без скриптов вообще и отправить ее по кнопке submit на эту же страницу. Однако для чуть более сложных сценариев вы можете воспользоваться следующими готовыми методами:

Обновление списка доставок

Это тот же самый дефолтный шаблон, но в файле script.js шаблона добавлен обработчик на изменение свойств типа location. При изменении свойства с типом location отправляется аякс запрос на расчет стоимости доставки. После получения результата расчета текущий список доставок очищается и строится заново по данным пришедшим из аякс.

То есть по сути отличается только файл script.js и немного отличается вывод списка доставок, чтобы можно было из скрипта удобнее их очищать.

Переключение типа плательщика

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

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

Поиск местоположений

Файл ajax.php в папке компонента. Метод searchLocationAction в классе OpenSourceOrderAjaxController. Сам поиск осуществляется с помощью штатного метода BitrixSaleLocationSearchFinder::find. Далее по каждому из найденных местоположений собирается полная информация, в том числе путь от корня — страна, регион, область, и т.д. Можно получить эту же информацию в обратном порядке — местоположение, область, регион, страна.

На вход метод принимает четыре параметра:

1. Поисковая строка string $q, любая фраза по которой осуществляется поиск. Например при вводе «санкт» битрикс с большой долей вероятности найдет «Санкт-Петербург».

2. Количество поисковых результатов, int $limit = 5. Сколько найденных местоположений вам придет в ответе. В коде стоит проверка — не меньше одного местоположения и не больше пятидесяти.

3. Тип местоположений, string $typeCode. Если вам нужны только города, то передайте аргумент $typeCode = ‘CITY’. По умолчанию поиск осуществляется вообще по всем типам местоположений.

4. Исключение каких то частей и найденного названия, array $excludeParts = []. Если в названии найденного местоположения вам не нужна страна и округ, то просто передайте в этот аргумент значение [COUNTRY, COUNTRY_DISTRICT] и в ответе вам придет название местоположения начиная с региона.

Всего в битриксе есть семь дефолтных типов местоположений. Любое местоположения которое хранится штатно в таблице местоположений будет принадлежать одному из семи типов — COUNTRY, COUNTRY_DISTRICT, REGION, SUBREGION, CITY, VILLAGE, STREET.

5. Порядок формирования имени, string $sortOrder = ‘desc’. Как я говорил выше, найденное местоположение может быть отдано в формате страна, округ, регион и т.д., а может быть наоборот в формате регион, округ, страна. В этом параметре вы можете указать нужный порядок.

Возвращает массив найденных местоположений:

Array
(
    [0] => Array
        (
            [name] => Москва
            [code] => 0000073738
            [type] => CITY
            [type_name] => Город
            [label] => Москва, Центр, Россия
            [parts] => Array
                (
                    [CITY] => Array
                        (
                            [name] => Москва
                            [code] => 0000073738
                            [type] => CITY
                            [type_name] => Город
                        )

                    [COUNTRY_DISTRICT] => Array
                        (
                            [name] => Центр
                            [code] => 0000028024
                            [type] => COUNTRY_DISTRICT
                            [type_name] => Округ
                        )

                    [COUNTRY] => Array
                        (
                            [name] => Россия
                            [code] => 0000028023
                            [type] => COUNTRY
                            [type_name] => Страна
                        )

                )

        )

        [...]     
)

Покупка в интернет-магазине без регистрации на «битрикс»

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

1. Настройте компонент sale.order.ajax

Измените настройку проверки E-mail на уникальность на “выключено”. Теперь заказ будет создаваться без проверки электронной почты.

2. Встраиваем проверку уникальности

После создания заказа вставьте код проверки уникальности, чтобы в базе не накапливались дубли почтовых адресов

AddEventHandler('main', 'OnBeforeUserRegister', array('CMainhandlers', 'OnBeforeUserRegisterHandler'));
class CMainhandlers
{
   public static function OnBeforeUserRegisterHandler(&$arFields)
   {
      if (CUser::GetList($by='id', $order='desc', array('=EMAIL' => $arFields['EMAIL']))->Fetch())
      {
         $GLOBALS['APPLICATION']->throwException('Пользователь с таким E-Mail уже зарегистрирован.');
         return false;
      }
   }
}

3. Привязываем созданный заказ к пользователю в базе

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

AddEventHandler('sale', 'OnSaleComponentOrderOneStepComplete', array('CSalehandlers', 'OnSaleComponentOrderOneStepCompleteHandler'));
class CSalehandlers
{
   public static function OnSaleComponentOrderOneStepCompleteHandler($ID, $arFields)
   {
      if ($arFields['ID'] > 0)
      {
         if ($arFields['USER_ID'] > 0)
         {
            $arExtUser = CUser::GetByID($arFields['USER_ID'])->Fetch();//1

            if ($arOldUser = CUser::GetList($by='id', $order='desc', array('=EMAIL' => $arExtUser['EMAIL'], '!ID' => $arExtUser['ID']))->Fetch())//2
            {
               CSaleOrder::Update($arFields['ID'], array('USER_ID' => $arOldUser['ID']));//3
               CUser::Delete($arExtUser['ID']);//4
               $GLOBALS['USER']->Logout();//5
               if (!isset($_SESSION['CAN_VIEW_ORDERS']))//1
   $_SESSION['CAN_VIEW_ORDERS'] = array();
$_SESSION['CAN_VIEW_ORDERS'][] = $arFields['ID'];

global $arResult, $arParams;//2
$arResult = array(
   'ORDER_ID' => $arFields['ID'],
   'ORDER' => $arFields,
   'PAY_SYSTEM' => self::GetPaySystem($arFields),
);
$arParams = array(
   'PATH_TO_PERSONAL' => '/personal/orders/',
   'PATH_TO_PAYMENT' => '/personal/orders/guest_payment.php',
);
require $_SERVER['DOCUMENT_ROOT'].'/bitrix/templates/.default/components/bitrix/sale.order.ajax/in/confirm.php';//3
               exit();//7
            }
         }
      }
   }
}

4. Организуем оплату: новый метод обработки

Для работы с массивом ПС добавьте к обработчикам CSaleHandlers новый метод:

private static function GetPaySystem($arOrder)
   {
      if (IntVal($arOrder['PAY_SYSTEM_ID']) > 0)
      {
         $dbPaySysAction = CSalePaySystemAction::GetList(
               array(),
               array(
                     'PAY_SYSTEM_ID' => $arOrder['PAY_SYSTEM_ID'],
                     'PERSON_TYPE_ID' => $arOrder['PERSON_TYPE_ID']
                  ),
               false,
               false,
               array('NAME', 'ACTION_FILE', 'NEW_WINDOW', 'PARAMS', 'ENCODING')
            );
         if ($arPaySysAction = $dbPaySysAction->Fetch())
         {
            $arPaySysAction['NAME'] = htmlspecialcharsEx($arPaySysAction['NAME']);
            if (strlen($arPaySysAction['ACTION_FILE']) > 0)
            {
               if ($arPaySysAction['NEW_WINDOW'] != 'Y')
               {
                  CSalePaySystemAction::InitParamArrays($arOrder, $ID, $arPaySysAction['PARAMS']);

                  $pathToAction = $_SERVER['DOCUMENT_ROOT'].$arPaySysAction['ACTION_FILE'];

                  $pathToAction = str_replace('\', '/', $pathToAction);
                  while (substr($pathToAction, strlen($pathToAction) - 1, 1) == '/')
                     $pathToAction = substr($pathToAction, 0, strlen($pathToAction) - 1);

                  if (file_exists($pathToAction))
                  {
                     if (is_dir($pathToAction) && file_exists($pathToAction.'/payment.php'))
                        $pathToAction .= '/payment.php';

                     $arPaySysAction['PATH_TO_ACTION'] = $pathToAction;
                  }

                  if(strlen($arPaySysAction['ENCODING']) > 0)
                  {
                     define('BX_SALE_ENCODING', $arPaySysAction['ENCODING']);
                     AddEventHandler('main', 'OnEndBufferContent', 'ChangeEncoding');
                     function ChangeEncoding($content)
                     {
                        header('Content-Type: text/html; charset='.BX_SALE_ENCODING);
                        $content = $GLOBALS['APPLICATION']->ConvertCharset($content, SITE_CHARSET, BX_SALE_ENCODING);
                        $content = str_replace('charset='.SITE_CHARSET, 'charset='.BX_SALE_ENCODING, $content);
                     }
                  }
               }
            }
            return $arPaySysAction;
         }
      }
   }

5. Подключите lang-файлы

В скрипте confirm.php после строки B_PROLOG_INCLUDED добавьте код:

$langPath = str_replace('\', '/', __FILE__);
$langPath = substr($langPath, 0, strlen($langPath)-strlen('/confirm.php'));
__IncludeLang($langPath.'/lang/'.LANGUAGE_ID.'/template.php');

6. Измените подключение платежной системы

В компоненте sale.order.payment замените вызов CSaleOrder::GetList на код

$arFilter = array("LID" => SITE_ID, "ID" => $ORDER_ID);
if (!is_array($_SESSION['CAN_VIEW_ORDERS']) || !in_array($ORDER_ID, $_SESSION['CAN_VIEW_ORDERS']))
   $arFilter["USER_ID"] = IntVal($GLOBALS["USER"]->GetID());
$dbOrder = CSaleOrder::GetList(array("DATE_UPDATE" => "DESC"), $arFilter);

Готово!

Назад в раздел

Работа с пользователями

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

Расчет стоимости доставки

Файл ajax.php в папке компонента. Метод calculateDeliveriesAction в классе OpenSourceOrderAjaxController. В методе как обычно создается объект заказа и отгрузка. По отгрузке выбираются все доступные доставки и рассчитывается стоимость этих доставок.

На вход принимает три параметра:

1. Тип плательщика, int $person_type_id.

2. Массив свойств, array $properties. В массиве должно быть как минимум одно свойство — свойство с местоположением. Иначе доставки ничего не смогут рассчитать. Лучше всего передавать вообще все свойства из формы заказа.

3. Массив ID доставок, array $delivery_ids. Может быть пустым, но если задан, то будут рассчитаны доставки только с указанными ID. Доставки в любом случае должны быть доступны для заказа.

Возвращает массив с рассчитанной стоимостью доставок:

Array
(
    [2] => Array
        (
            [id] => 2
            [success] => 1
            [name] => Доставка курьером
            [logo_path] => /upload/sale/delivery/logotip/789/78984a7a84ff245422e5abd911c1972a.png
            [period] => 
            [base_price] => 500
            [base_price_display] => 500 руб.
            [price] => 350
            [price_display] => 350 руб.
            [discount] => 150
            [discount_display] => 150 руб.
            [errors] => Array
                (
                )

        )
        
        [...]
)

Сохранение заказа

Файл ajax.php в папке компонента. Метод saveOrderAction в классе OpenSourceOrderAjaxController. По сути вызываются те же самые методы основного компонента, что и при отправке формы на страницу. Полный аналог executeComponent, за исключением вывода шаблона. Результат сохранения заказа, либо ошибки отдаются в виде чистых данных.

Стартовое состояние

Важной частью штатного sale.order.ajax является работа с профилями. Что такое профиль покупателя, если смотреть в самую суть? Это только стартовое состояние формы заказа. Если мы переключаем профиль, то стартовое состояние формы (заполненные значения в полях) меняется.

Таким образом что должен делать блок работы с профилями? Где-то выше компонента оформления заказа он должен сформировать массив со стартовыми полями заказа. И далее передать этот массив в компонент.

Установка через composer

Для начала пропишите в своем composer.json путь до папки bitrix. Обратите внимание на блок extra:

Файл class.php компонента

Именно с этого файла начинается работа всего компонента. Основная цель этого файла — создать объект заказа и передать его дальше.

Массив $arResult вообще не используется. Компонент в результате своей работы формирует всего две публичные переменные — переменную $this->order с объектом заказа и переменную $this->errorCollection с ошибками.

Для построения объекта заказа могут быть использованы два источника — параметры компонента и переменные запроса. При первом открытии никаких переменных запроса у нас разумеется нет, в этом случае все пять переменных будут получены из параметров компонента. Но данные из запроса приоритетнее. То есть при наличии данных в запросе, параметры компонента будут проигнорированы.

Шаблон компонента

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

На вход шаблона мы получаем массив $arResult, на выходе шаблон отдает готовый html код, плюс скрипты и стили если это необходимо. Если вы используете webpack vue/react, то в шаблоне просто передаем массив $arResult в браузер через глобальный window.

Файл result_modifier.php

Основная цель этого файла — сформировать массив $arResult и передать этот массив в шаблон.

На выходе из компонента мы получили объект заказа. В принципе можно сразу в шаблоне компонента пользоваться методами этого объекта, без формирования $arResult вообще. Однако же с предварительно сформированным $arResult, в шаблоне код получается более чистым и понятным.

На вход в result_modifier.php мы получаем объект заказа и коллекцию ошибок, на выходе мы отдаем $arResult с данными для шаблона.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *