Как мы интегрировались в казахстанский маркетплейс или история о нюансах
Привет! Меня зовут Ваня Крючков, я бэкенд-разработчик в Далее. Сегодня поделюсь опытом интеграции интернет-магазина Haier с маркетплейсом Kaspi. Это история о том, как, несмотря на ограничения и не самое удобное API, нам удалось интегрироваться с самым популярным маркетплейсом в Казахстане и увеличить продажи в 3 раза.
На Хабре про Kaspi в целом мало информации, в основном, про финансы и новости. Поэтому мне показалось логичным на своем опыте показать, какие подводные камни могут встретиться на пути к автоматизации продаж на казахстанском маркетплейсе.
Что такое Kaspi
Kaspi — самый популярный маркетплейс в Казахстане. Им пользуются около 11 миллионов человек в месяц. Он входит в структуру Kaspi Group, в которой, в том числе есть одноименная платежная система и банк.
Сайт маркетплейса представляет собой витрину товаров, а все покупки осуществляются через суперапп.
Экраны приложения Kaspi
Сайт Haieronline.kz уже интегрирован с внешними системами разными способами и с разными форматами данных. Так что еще одна интеграция казалась задачкой довольно тривиальной.
Связи между системами и зоны ответственности
Процесс онлайн-продаж Haier в Казахстане обеспечивается тремя основными системами:
За каждой системой закреплена отдельная команда. Мы работаем с «приемником» заказов, то есть с сайтом.
Основная роль сайта в этой схеме — сохранение заказов в БД и отправка данных в систему аналитики (СА) и систему управления заказами (CRM).
За изменение состояния заказов отвечает CRM. В ней реализованы интеграции с транспортными компаниями и личный кабинет для менеджеров контактного центра.
CRM находится в закрытом контуре, поэтому на стадии разработки сайта мы решили реализовывать обмен через json-файлы (пакеты), которые сайт и CRM помещает на отдельный сервер.
Интеграция с маркетплейсом
Декомпозируем
На старте определим процессы, связанные с маркетплейсом:
Загрузка товаров на маркетплейс.
Обновление цен и остатков по товарам в каспи.
Получение заказов.
Генерация и отправка пакетов заказов в CRM.
Обновление заказов на основе пакетов CRM.
Закрытие заказов.
Рассмотрим каждый их них.
Загрузка товаров
Когда мы взяли задачу в работу, у Kaspi API не было подходящих методов для автоматической загрузки товаров в маркетплейс. Поэтому они загружались руками через ЛК продавца в маркетплейсе.
Обновление цен и остатков по товарам
Здесь все было просто: делаем на сайте фид, указываем ссылку на него в кабинете — profit. Теперь маркетплейс сам может актуализировать цены и остатки на основе данных из фида. Правда, стоит учитывать, что Kaspi ходит в этот фид один раз в час. А цены и остатки на сайте могут меняться в течение часа несколько раз (на практике изменения цен происходят не так часто). Побочный эффект от такого тайминга состоит в том, что на сайте товар может быть в количестве двух единиц, а тем временем на маркетплейсе оформили заказов на пять.
Решение кажется на поверхности: просто менять остатки на маркетплейсе через апишку. Но есть один нюанс — у Kaspi API до сих пор нет метода, который бы позволил менять остаток на складе.
Ну окей, давайте просто отменим такой заказ с пометкой, что товар, к сожалению, уже раскуплен. Но есть второй нюанс: Kaspi довольно строго относится к отменам со стороны продавцов. Если у вас накопилось более 5% таких отмен, начинают вступать ограничительные меры:
Первое нарушение — отключение продаж на 7 дней
Второе нарушение — отключение продаж на 7 дней
Третье — прекращение сотрудничества.
Чтобы исключить подобные отмены, реализовали оповещения для менеджеров. Отправка уведомления происходит, когда значение остатков по товару становится менее трех единиц. При получении оповещения менеджер заходит в ЛК Kaspi и руками деактивирует товар.
Оповещения срабатывают если остатки действительно подходят к концу. Например, если по какому-то товару несколько дней у нас нулевые остатки, или меняются с двух штук до одной — оповещение по ним не шлём.
Получение заказов
Ура, товары есть! Можно создавать заказ и пробовать его получить. Для начала разберем метод GET https://kaspi.kz/shop/api/v2/orders
(https://guide.kaspi.kz/partner/ru/shop/api/orders/q3201)
В параметрах запроса нам важны три параметра:
state
(string) — состояние заказаcreationDate
(array) — временные метки создания заказа «от» и «до»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 заказа в Kaspicode_kaspi
— числовой код заказа в Kaspiid_crm
— строковый идентификатор заказа в CRM (не uuid)status_kaspi
— статус заказа в Kaspistatus_crm
— статус заказа в CRMreceipt
— ссылка на чек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 для таких случаев есть подтверждение через смс. Схема следующая:
ТК доставляет заказ клиенту.
Нам нужно как-то связаться с клиентом и получить от него смс с кодом, который имеет ограниченный срок активности.
С помощью данного кода закрываем заказ на стороне Kaspi.
ТК у нас партнерская, поэтому напрямую интегрировать курьеров в наши системы мы не можем. Но у нас есть Контактный центр (КЦ). Его менеджеры общаются с клиентом и сопровождают покупку. Так что в теории они могут звонить, чтобы подтвердить и закрыть заказ. Данную реализацию логично было бы разместить в CRM, но их команда в тот момент была нарасхват, поэтому мы реализовали это на стороне сайта.
Интерфейс закрытия заказа
Когда статус заказа в CRM — «Завершен», а в Kaspi — еще нет, выводим кнопку «Начать закрытие заказа». По кнопке срабатывает триггер для отправки смс. Смс отправляем через Kaspi API. Менеджер КЦ вводит код от клиента в появившееся поле, и мы успешно закрываем заказ. После этого генерируем чек и отправляем его клиенту.
Минус такого процесса — зависимость от смс-провайдера Kaspi.
Переход на доставку маркетплейса
В скором времени был инициирован переход на доставку средствами маркетплейса. Главное преимущество такой доставки в том, что заказ закрывается автоматически на стороне Kaspi (они сами его доставляют). Поэтому табличка для КЦ теперь используется в качестве быстрого доступа «а что там с заказами».
В связи с новым порядком доставки в процессах произошли изменения:
Type
в заказе теперьKASPI_DELIVERY
(но осталась поддержка и доставки силами нашей ТК).После подтверждения заказа нужно отправить еще один запрос на сборку заказа (изменение статуса на
ASSEMBLE
). После того как заказ в Kaspi оказался в этом статусе, автоматически формируется накладная, доступная по ссылке. Эту ссылку мы передаем в CRM отдельным пакетом, чтобы с их стороны она отправилась на склад. Накладная представляет собой этикетку на упакованном товаре, по которой сотрудники Kaspi понимают, что это за заказ и сколько места он займет в их транспорте.Поскольку ручное закрытие заказа больше нам не требуется, чек генерируем в момент получения от CRM финального статуса заказа
Отмененные клиентами заказы
Дополнительный кейс: заказ в работе, а клиент отменяет заказ в маркетплейсе. По хорошему, мониторинг заказов должен быть реализован на стороне CRM (это мастер система), либо с использованием сайта в качестве адаптера. Но тогда нужно было бы реализовывать дополнительный функционал для обработки новых пакетов на стороне CRM, а свободных ресурсов у их команды не было.
Для таких случаев мы реализовали еще одну схему оповещения. Сайт забирает отмененные заказы и, если они у нас еще в работе, мы генерируем отправку этой информации менеджерам. А менеджеры принимают решение о том, что делать с заказом на стороне CRM.
One more nuance
Период распродаж В Kaspi называется просто «Жұма». В переводе на русский тоже пятница. Такие распродажи проходят три раза в год: в феврале, в июле и в ноябре. Во время последней Жумы Kaspi API не очень хорошо справлялся и приходилось увеличивать таймаут ответа аж до 20 секунд.
Пока готовил статью, мы успели выпустить пару минорных апдейтов, связанных с Kaspi, так что работа продолжается.
Всем рахмет!