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

Процент брошенных корзин с 2006 по 2017


Процент брошенных корзин с 2006 по 2017
Источник

Процент брошенных корзин на первый квартал 2018 года в разрезе индустрии:
Процент брошенных корзин на первый квартал 2018 года в разрезе индустрии
Источник

При этом, несмотря на общедоступную статистику, большинство интернет-магазинов не пользуются доступными возможностями и не подключают брошенную корзину. Недавнее «домашнее» исследование от EmailSoldiers наглядно показывает, что бОльшая часть магазинов вообще не заморачивается об этом.

Текущая статистика по подключенным брошенным корзинам


Исследование EmailSoldiers
Источник

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

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

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

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

Конверсия для брошенной корзины по данным RetailRocket


RetailRocket конверсия брошенной корзины
Источник

И вот мы с камрадом Артемом Александровым начали внедрение корзины с двух сторон.

Техническая реализация


ТЗ на интеграцию


Кратко описываем суть задачи.

Задача: подключить брошенную корзину для сайта ххх.хх с помощью рассылочного сервиса Mailchimp

Выдаем все необходимые материалы.

Ключ API: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-usXX

Где взять ключ?

Где взять ключ?

> Даем ссылку на документацию

ID листа, к которому подключаем Store: XXXXXXXXXX

Где взять ID листа?

Где взять ID листа?

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

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

Верстка шаблона


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

Создание автоматизации для брошенной корзины в Mailchimp

В базовых шаблонах мч предлагает на выбор три штуки:

Письма на выбор для брошенной корзины Mailchimp
  1. Брошенная корзина с динамическими товарами
  2. Брошенная корзина с продуктовыми рекомендациями (нужно настраивать отдельно)
  3. Брошенная корзина без товаров (просто текстовое письмо)

В лучших традициях, если у вас есть время, можно заверстать корзину самостоятельно.

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

<table>
<tbody> 
*|ABANDONED_CART:[$total=3]|*
<table>
<tbody> 
<tr>
<td>
<a href="*|CART:URL|*" title="*|PRODUCT:TITLE|*" target="_blank">
<img src="*|PRODUCT:IMAGE_URL|*">
</a>
</td>
<td>
*|PRODUCT:TITLE|* — *|PRODUCT:PRICE|*
</td>
</tr>
</tbody>
</table>
*|END:ABANDONED_CART|*
</tbody>
</table>
*|END:ABANDONED_CART|*
</tbody>
</table>

Казалось бы, если изменить цифру в переменной *|ABANDONED_CART:[$total=3]|*, то в письме будет отображаться другое количество товаров, но нет, поставьте хоть 5, хоть 100, мч отказывается показывать другое количество.

И, что тоже немного странно, переменная *|PRODUCT:PRICE|* заменяется на значения формата RUB288, и поменять это тоже почему-то нельзя, но об этом позже.

Для разнообразия мы пытались подставить еще и переменные с количеством игр и с общей стоимостью заказа, которые передаем по api, но мейлчимп и тут сказал «нет». Что ж, да будет так.

Слово программисту :)


С мэйлчимпом был исторический опыт интеграции с их сторонним сервисом mandrill, там все просто и понятно. Очень дружелюбная документация (конечно же, на английском), но не возникло никаких шероховатостей и заработало с первого пинка.

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

Исходные данные такие: язык php7 и фрэймворк yii2, который сильно оброс уже своей экосистемой. Т.е. у нас уже 6 небольших проектов, которые стараются использовать общие компоненты как на бэкенде, так и на фронтенде. Соответственно, реализация любой задачи требует решать ее проектонезависимо, но это не подразумевает фрэймворконезависимость, т.к. за это приходится платить человекочасами, коих всегда дефицит.

Получив задачу на интеграцию, надо первым делом осмотреться. Что нам дано? Во-первых, сервис мэйлчимп, с которым надо подружиться. Идем на гитхаб и видим, что там достаточно много реализаций. Но выбор прост — у самого популярного пакета 1.5к звезд (drewm/mailchimp-api).

Пакет дает простую обертку над rest-взаимодействием с мэйлчимпом. Нам остается только обрастить это своей логикой.

Во-вторых, нам дана документация. Исходя из документации, у нас есть ресурс Store с вложенными ресурсами: Cart, Customer, Order, Product, Promo rule. Для брошенной корзины без рекомендованных товаров нам понадобятся только Product, Cart и Customer. Cart в свою очередь состоит из набора Cart line, а Product содержит Product variants.

