Как мы интегрировались в казахстанский маркетплейс или история о нюансах

от автора

Привет! Меня зовут Ваня Крючков, я бэкенд-разработчик в Далее. Сегодня поделюсь опытом интеграции интернет-магазина Haier с маркетплейсом Kaspi. Это история о том, как, несмотря на ограничения и не самое удобное API, нам удалось интегрироваться с самым популярным маркетплейсом в Казахстане и увеличить продажи в 3 раза.

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

Что такое Kaspi

Kaspi — самый популярный маркетплейс в Казахстане. Им пользуются около 11 миллионов человек в месяц. Он входит в структуру Kaspi Group, в которой, в том числе есть одноименная платежная система и банк.

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

Экраны приложения Kaspi

Экраны приложения Kaspi

Сайт Haieronline.kz уже интегрирован с внешними системами разными способами и с разными форматами данных. Так что еще одна интеграция казалась задачкой довольно тривиальной.

Связи между системами и зоны ответственности

Процесс онлайн-продаж Haier в Казахстане обеспечивается тремя основными системами:

  • сайт

  • система управления заказами (CRM)

  • система аналитики (СА).

За каждой системой закреплена отдельная команда. Мы работаем с «приемником» заказов, то есть с сайтом. 

Основная роль сайта в этой схеме — сохранение заказов в БД и отправка данных в систему аналитики (СА) и систему управления заказами (CRM).
За изменение состояния заказов отвечает CRM. В ней реализованы интеграции с транспортными компаниями и личный кабинет для менеджеров контактного центра.

CRM находится в закрытом контуре, поэтому на стадии разработки сайта мы решили реализовывать обмен через json-файлы (пакеты), которые сайт и CRM помещает на отдельный сервер.

Интеграция с маркетплейсом

Декомпозируем 

На старте определим процессы, связанные с маркетплейсом:

  1. Загрузка товаров на маркетплейс.

  2. Обновление цен и остатков по товарам в каспи.

  3. Получение заказов.

  4. Генерация и отправка пакетов заказов в CRM.

  5. Обновление заказов на основе пакетов CRM.

  6. Закрытие заказов.

Рассмотрим каждый их них.

Загрузка товаров

Когда мы взяли задачу в работу, у Kaspi API не было подходящих методов для автоматической загрузки товаров в маркетплейс. Поэтому они загружались руками через ЛК продавца в маркетплейсе.

Обновление цен и остатков по товарам

Здесь все было просто: делаем на сайте фид, указываем ссылку на него в кабинете — profit. Теперь маркетплейс сам может актуализировать цены и остатки на основе данных из фида. Правда, стоит учитывать, что Kaspi ходит в этот фид один раз в час. А цены и остатки на сайте могут меняться в течение часа несколько раз (на практике изменения цен происходят не так часто). Побочный эффект от такого тайминга состоит в том, что на сайте товар может быть в количестве двух единиц, а тем временем на маркетплейсе оформили заказов на пять.

Решение кажется на поверхности: просто менять остатки на маркетплейсе через апишку. Но есть один нюанс — у Kaspi API до сих пор нет метода, который бы позволил менять остаток на складе.

Ну окей, давайте просто отменим такой заказ с пометкой, что товар, к сожалению, уже раскуплен. Но есть второй нюанс: Kaspi довольно строго относится к отменам со стороны продавцов. Если у вас накопилось более 5% таких отмен, начинают вступать ограничительные меры:

  1. Первое нарушение — отключение продаж на 7 дней

  2. Второе нарушение — отключение продаж на 7 дней

  3. Третье — прекращение сотрудничества.

Чтобы исключить подобные отмены, реализовали оповещения для менеджеров. Отправка уведомления происходит, когда значение остатков по товару становится менее трех единиц. При получении оповещения менеджер заходит в ЛК Kaspi и руками деактивирует товар.

Оповещения срабатывают если остатки действительно подходят к концу. Например, если по какому-то товару несколько дней у нас нулевые остатки, или меняются с двух штук до одной — оповещение по ним не шлём.

Получение заказов

Ура, товары есть! Можно создавать заказ и пробовать его получить. Для начала разберем метод GET https://kaspi.kz/shop/api/v2/orders (https://guide.kaspi.kz/partner/ru/shop/api/orders/q3201)

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

  1. state (string) — состояние заказа

  2. creationDate (array) — временные метки создания заказа «от» и «до»

  3. status (string) — статус заказа

