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

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

Что такое Kaspi

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

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

Экраны приложения 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, так что работа продолжается. 

Всем рахмет!