Когда ваш бизнес становится глобальным, его приложение приходится адаптировать под новые реалии — обновлять вёрстку, добавлять переводы и изображения. Ну а если ваше приложение тесно связано со сторонним железом, то тут уже приходится включать фантазию на всю катушку.

Два года назад @rexepted написал статью о том, как мы работаем с чеками в разных странах. В ней он описал принцип работы нашей плагинной системы для соединения с разными моделями контрольно-кассовых машин (ККМ). А ещё рассказал о том, как она работает с нашим frontend-приложением через IPC-шину (inter-process communication). С того времени мы запустились ещё в 7 странах, а число наших точек увеличилось на треть — с 890 до 1200+. Благодаря нашей плагинной системе мы ни дня не потратили на интеграцию нашего софта с налоговым ПО новых стран. Этим теперь занимались команды на аутсорсе.

Однако мы добиваемся 100-процентной диджитализации опыта в наших кофейнях и ресторанах и постоянно создаём новые гаджеты для них. Так у нас появились киоски самообслуживания — им и нужна интеграция с POS-терминалами. Как и в случае с ККМ, мы сами занимались интеграцией с POS-терминалами и, выходя на новые рынки, поняли, что больше этого делать не можем.

Киоск самообслуживания

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

Кассир взаимодействует с кассой ресторана

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

Задача

Нам необходимо:

  • научить кассы и киоски принимать оплаты через единые контракты;

  • прикрутить оплату заказа не только через терминал, но и с помощью QR-кода;

  • дать возможность любой аутсорс-команде работать над нашей плагинной системой;

  • дать киоскам и кассам возможность выбирать, с каким платёжным плагином они будут работать;

  • собрать плагины двух типов Web- и Electron-.

Контракты

Контракты — это соглашения приложений или разных их частей о взаимодействии друг с другом. Они определяют набор правил и форматов данных для обеспечения корректного взаимодействия и обмена информацией между компонентами.

Для того, чтобы плагин мог взаимодействовать с кассой или киоском, нам нужны универсальные контракты — они подходят к 90% терминалов. Если коротко, то образ результата такой: одна вилка для розеток самых разных регионов.

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

Выделим основные методы. Пока что их два:

pay — лежит в основе всего, ведь POS-терминал должен, прежде всего, принимать оплату. Этому методу мы сообщаем сумму и тип оплаты: карта или QR.

refund — терминал должен вернуть деньги, если клиент откажется от покупки. Одним терминалам достаточно сообщить сумму покупки и приложить карту, а некоторым для возврата нужен ID транзакции или RRN.

Затем определим, что ещё умеют POS-терминалы и как мы можем их использовать в будущем. Мы собрали следующие необязательные функции:

  • отмена транзакции прямо в интерфейсе приложения для более удобного UX;

  • HealthCheck — проверка связи с терминалом;

  • получение данных о последнем платеже для возврата средств. Поможет, если оплата прошла, а информацию о ней сервис не получил или вообще не создал заказ;

  • получение статуса платежа по его идентификаторам. Очень актуально для QR-платежей;

  • закрытие банковской смены, Z-отчёт;

  • получение банковского слипа. Например, для печати через принтер ККМ, а не POS-терминала;

  • отображение настроек плагина для сохранения полезной информации в локальном хранилище. Например, IP терминала в локальной сети.

Опираясь на прошлый опыт разработки плагинных систем, мы понимали, что на старте фронтенд-приложения, всё это дело нужно инициализировать. Для этого мы добавили обязательный метод Initialize. Он возвращает функционал плагина из перечня выше и доступные способы оплаты — карта, QR-код или всё вместе. На выходе получаем такой интерфейс:

Приватные контракты IPaymentsIntegration отличаются от публичных IPaymentPlugin лишь парой новых методов. Для реализации этих контрактов мы изолируем логику общения с бэкендом и получения каких-либо данных. Например, в приватном методе Initialize мы сначала узнаём актуальную версию и тип плагина, скачиваем его и устанавливаем в зависимости от типа.

Архитектура приложения

Чтобы быстрее объяснить устройство нашей архитектуры, напомню, что у нас есть приложение кассы, написанное с помощью Electron. Оно общается с различными ККМ через IPC-коммуникацию. У киосков самообслуживания есть аналогичная Electron программа, а также iOS-приложение.

Если терминал поддерживает беспроводное соединение — например, HTTP — его можно реализовать как web-плагин. Если нужно что-то посерьезнее — проводная связь через USB или COM-порты —, то нам пригодится именно Electron плагин. Он может достучаться до «железок», а обычный браузер нет.

Ниже представлена схема взаимодействия наших frontend-приложений с платёжными плагинами:

Архитектура плагинной системы