Status, очевидно, должен быть начальный —  APPROVED_BY_BANK (одобрен банком). 

В качестве CreationDate мы взяли разницу в два часа с момента отправки запроса (в этом параметре передаются временные метки в миллисекундах).

Поскольку изначально доставка осуществлялась партнерами Haier, а не самим маркетплейсом, то state у нас — DELIVERY.

Код написан, но есть третий нюанс — у Kaspi нет никакой тестовой среды. Тестировать предлагают на боевых заказах.

Есть же документация, можно по ней написать код и обернуть тестами, нет? 

Документация есть, но Kaspi API работает не всегда очевидно. Например, если в параметре status передать APPROVED_BY_BANK, то в выборку попадут и заказы со статусом ACCEPTED_BY_MERCHANT.

Так что тестировали заказ через покупку за реальные деньги и отмену на стороне клиента.

Диалог во время тестирования:

Разработчик: «Мы готовы протестировать заказ».

Бизнес: «Хорошо, активируйте товар».

Аналитик активирует товар

Бизнес: «Заказ оформил, номер 999».

Разработчик запускает скрипт

Разработчик: «Данные получил, пакет сгенерировался отправился в CRM.

CRM: «Пакет приняли, обработали, высылаем пакет с подтверждением заказа».

Разработчик: «Пакет приняли, отправили подтверждение в Kaspi».

Бизнес: «Отменяю заказ»?

Разработчик: «Да»

Бизнес: «Заказ отменил, скрывайте товар. Получается можно в релиз»?

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

Разработчик: «Давайте чуть позднее еще проверим другой кейс». 

Бизнес идет на следующий созвон…

Генерация и отправка пакета заказа в CRM

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

{   "ID": "HOKZ-0000065603",   "CreateDate": "2024-03-27T16:10:39",   "MarketPlace": null,   "Contact": {     "FirstName": "Локаль",     "LastName": "Тестов",     "Email": "local.test@test.kz",     "PhoneNumber": "+77991112233"   },   "TotalSum": 1319980,   "ProductList": [     {       "ID": "579694",       "Key": 12452,       "ProductID": "SKU579694",       "ProductName": "Игровой компьютер Thunderobot Black Warrior IV Max D",       "Quantity": 2,       "BasePrice": 849990,       "DiscountSum": 380000,       "Sum": 1319980     }   ],   "DeliveryAddress": {     "ID": "750000000",     "Address": "Алматы, Ленина, 12",     "Region": "г Алматы",     "City": "Алматы",     "Street": "Ленина",     "Building": "12",     "FlatOffice": "",     "Entrance": "",     "Floor": "",     "Intercom": "",     "Elevator": false,     "DateTimeFrom": "2024-03-28T09:00:00",     "DateTimeTo": "2024-03-28T20:00:00",     "TransitTime": 1   },   "TransportCompany": {     "ID": "0004",     "UnitName": "Beta Express",     "DeliveryComment": "123"   } }

По объекту MarketPlace CRM понимает, что на их стороне нужно запустить отдельный  процесс под заказы с маркетплейсов. Для Kaspi в этом объекте описаны:

  • id и название маркетплейса

  • способ доставки (нашей ТК либо силами Kaspi)

  • код заказа на стороне маркетплейса

  • дата доставки (которую выставляет сам маркетплейс).

Посмотрим, что возвращает Kaspi и хватит ли нам данных для генерации полного пакета заказа:

{   "data": [     {       "type": "orders",       "id": "MjAwMTMwMDQ=",       "attributes": {         "customer": {           "firstName": "Иван Иваныч",           "lastName": "Иванов",           "cellPhone": "7xx0xxxxxx"         },         "code": "20013004",         "totalPrice": 96045,         "deliveryMode": "DELIVERY_PICKUP",         "paymentMode": "PAY_WITH_CREDIT",         "signatureRequired": false,         "state": "PICKUP",         "creationDate": 1479470446241,         "approvedByBankDate":1479470451108,         "status": "ACCEPTED_BY_MERCHANT",         "deliveryCost": 1000       },       "relationships": {         "entries": {           "links": {             "self": "/v2/orders/MjAwMTMwMDQ=/relationships/entries",             "related": "/v2/orders/MjAwMTMwMDQ=/entries"           }         },         "user": {           "links": {             "self": "/v2/orders/MjAwMTMwMDQ=/relationships/user",             "related": "/v2/orders/MjAwMTMwMDQ=/user"           },           "data": {             "type": "customers",             "id": "Nzc3MDAwMDAwMA=="           }         }       },       "links": {         "self": "/v2/orders/MjAwMTMwMDQ="       }     }   ],   "included": [     {       "type": "customers",       "id":"Nzc3MDAwMDAwMA==",       "attributes": {         "firstName": "Иван",         "lastName": "Иваныч",         "cellPhone": "7xx0xxxxxx"       },       "relationships": {},       "links": {         "self":"/v2/customers/Nzc3MDAwMDAwMA=="       }     }   ],   "meta": {     "pageCount": 1,     "totalCount": 1   } }