Мы декомпозировали задачу следующим образом:

  1. Загрузить данные по магазину в ресурс Stores
  2. Загрузить все доступные к покупке товары в Products
  3. Настроить загрузку корзин с пользователями по расписанию

Ок, поехали. Первым делом беремся за сущность «магазин». Мы решили сразу использовать тестовый и боевой вариант магазина и, в зависимости от переменной окружения, отвечающей за дев/прод режим, мы работали либо с одним магазином, либо с другим.

Чтобы загрузить данные по магазину, мы стучимся post-запросом по адресу /ecommerce/stores со следующим набором параметров:

[
   'id' => 'dev.***.ru',
   'list_id' => '****',
   'name' => '*** - test',
   'domain' => 'dev.***.ru',
   'email_address' => 'admin@***.ru',
   'currency_code' => 'RUB',
   'primary_locale' => 'ru',
   'money_format' => '?',
]

Параметров несколько больше, но все зависит от потребностей. Т.к. мы не собирались использовать контактные данные магазина в письмах, то не заполнили поля phone, address, timezone и т. п.
Но нас ожидал небольшой сюрприз. Поле money_format вроде специально создано для возможности представить цену в удобном нам формате. Но при построении шаблона брошенной корзины мэйлчимп упорно подставляет RUB перед числом. Мэйлчимп, перестань!

После загрузки мы можем проверить данные с помощью get-запроса по адресу /ecommerce/stores, чтобы увидеть все загруженные магазины, либо /ecommerce/stores/{id} для получения данных по конкретному магазину.

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

Так-с, чтобы МЧ мог подставлять товары в брошенную корзину, надо ему скормить эти товары. Для этого у нас есть адрес /ecommerce/stores/{store_id}/products, куда мы отправляем post-запросы на создание продуктов в системе.


[
   'id' => '742',
   'title' => 'Кастрюля',
   'handle' => 'kastrulya',
   'url' => 'http://***.ru/catalog/kastrulya/',
   'description' => 'Кастрюля — незаменимая вещь на кухне. Купив кастрюлю, вы измените    свою жизнь в лучшую сторону. Вы поймете, что невозможно прожить без этой вещи и дня! В каждый дом по кастрюле и пусть никто не уйдет обиженным!',
   'type' => 'Посуда',
   'vendor' => 'Рога и Копыта',
   'image_url' => 'http://***.ru/images/742/product.png',
   'variants' => [
       [
           'id' => '742',
           'title' => 'Кастрюля',
           'url' => 'http://***.ru/catalog/kastrulya/',
           'price' => 890,
           'sku' => 'KA453',
           'inventory_quantity' => 1000,
           'image_url' => 'http://***.ru/images/742/product.png',
           'visibility' => 'visible',
       ],
   ],
]

Что тут примечательного? Ну во-первых, каждый товар должен состоять хотя бы из одного товарного предложения. По сути Product — это некий контейнер для загрузки товарных предложений. Причем, id товара и товарного предложения могут пересекаться, т. к. это разные ресурсы в api МЧ.

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

Поле handle было описано как «the handle of a product». Ок, посовещавшись мы решили, что это часть урла, относящегося к самому продукту (чпу). Но это подтвердилось только в ходе тестов.

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

И тут у нас возникла проблема, товары почему-то не отображались в темплейтах мейлчимпа.

Начали рыться в доке по Product. И нашли поле visibility с роскошным описанием:

описание поля visibility

Ну ок, тип String! А что туда можно передавать? Почему нельзя описать все возможные значения?! Я ведь могу туда отправить, например, «show me pls!».

Благо есть пример запроса!

пример запроса

Ну, это не отменяет проблему. Я ведь так и не знаю, какие еще могут быть значения, которые могут оказаться полезными.

Все, с этим справились! Теперь email-маркетолог может убедиться в наличии товаров в системе через построение шаблона с участием товаров или все через те же get-запросы с помощью консоли.

Дальше перед нами стоит задача загрузки брошенных корзин в МЧ. Изначально в голову пришло 2 варианта:

  1. При каждом изменении корзины (добавление/удаление товара), мы повторяем это действие в МЧ. Из минусов — сразу напрашивается огромное количество запросов к внешнему сервису.
  2. Раз в n минут смотреть корзины, которые не менялись более часа назад. После чего отправляем их в МЧ. Проблема только одна — следить за корзинами, которые были возобновлены после того, как отправились в МЧ.

