Опыт интеграции с VK Market

2022.12.13
Обзор интеграции с одной из популярных отечественных социальных сетей, предоставляющей клиентам возможность организации интернет-магазина, разберем детали со ссылками и решениям проблем

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

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

Задача

В VK есть сообщества (группы), они могут иметь функционал интернет-магазина с возможностью оформления и оплаты заказов, но оплата может быть только через VK Pay.

Для организации интернет-магазина в сообществе VK нужно перейти в Управление - Разделы и выбрать Товары - Расширенные:

Расширенные товары для интернет-магазина в VK

Заказ в админке имеет базовые поля, возможность менять статус заказа и статус оплаты, а также есть возможность вставить ссылку на чек:

Заказ в админке VK

Моя задача заключалась в интеграции облачной кассы с произвольным интернет-магазином VK на базе внутренней интеграционной платформы компании провайдера облачной кассы.

Реализация

У VK есть механизм webhook именуемый Callbak API. При помощи этого механизма, наша интеграция может получать уведомления об интересующих нас событиях.

Заходим в сообщество как администратор, далее переходим в Управление - Работа с API - Callback API - Типы событий.

Типы событий Callback API VK

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

Среди событий сразу замечаются 3 подходящих события:

Но:

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

Значит редактирование заказа является для нашей интеграции триггерным событием для фискализации.

Webhook Редактирование заказа

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

К слову у нас в компании уже был прототип реализации написанный годом ранее, он принимал webhookСоздание заказа и Редактирование заказа, но проблема заключалась в том, что оба эти webhook'а приходили в разных кодировках, один в UTF8, другой в CP-1251. Что немного странно, а тех поддержка не помогла (об этом позже).

Кроме того:

Webhook не присылал ключи delivery и recipient, а preview_order_items содежит 5 рандомных товаров, а нужны все товары.

Недостающие данные обязательны, но у нас уже есть id заказа, а значит есть за что зацепиться :)

Достаем недостающие данные

VK имеет ключ доступа сообщества - это статичный ключ, с которым можно осуществлять запросы к VK API и действовать от именни группы. Он достаточно ограниченный по правам, но при помощи него можно достать недостающие данные, что является необходимым минимумом.

С ключом доступа сообщества можно получить заказ (достаем delivery и recipient) и позиции заказа.

Считаем скидки

Как оказалось, VK отдает данные с непосчитанными скидками, а значит нужно считать самому.

Ранее я уже писал код для пересчета позиций заказа на основании скидки для интеграции с Tilda Publishing, но в этот раз нужна была модификация.

На вход имеем следующие данные:

Считаем процент позиции от общей суммы:

const amountByItems = this.getTotalAmountByItems();

const significances: Array<{ i: number, significance: number }> = [];
this.items.forEach((value, i) => {
    significances.push({
        i,
        significance: Math.round((100.0 / amountByItems) * value.amount * 100) / 100,
    });
});

// сортируем по увеличению значения, чтобы 
significances.sort((a, b) => a.significance - b.significance);

На основании посчитанного процента считаем скидку:

let remainderDiscount = totals.discount;
significances.forEach((value, i) => {
    const { significance } = value;
    const item = this.items[value.i];
    let { amount, price } = item;
    let currDiscount = totals.discount * (significance / 100.0);

    // если считаем последнюю позицию - применяем оставшуюся скидку
    // это проще сделать для самой крупной позиции
    if (i === this.items.length - 1) {
        currDiscount = remainderDiscount;
        amount = Math.round((amount - currDiscount) * 100) / 100;
        price = Math.round((amount / item.quantity) * 100) / 100;
    } else {
        amount = Math.round((amount - currDiscount) * 100) / 100;
        price = Math.round((amount / item.quantity) * 100) / 100;

        // цена позиции не может быть 0 копеек
        // если внезапно так случилось ставим минимально возможную
        if (Math.round(price * 100) < 1) {
            price = 0.01;
        }

        // примененную скидку пересчитываем и отнимаем от общей скидки
        amount = Math.round((price * item.quantity) * 100) / 100;
        currDiscount = item.amount - amount;
    }
    remainderDiscount -= currDiscount;
    this.items[value.i].price = price;
    this.items[value.i].amount = amount;
});

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

Проблема заключалась в малых числах и ограничении количества цифр после запятой (2 цифры на копейки).

Теперь нужно сравнить значения каждой позиции на равенство, и если есть разница надо выровнять:

const items: ItemCollection = [];
this.items.forEach((item) => {
    // если "стоимость позиции" не равна "цена за ед." * "количество"
    // выравниваем числа
    if (item.amount * 100 !== item.price * item.quantity * 100) {
        const qOld = item.quantity;
        let qNew = Math.floor(item.amount / item.price);
        let amountNew = Math.round(item.price * qNew * 100) / 100;
        if (amountNew !== item.amount && qOld === qNew) {
            qNew -= 1.0;
            amountNew = Math.round(item.price * qNew * 100) / 100;
        }
        items.push({
            ...item,
            quantity: qNew,
            amount: amountNew,
        });

        // если не сошлись количества или стоимости
        // тогда в предыдущей операции мы пошли на уменьешние чисел то надо вставить новую позицию
        if (qOld !== qNew || item.amount !== amountNew) {
            const price = Math.round((item.amount - amountNew) * 100) / 100;
            items.push({
                name: item.name,
                price,
                amount: price,
                quantity: 1,
            });
        }
    } else {
        items.push(item);
    }
});

Этот код ждет рефакторинг ...

Ссылка на чек

В заказе есть поле для вставки ссылки на чек (на одном из скринов в самом начале). Вставить ее можно через VK API все тем же ключом сообщества.

Однако, на момент разработки интеграции на странице версий VK API последней была 5.131, но ссылка на чек требует 5.159. Ну ладно, поставим :)

Последняя версия VK API на момент написания интеграции

Требуемая версия VK API на момент написания интеграции

Техподдержка

Когда я столкнулся с разными кодировками в разных webhook'ах то решил обратиться с техническую поддержку VK, но оказалось бесполезно: на вопрос не ответили, ситуацию не исправили. Вот такой диалог у меня состоялся:

Общение с ТП VK

Итог

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

Несмотря на возникшие проблемы с VK, эта интеграция была одной из простых, стабильность данных webhook'ов подвела, но дополнительные инструменты взаимодействия с VK API позволили легко решить проблемы. Такая вариативность есть не в каждом сервисе.

В телеграм канале DevOps от первого лица можно оставить комментарий или почитать интересные истории из практики DevOps