Как видим, в ответе совсем нет информации о товарах и адресе доставки. Поэтому придется запрашивать еще один метод на получение товаров — GET https://kaspi.kz/shop/api/v2/orders/{kaspi_order_id}/entries (https://guide.kaspi.kz/partner/ru/shop/api/orders/q3203)

{   "data": [     {       "type": "orderentries",       "id": "MTQwMjYzNjcwIyMw",       "attributes": {         "quantity": 1,         "totalPrice": 1390.0,         "entryNumber": 0,         "deliveryCost": 0.0,         "basePrice": 1390.0       },       "relationships": {         "product": {           "links": {             "self": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMw/relationships/product",             "related": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMw/product"           },           "data": {             "type": "masterproducts",             "id": "MjYwMDExMTQ="           }         },         "deliveryPointOfService": {           "links": {             "self": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMw/relationships/deliveryPointOfService",             "related": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMw/deliveryPointOfService"           },           "data": {             "type": "pointofservices",             "id": "TUhvbWVWaWRlb19BQVIxODY="           }         }       },       "links": {         "self": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMw"       }     },     {       "type": "orderentries",       "id": "MTQwMjYzNjcwIyMx",       "attributes": {         "quantity": 2,         "totalPrice": 2690.0,         "entryNumber": 1,         "deliveryCost": 0.0,         "basePrice": 1345.0       },       "relationships": {         "product": {           "links": {             "self": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMx/relationships/product",             "related": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMx/product"           },           "data": {             "type": "masterproducts",             "id": "MTAwMDIwNzM4"           }         },         "deliveryPointOfService": {           "links": {             "self": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMx/relationships/deliveryPointOfService",             "related": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMx/deliveryPointOfService"           },           "data": {             "type": "pointofservices",             "id": "TUhvbWVWaWRlb19XTVI="           }         }       },       "links": {         "self": "https://kaspi.kz/shop/api/v2/orderentries/MTQwMjYzNjcwIyMx"       }     }   ],   "included": [] }

Теперь данные по товарам в заказе есть, но есть четвертый нюанс. В пакете для CRM нам нужно передавать артикул товаров и id в БД на сайте. Эмпирическим путем выяснили, что ни один из методов Kaspi API не возвращает артикул товара (сейчас этот момент Kaspi уже поправили). Ладно, но мы же можем как-то связать наши артикулы с внутренними кодами товаров Kaspi? Конечно. Осталось понять, по какому полю.

У товаров на маркетплейсе есть две подходящие для этого характеристики: id и числовой код товара (назовем его kaspi_product_code). Идентификатор можно получить, используя запросы, а kaspi_product_code можно увидеть на самом сайте после того как завели карточку товара в ЛК. 

Поэтому менеджеры, которые добавляли товары на маркетплейс, сразу готовили нам сводную таблицу SKU-kaspiProductCode, под которую мы сделали отдельную таблицу kaspi_products. Но числовых кодов в ответе на запрос получения товаров мы не видим. Чтобы их получить, отправляем еще по одному запросу на каждый товар в заказе: GET https://kaspi.kz/shop/api/v2/orderentries/{kaspi_subitem_id}/product, где kaspi_subitem_id — это строковый идентификатор товара в заказе (https://guide.kaspi.kz/partner/ru/shop/api/orders/q3207)

А что там с доставкой? Поскольку Kaspi-заказы доставляла только одна ТК, то проблем с ее описанием в пакете не возникало. Но для определения адреса доставки приходится отправлять еще один запрос — GET https://kaspi.kz/shop/api/v2/orders?filter[orders][code]=ordercode, где ordercode — это числовой код заказа (https://guide.kaspi.kz/partner/ru/shop/api/orders/q3202).

Итого на один заказ, в котором один товар, получается 4 запроса. Если товаров больше — то и запросов будет больше. Логично возникает вопрос — а что там с лимитами на запросы к Kaspi API? Задав вопрос получили интересный ответ: 100 запросов в час.

Переспросили на всякий случай — нам ответили, что ошибки нет, 100 в час. В будущем этот момент Kaspi поправили, лимит стал 100 запросов в секунду.

Модель заказа Kaspi

Здесь стоит немного остановиться и добавить небольшое описание модели заказа в БД на сайте. Итоговая версия следующая:

  • id

  • date_created

  • date_updated

  • type — тип заказа (KASPI_DELIVERY, DELIVERY)

  • id_kaspi — строковый id заказа в Kaspi

  • code_kaspi — числовой код заказа в Kaspi

  • id_crm — строковый идентификатор заказа в CRM (не uuid)

  • status_kaspi — статус заказа в Kaspi

  • status_crm — статус заказа в CRM

  • receipt — ссылка на чек

  • waybill — ссылка на накладную

  • order_data — сериализованный ответ на запрос получения заказов

  • delivery_data — сериализованный ответ на запрос получения доставки

  • products_data — сериализованный ответ на запрос получения товаров

  • products_codes — сериализованный массив числовых кодов товаров

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

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

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

С маркетплейсом на этом этапе мы общаемся в двух случаях: подтверждение заказа (установка статуса ACCEPTED_BY_MERCHANT) и отмена заказа (запрос https://guide.kaspi.kz/partner/ru/shop/api/orders/q3213).

Закрытие заказов

«Мы практически дошли до конца и, кажется, можем полностью протестировать процесс!», — говорили мы на дейликах тогда.
«Но есть еще пятый нюанс», — говорю я сейчас.

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

У Kaspi для таких случаев есть подтверждение через смс. Схема следующая:

  1. ТК доставляет заказ клиенту.

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

  3. С помощью данного кода закрываем заказ на стороне Kaspi.

ТК у нас партнерская, поэтому напрямую интегрировать курьеров в наши системы мы не можем. Но у нас есть Контактный центр (КЦ). Его менеджеры общаются с клиентом и сопровождают покупку. Так что в теории они могут звонить, чтобы подтвердить и закрыть заказ. Данную реализацию логично было бы разместить в CRM, но их команда в тот момент была нарасхват, поэтому мы реализовали это на стороне сайта.

Интерфейс закрытия заказа

Интерфейс закрытия заказа

Когда статус заказа в CRM — «Завершен», а в Kaspi — еще нет, выводим кнопку «Начать закрытие заказа». По кнопке срабатывает триггер для отправки смс. Смс отправляем через Kaspi API. Менеджер КЦ вводит код от клиента в появившееся поле, и мы успешно закрываем заказ. После этого генерируем чек и отправляем его клиенту.

Минус такого процесса — зависимость от смс-провайдера Kaspi.

Переход на доставку маркетплейса

В скором времени был инициирован переход на доставку средствами маркетплейса. Главное преимущество такой доставки в том, что заказ закрывается автоматически на стороне Kaspi (они сами его доставляют). Поэтому табличка для КЦ теперь используется в качестве быстрого доступа «а что там с заказами».

В связи с новым порядком доставки в процессах произошли изменения:

  1. Type в заказе теперь KASPI_DELIVERY (но осталась поддержка и доставки силами нашей ТК).

  2. После подтверждения заказа нужно отправить еще один запрос на сборку заказа (изменение статуса на ASSEMBLE). После того как заказ в Kaspi оказался в этом статусе, автоматически формируется накладная, доступная по ссылке. Эту ссылку мы передаем в CRM отдельным пакетом, чтобы с их стороны она отправилась на склад. Накладная представляет собой этикетку на упакованном товаре, по которой сотрудники Kaspi понимают, что это за заказ и сколько места он займет в их транспорте.

  3. Поскольку ручное закрытие заказа больше нам не требуется, чек генерируем в момент получения от CRM финального статуса заказа

Отмененные клиентами заказы

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

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

One more nuance

Период распродаж В Kaspi называется просто «Жұма». В переводе на русский тоже пятница. Такие распродажи проходят три раза в год: в феврале, в июле и в ноябре. Во время последней Жумы Kaspi API не очень хорошо справлялся и приходилось увеличивать таймаут ответа аж до 20 секунд.

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

Всем рахмет!


ссылка на оригинал статьи https://habr.com/ru/articles/806447/


Комментарии

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

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