Как это выглядит на практике? При загрузке кассы ресторана или киоска самообслуживания импортируется микрофронтенд — объект, реализующий приватные контракты IPaymentsIntegration. После этого вызывается метод Initialize. Он скачивает и устанавливает актуальную версию плагинов, доступных для текущего устройства.

Вызывая другие методы, мы уже знаем, какой плагин инициализирован. На основе его типа получаем wrapper, а он вызывает код самого плагина. Многие вещи уже реализованы в нашем микрофронтенде:

  • гарантии доставки данных до нашего бэкенда;

  • фоновые джобы, отслеживающие статус QR-платежей;

  • установка и кеширование версий плагина;

  • логирование всех операций для понимания того, что происходит с оплатой на конкретной кассе или киоске.

Как происходит разработка плагина

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

Для примера создадим тестовый web-плагин:

Видим, что файлы для нового плагина добавились. В них входят:

  • базовые файлы настройки TypeScript;

  • конфигурация eslint, чтобы код был красивый и единообразный;

  • базовая vite-конфигурация для простейшей сборки плагина.

При выполнении команды добавляется и GitHub Action. Он позволяет опубликовать версию плагина при мёрже кода в основную ветку.

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

Код написан и протестирован на локальном стенде. Разработчики приносят Pull Request. Если в нём нет ничего криминального, то мы мёржим его в основную ветку. После успешных тестов и билда плагин публикуется в блоб-хранилище, где ему указывается актуальная версия в файле version.

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

Администрирование и настройка

Возникает вопрос: как конкретная касса или киоск узнает, с каким плагином нужно работать? Когда мы только запустили плагинную систему, эта информация была в коде. Первый плагин был написан для касс в Кыргызстане. Их идентификатор указывал на страну, а для неё определялся конкретный плагин.

Один POS-терминал — одна страна? Так просто? На самом деле нет. Иногда в одной стране могут быть несколько интеграций с разными терминалами. А ещё же есть оплата по QR-коду, которая идёт параллельно POS-терминалам.

В итоге мы решили собрать админку для платежей в нашей системе Dodo IS, в которой для конкретных касс и киосков указывались доступные платёжные провайдеры. Оставался всего один вопрос: где хранить логины, пароли, идентификаторы и т.д. каждого из провайдеров?

Мы вспомнили об админке для платежей в киоске — её мы собрали для старых интеграций и решили доработать. Теперь при настройке киоска — а в будущем и кассы — мы можем указать его настройки и доступных для него провайдеров.

Как это работает? Сначала нам нужно добавить к устройству тип оплаты. Подразумевается, что платёжные провайдеры есть двух видов. У первого нет никаких параметров — всё настраивается на самом устройстве, например, после указания IP терминала.

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

Второй вид посложнее. Ему нужны настройки и секретная информация, такая как в примере с KaspiQR.

Пример QR-провайдера, которому нужны дополнительные настройки для работы с плагином

Как это выглядит в интерфейсе настроек:

Всё идеально?

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

  • добавить в наш бэкенд данные нового платёжного провайдера и его мета-информацию о транзакциях — обычно transactionID и RRN, но иногда и другие данные. Так мы сможем хранить всю информацию в нашей базе данных в нормальном виде;

  • указать настройки, которые поддерживает платёжный провайдер. В большинстве случаев достаточно указать локальный IP терминала на самом устройстве. Однако оплаты по QR-коду требуют логины и пароли для общения с сервисами банка и определением счёта, на который нужно зачислить средства;

  • добавить платёжный провайдер в админку, чтобы привязать его к конкретному устройству и настроить;

  • указать в плагинной системе местонахождение плагина для конкретного платёжного провайдера.

Однако это просто рутина. Любой разработчик из нашей команды справится с этими задачками за 2-3 часа.

Что в итоге?

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

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

Принимать платежи с помощью QR-кодов мы учились в Казахстане. Для этого мы добавили в качестве способа оплаты KaspiQR — с этим мы тоже справились быстро.

В это же время наши аутсорс-команды, собиравшие плагины для ККМ, разработали платёжные плагины для Беларуси и Кипра. Для Беларуси, кстати, уже тестируют и планируют активно внедрять во всех ресторанах страны. А вот Кипр уже успешно работает с новой интеграцией.

Планы на будущее

Осталось всего 20 интеграций в разных странах, но на каждую мы не потратим больше 2-3 часов. Теперь новые страны смогут открываться без нашего участия, а мы сможем сфокусироваться на других задачах и проектах.

В будущем хотим попробовать устройства 2 в 1. Они часто встречаются, когда мы ищем платёжных провайдеров в других странах. Эти устройства умеют принимать оплату, фискализировать и печатать чеки. Конечно, их пока нельзя привязать к нашим плагинным системам, но, возможно, мы подумаем о том, как их применять в наших реалиях. Но об этом уже как-нибудь в другой раз…