Для начала делаем запрос в нашу базу данных (далее БД) за нашими данными в окне от 1 часа до 3. Почему 3? Через час после последнего изменения мы отправляем корзину в систему. В МЧ настроен минимально возможный интервал отправки корзины — 1 час. Поэтому в теории через 2 часа ± 5 минут произойдет отправка письма. Так что 3 часа — это величина даже с запасом.

Получив данные из БД, мы делаем get-запрос по адресу /ecommerce/stores/{store_id}/carts. Таким образом мы получаем все корзины, которые сидят в системе e-commerce и ждут своей очереди на отправку (либо уже отправлены). Для чего нам это нужно? Нужно для синхронизации с нашими данными. Мы отправим все корзины, полученные из БД, но нам нужно удалить те, которые уже не находятся в промежутке 1-3 часа. После 3х — уже неактуальные данные. До часа — корзины, которые могли опять возобновить, либо оформить заказ.

Для удаления нам надо просто найти разницу между двумя массивами/коллекциями корзин.
Получив корзины, которые необходимо удалить, мы отправляем delete-запрос /ecommerce/stores/{store_id}/carts/{cart_id}.

Дальше берем корзины для загрузки и циклом отправляем их post-запросами в систему.

Параметры корзины выглядят как-то так:


[
   'id' => '1207',
   'customer' =>
       [
           'id' => '25',
           'email_address' => 'email@example.com',
           'opt_in_status' => false,
       ],
   'currency_code' => 'RUB',
   'order_total' => 1597,
   'checkout_url' => 'http://***.ru/cart/abandoned/?cart=eyJpdGVtcyI6eyI1OTgwIjoxLCIzNDA0IjoxLCI3NzMiOjEsIjkwNTgiOjEsIjkwOTEiOjEsIjE4ODciOjEsIjc4NCI6MSwiNTExMSI6MSwiODA1MyI6MSwiMTk0MSI6MSwiNTQ0NSI6MSwiNzk1NCI6MywiOTA2NyI6NCwiOTA2NSI6NCwiNzg0MyI6MSwiOTA2NiI6M30sInByb21vY29kZSI6bnVsbH0%253D',
   'lines' => [
       0 => [
           'id' => '123',
           'product_id' => '5980',
           'product_variant_id' => '5980',
           'quantity' => 1,
           'price' => 841,
       ],
       1 => [
           'id' => '124',
           'product_id' => '3404',
           'product_variant_id' => '3404',
           'quantity' => 1,
           'price' => 756,
       ],
   ],
]

И опять наша любимая рубрика «догадайся, как работают эти поля». Например, методом научного тыка было выявлено, что можно не создавать покупателя отдельным запросом. Надо передать минимально требуемый набор полей, и он автоматически создастся, если его не было в системе. В нашем случае мы ограничились id, email, opt_in_status. Последний параметр отвечает за состояние подписки юзера в нашем листе. Если он true, то это означает состояние subscribed, в противном случае transactional.

Список товаров без проблем загружается через массив Cart Lines, который в свою очередь является ресурсом сущности Cart. Т.е. мы можем отдельно управлять этим набором с помощью rest-запросов.

Ну вот вроде бы и все? А вот и нет.

При тестировании мы обратили внимание, что отправив одну и ту же корзину, она отправляется лишь раз. Хотя мы ее удаляли из системы и загружали заново. Нигде ничего не сказано, ни единого слова! В итоге опытным путем, с помощью какой-то матери, мы приняли за основу гипотезу, что корзина с одним и тем же id может быть отправлена только один раз.

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

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

Было решено кодировать состав корзины в строку и передавать в checkout_url при отправке корзины в МЧ. А при переходе на сайт ловить эту строку, декодировать и накидывать все товары в корзину, не забыв перед этим ее полностью обнулить.

Таким образом, в какой бы браузер мы не отправили юзера, он получает свою корзину, как мы и обещали. Единственный минус, что ему остается только авторизоваться. Но авторизовывать через ссылку — это моветон, да и вообще дело опасное, в первую очередь для наших клиентов.
Что в итоге? В принципе, проблем особых не было при реализации, так как очень часто выручали ответы МЧ при ошибках, связанных с валидацией переданных полей. Но их было бы еще меньше, если бы они нормально по-человечьи описали все эти тонкости работы МЧ и более подробно описали бы поля.

