Интеграция интернет-магазина на 1С-Битрикс с Mindbox

    Для развития систем лояльности интернет-магазины обращаются к платформам автоматизации маркетинга, Customer Data Platform (CDP). При этом иногда для успешной интеграции нужно сохранять больше данных, чем указано в документации к API.

    Рассказываем, какие данные понадобились нам для интеграции магазина на «1С-Битрикс» с платформой Mindbox, как их можно получить с помощью API и SDK и как использовать комбинированный подход с асинхронной отправкой данных.



    С помощью сервисов Customer Data Platform ритейлеры «узнают» портрет своего покупателя, в том числе поведенческие данные. Эта информация хранится в CDP в защищенном виде и помогает ритейлерам в проведении маркетинговых кампаний и аналитике.

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

    Один из наших клиентов — сеть магазинов электроники — принял решение подключиться к CDP-платформе Mindbox и обратился к нам за помощью в интеграции. Мы проводили интеграцию по ключевым пользовательским сценариям: авторизация, добавление в корзину, оплата и др.

    Предыстория


    Интернет-магазины могут подключиться к Mindbox двумя основными способами: с помощью API либо JavaScript SDK (об отличиях мы расскажем далее).

    Для выбора оптимального способа мы обратились к документации Mindbox, а если информации не хватало, то задавали вопросы менеджеру. Мы выяснили, что наше сотрудничество совпало с периодом бурного роста платформы Mindbox: среднесуточная нагрузка по вызовам API Mindbox увеличилась вдвое (до 120 тысяч запросов в минуту, в пик — до 250 тысяч). Это означало, что в период Черной пятницы и прочих распродаж из-за дополнительного роста нагрузки возникал риск, что CDP-сервис окажется недоступен и не получит данные интернет-магазина, который с ним интегрирован.

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

    Методы интеграции с Mindbox


    Как отмечено выше, Mindbox предлагает использовать для подключения API или JavaScript SDK. Далее рассмотрим их особенности.

    • JavaScript SDK


    Библиотека-«обёртка» над API, предоставляемая сервисом. Её плюсы — простота интеграции и возможность асинхронной передачи данных. Оптимально подходит для тех случаев, когда нужно поддерживать только web-платформу.

    Ограничения: возможна потеря данных, если Mindbox недоступен в момент отправки. Скрипты платформы не подгрузятся, если на стороне интернет-магазина есть js-ошибки.

    • Интеграция по API


    Интеграцию магазина с Mindbox можно провести через API. Этот способ снижает зависимость от JavaScript и также подходит для настройки асинхронной отправки данных.

    Ограничения: мы столкнулись с тем, что не получали некоторые данные cookie, а именно уникальный идентификатор пользователя на устройстве (mindboxDeviceUUID). Его необходимо передавать в большинстве операций Mindbox для склеивания информации по пользователю.

    В документации эти cookie обязательны не для всех операций. И всё же, стремясь к бесперебойной передаче данных, мы обсудили этот вопрос с менеджером Mindbox. Выяснили, что для максимальной надежности желательно всегда отправлять cookie. При этом для получения cookie нужно использовать JavaScript SDK.

    Комбинированный метод


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

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

    С помощью Javascript SDK мы идентифицируем пользователя на сайте (mindboxDeviceUUID). Затем на стороне сервера формируем запрос со всеми необходимыми данными и помещаем его в очередь. Запросы из очереди через API отправляются сервису Mindbox. В случае отрицательного ответа запрос повторно помещается в очередь. Таким образом, при отправке данных Mindbox получает полный комплект необходимой информации.

    В приведенном далее примере класс Sender позволяет собрать и отправить запрос, выполнив первичную обработку ответа. Класс использует данные из самой команды (тип запроса/ответа, deviceUUID и др.) и из настроек модуля (параметры работы с API, токены и т.п.).

    <?php
    declare(strict_types=1);
    
    namespace Simbirsoft\MindBox;
    
    use Bitrix\Main\Web\Uri;
    use Bitrix\Main\Web\HttpClient;
    use Simbirsoft\Base\Converters\ConverterFactory;
    use Simbirsoft\MindBox\Contracts\SendableCommand;
    
    class Sender
    {
        /** @var Response Тело ответа */
        protected $response;
        /** @var SendableCommand Команда */
        protected $command;
    
        /**
         * Sender constructor.
         *
         * @param SendableCommand $command
         */
        public function __construct(SendableCommand $command)
        {
            $this->command = $command;
        }
    
        /**
         * Сформировать массив заголовков запроса.
         *
         * @return array
         */
        protected function getHeaders(): array
        {
            return [
                'Accept'        => Type\ContentType::REQUEST[$this->command->getRequestType()],
                'Content-Type'  => Type\ContentType::RESPONSE[$this->command->getResponseType()],
                'Authorization' => 'Mindbox secretKey="'. Options::get('secretKey') .'"',
                'User-Agent'    => $this->command->getHttpInfo('HTTP_USER_AGENT'),
                'X-Customer-IP' => $this->command->getHttpInfo('REMOTE_ADDR'),
            ];
        }
    
        /**
         * Сформировать адрес запроса.
         *
         * @return string
         */
        protected function getUrl(): string
        {
            $uriParts = [
                Options::get('apiUrl'),
                $this->command->getOperationType(),
            ];
            $uriParams = [
                'operation'  => $this->command->getOperation(),
                'endpointId' => Options::get('endpointId'),
            ];
    
            $deviceUUID = $this->command->getHttpInfo('deviceUUID');
            if (!empty($deviceUUID)) {
                $uriParams['deviceUUID'] = $deviceUUID;
            }
    
            return (new Uri(implode('/', $uriParts)))
                ->addParams($uriParams)
                ->getUri();
        }
    
        /**
         * Отправить запрос.
         *
         * @return bool
         */
        public function send(): bool
        {
            $httpClient = new HttpClient();
    
            $headers = $this->getHeaders();
            foreach ($headers as $name => $value) {
                $httpClient->setHeader($name, $value, false);
            }
    
            $encodedData = null;
            $request = $this->command->getRequestData();
            if (!empty($request)) {
                $converter = ConverterFactory::factory($this->command->getRequestType());
                $encodedData = $converter->encode($request);
            }
    
            $url = $this->getUrl();
            if ($httpClient->query($this->command->getMethod(), $url, $encodedData)) {
                $converter = ConverterFactory::factory($this->command->getResponseType());
                $response = $converter->decode($httpClient->getResult());
                $this->response = new Response($response);
                return true;
            }
            return false;
        }
    
        /**
         * @return Response
         */
        public function getResponse(): Response
        {
            return $this->response;
        }
    }
    

    Трейт Sendable содержит все возможные настройки команды для отправки запроса в Mindbox, в том числе предустановленные, такие как тип запроса/ответа, метод запроса и параметр синхронности/асинхронности. Также в нем присутствуют методы, общие для всех команд.

    <?php
    declare(strict_types=1);
    
    namespace Simbirsoft\MindBox\Traits;
    
    use RuntimeException;
    use Bitrix\Main\Context;
    use Simbirsoft\MindBox\Type;
    use Simbirsoft\MindBox\Sender;
    use Simbirsoft\MindBox\Response;
    use Bitrix\Main\Localization\Loc;
    use Simbirsoft\MindBox\Contracts\SendableCommand;
    
    Loc::loadMessages($_SERVER['DOCUMENT_ROOT'] .'/local/modules/simbirsoft.base/lib/Contracts/Command.php');
    
    trait Sendable
    {
        /** @var string Метод отправки (GET/POST) */
        protected $method = Type\OperationMethod::POST;
        /** @var string Тип операции (sync/async) */
        protected $operationType = Type\OperationType::ASYNC;
        /** @var string Тип запроса (json/xml) */
        protected $requestType = Type\ContentType::JSON;
        /** @var string Тип ответа (json/xml) */
        protected $responseType = Type\ContentType::JSON;
        /** @var array Вспомогательные данные */
        protected $data = [];
    
        /**
         * Название операции.
         * @return string
         */
        abstract public function getOperation(): string;
    
        /**
         * Формируем данные.
         *
         * @return array
         */
        abstract public function getRequestData(): array;
    
        /**
         * HTTP метод запроса
         *
         * @return string
         */
        public function getMethod(): string
        {
            return $this->method;
        }
    
        /**
         * Тип операции
         *
         * @return string
         *
         * @noinspection PhpUnused
         */
        public function getOperationType(): string
        {
            return $this->operationType;
        }
    
        /**
         * Тип запроса.
         *
         * @return string
         *
         * @noinspection PhpUnused
         */
        public function getRequestType(): string
        {
            return $this->requestType;
        }
    
        /**
         * Тип ответа.
         *
         * @return string
         *
         * @noinspection PhpUnused
         */
        public function getResponseType(): string
        {
            return $this->responseType;
        }
    
        /**
         * Вспомогательные данные запроса
         *
         * @return void
         */
        public function initHttpInfo(): void
        {
            $server = Context::getCurrent()->getServer();
            $request = Context::getCurrent()->getRequest();
    
            $this->data = [
                'X-Customer-IP' => $server->get('REMOTE_ADDR'),
                'User-Agent'    => $server->get('HTTP_USER_AGENT'),
                'deviceUUID'    => $request->getCookieRaw('mindboxDeviceUUID'),
            ];
        }
    
        /**
         * Получить вспомогательные данные запроса
         *
         * @param string $key
         * @param string $default
         *
         * @return string
         *
         * @noinspection PhpUnused
         */
        public function getHttpInfo(string $key, string $default = ''): string
        {
            return $this->data[$key] ?? $default;
        }
    
        /**
         * Выполняем команду.
         *
         * @return void
         *
         * @throws RuntimeException
         */
        public function execute(): void
        {
            /** @var SendableCommand $thisCommand */
            $thisCommand = $this;
            $sender = new Sender($thisCommand);
            if ($sender->send()) {
                throw new RuntimeException(Loc::getMessage('BASE_COMMAND_NOT_EXECUTED'));
            }
    
            $response = $sender->getResponse();
            if (!$response->isSuccess()) {
                throw new RuntimeException(Loc::getMessage('BASE_COMMAND_NOT_EXECUTED'));
            }
    
            if (!$this->prepareResponse($response)) {
                throw new RuntimeException(Loc::getMessage('BASE_COMMAND_NOT_EXECUTED'));
            }
        }
    
        /**
         * Обработка ответа запроса.
         *
         * @param Response $response
         *
         * @return bool
         */
        public function prepareResponse(Response $response): bool
        {
            // $body   = $response->getBody();
            // $status = $body['customer']['processingStatus'];
            /**
             * Возможные статусы:
             * AuthenticationSucceeded - Если пароль верен
             * AuthenticationFailed         - Если пароль не верен
             * NotFound                          - Если потребитель не найден
             */
            return true;
        }
    }
    

    В качестве примера рассмотрим событие авторизации пользователя. В обработчике события авторизации мы добавляем в нашу очередь объект класса AuthorizationCommand. В этом классе происходит минимально необходимая подготовка информации, поскольку в момент выполнения команды данные в базе могут измениться, и нужно их сохранить. Также устанавливаются соответствующие параметры для запроса в Mindbox, в данном случае это название операции (узнаем в админ. панели Mindbox). Дополнительно можно указать тип запроса/ответа, метод запроса и параметр синхронности/асинхронности согласно трейту Sendable.

    <?php
    declare(strict_types=1);
    
    namespace Simbirsoft\MindBox\Commands;
    
    use Simbirsoft\Queue\Traits\Queueable;
    use Simbirsoft\MindBox\Traits\Sendable;
    use Simbirsoft\Queue\Contracts\QueueableCommand;
    use Simbirsoft\MindBox\Contracts\SendableCommand;
    
    final class AuthorizationCommand implements QueueableCommand, SendableCommand
    {
        use Queueable, Sendable;
    
        /** @var array Данные пользователя */
        protected $user;
    
        /**
         * AuthorizationCommand constructor.
         *
         * @param array $user
         */
        public function __construct(array $user)
        {
            $keys = ['ID', 'EMAIL', 'PERSONAL_MOBILE'];
            $this->user = array_intersect_key($user, array_flip($keys));
    
            $this->initHttpInfo();
        }
    
        /**
         * Название операции.
         *
         * @return string
         */
        public function getOperation(): string
        {
            return 'AuthorizationOnWebsite';
        }
    
        /**
         * Формируем данные.
         *
         * @return array
         */
        public function getRequestData(): array
        {
            return [
                'customer' => [
                    'email' => $this->user['EMAIL'],
                ],
            ];
        }
    }

    Схема взаимодействия модулей


    В нашем проекте мы выделили три модуля:

    • Базовый


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

    • Модуль очередей


    Взаимодействие с Mindbox реализовано через команды. Для их последовательного выполнения мы используем наш модуль очередей. Этот модуль сохраняет поступившие команды и выполняет их, когда наступает время выполнения.

    • Модуль интеграции с Mindbox


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



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

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

    Подводя итоги


    В этой статье мы рассмотрели, какими способами интернет-магазин может подключиться к Customer Data Platform для развития систем лояльности.

    В нашем примере в документации Mindbox были описаны два основных способа подключения: через Javascript SDK и через API. Для повышения надежности передачи данных, даже в случае временной недоступности CDP-сервиса, мы выбрали и реализовали третий, комбинированный способ: с помощью API и Javascript SDK, с асинхронной отправкой данных.

    Спасибо за внимание! Надеемся, эта статья была для вас полезна.
    SimbirSoft
    Лидер в разработке современных ИТ-решений на заказ

    Комментарии 0

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

    Самое читаемое