Настройка отчета в Google.Analytics


Чтобы отслеживать успех всей операции, придется настроить и периодически посматривать отчет в аналитике. Представим, что у нас у всех, как у взрослых, подключен ecommerce, иначе чуда не получится :)

Чтобы собрать новый отчет под брошенную корзину, идем в «Мои отчеты»:

Мои отчеты

Дальше «Добавить отчет»:

Добавить отчет

А после добавляем параметры, которые будем отслеживать. Мы решили, что будем смотреть на брошенную корзину в разрезе городов, у вас может быть другое видение.

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

image

That’s all forks!

Теперь у вас настроена отправка брошенной корзины и отчет, по которому вы можете смотреть выхлоп с нее. И тестируйте, тестируйте, тестируйте! ;)

Комментарии (14)


  1. Klenov_s
    27.08.2018 14:05

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


    1. marad3r
      27.08.2018 15:23

      Вы недооцениваете email-сервисы. Это один из сотен моментов, которые можно реализовать через подобный, и не важно как он называется. Чтобы получить похожий функционал, вам необходимо потратить сотни человеко-часов. И быть очень смелым, чтобы не завалить свой домен в спам-фильтры.


      1. Klenov_s
        27.08.2018 15:53

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


        1. marad3r
          27.08.2018 16:04
          +1

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

          Но если рассуждать изолировано, что у вас никакой email-активности не ведется с пользователями, то конечно, проще накидать свой скрипт. Тут вы правы, спору нет. У самого было пару опытов в написании таких скриптов, и потом эти проекты неизбежно приходили к подобным сервисам.

          Это вопрос безотносительно к данному кейсу. Очень часто, особенно в рамках MVP-проектов, надо сделать быстро, и чтобы работало. Но если к вопросу подходить комплексно и когда проект прошел стадию стартапа, то сам не напишешься.


          1. Klenov_s
            27.08.2018 16:16

            Я отталкиваюсь от того, что если на сайте есть корзина, то у него наверняка развернута почтовая инфраструктура в том или ином виде и добавить в нее новый тип спама — не проблема. А если инфраструктуры нет, то первым делом настраивать брошенные корзины по такому вот мануалу не слишком актуально на этом этапе. ))


            1. marad3r
              27.08.2018 16:23

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


          1. Ambrosian
            28.08.2018 09:52

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


            Это относится только к массовым рассылкам писем.


  1. alexanster
    27.08.2018 14:25
    +1

    Не понял, что такое «брошенная корзина» — это когда я копался-копался в товарах, потом мне по какой-либо причине расхотелось делать заказ и я тупо ушёл с сайта?
    Если так, то могу сказать одно — как же этот спам за... надоел. Не знаю, как других покупателей, но меня эти письма только раздражают и отбивают всякое желание заходить в этот магазин ещё раз.


  1. supadoctor
    27.08.2018 15:09

    That’s all forks!

    Это фиаско, братан!


    1. dpyatnisa Автор
      27.08.2018 15:09

      ну это же шутка)


  1. Tyusha
    27.08.2018 15:21

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


    1. marad3r
      27.08.2018 16:21

      Варианта два:

      1. Если пользователь авторизован, то используем мыло из его профиля.
      2. Либо выводим всплывашку: «Отправить корзину вам на почту?» при попытке уйти с сайта или после какого-либо таймаута

      Тут уже вопрос религии и предпочтений. Надо чувствовать ЦА или проводить А/Б тестирование.


  1. NetMozg
    27.08.2018 19:49

    Я думаю, ценность брошенной корзины сильно преувеличена. Многие пользуются ею как «закладками» — отложить, потом посмотреть, сравнить и т.п. Но покупать никто и не собирался…


  1. ArgentMind
    28.08.2018 09:38

    Хочу поблагодарить автора за написание нужной и востребованной методички: если б сервисы сами писали настолько полушаговые инструкции — цены бы им не было. Обычная формулировка «После покупки настойчиво и качественно обработать напильником» уже как-то в кровь глаза натерла: главное реализовать идею, а не научить пользоваться.

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

    P.S. Еще бы добавил в статью реализацию галочки «Согласен на рассылку» на стороне сайта, которая автоматом в МЧ верифицирует мыло авторизированного пользователя, а то местами с этим проблемка (вроде раньше у них была такая фича в API).