Как устроены интернет-платежи в Dodо

7f45ac4febf18b8816fd1b3703f10621.png

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

Меня зовут Дмитрий Кочнев, я разработчик в команде интернет-платежей, и в статье расскажу о том, какой путь проделала компания в этом направлении, какое положение дел сейчас и какие планы. Статья написана в формате мини-историй в хронологическом порядке, возможно, некоторые из них превратятся в отдельные статьи с более глубоким разбором.

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

Термины онлайн-оплата, интернет-платежи, интернет-эквайринг в рамках статьи — синонимы. Так же синонимы платёжный сервис, платёжный провайдер и провайдер.

Интро

Чтобы в полной мере понимать материал статьи, нужно знать несколько моментов:

  1. Команда Dodo Engineering занимается разработкой сетевых бизнес-моделей, а также автоматизирует их с помощью софта.

  2. Софт, который мы пилим, включая интернет-платежи, — одна большая система, называем её Dodo IS, работает по SaaS-модели.

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

  4. Бизнес-структура компании выглядит так:

    1. Бизнес-структура компании.1. Бизнес-структура компании.

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

От начала времён

Я пришёл в компанию в 2015 году, поэтому начало времён в случае статьи — 2015 год.

Первые интернет-платежи

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

Вот так это работало:

  1. На сайте клиент жмёт кнопку размещения заказа.

  2. Сайт отправляет его на страницу платёжного сервиса.

  3. В платёжном сервисе клиент вводит данные карты, проходит 3DS/

  4. Платёжный сервис отправляет его обратно на сайт.

  5. В фоне платёжный сервис присылает вебхук с результатом оплаты.

  6. Сайт показывает страницу заказа.

2. Старый сайт, редиректная схема оплаты.2. Старый сайт, редиректная схема оплаты.

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

3. Dodo IS в 2015.3. Dodo IS в 2015.

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

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

Плюсы этого решения:

  • расчётами занимается внешняя компания,

  • требуется минимум разработки.

О минусах дальше будет подробнее, а сейчас коротко:

  • сложно сводить бухгалтерию, часть денег может быть в одном сервисе, часть в другом;

  • бывают сложности в коммуникациях с платёжными сервисами.

Свои карточные формы

Время шло, мы решили сделать новый сайт — вне монолита, кастомизируемый под разные страны, в новом дизайне и с лучшим UX. Проект назвали «глобальный сайт». Это тот сайт, который вы сейчас видите на dodopizza.ru, dodopizza.by, dodopizza.de и других доменах. Чуть позже мы начали работать над своим мобильным приложением, для этого форкнули бэкенд сайта и сделали из него API для мобильного приложения.

Идеи по улучшению UX, они же функциональные требования, касающиеся оплаты, были такие:

  • формы для ввода данных карт в нашем сайте и приложениях,

  • новые способы оплаты — сохранённые карты, Google Pay, Apple Pay/

Также были и нефункциональные требования:

  • не обрабатывать и не хранить у себя данные карт клиентов,

  • собрать платёжный функционал в отдельном сервисе,

  • заимплементить сервис по дизайн-доку.

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

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

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

В итоге Dodo IS пришла к такому виду:

4. ДодоИС в 20164. ДодоИС в 2016

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

Так работает оплата новой картой на сайте:

5. Глобальный сайт, оплата по новой карте.5. Глобальный сайт, оплата по новой карте.

Сохранённые карты, Apple Pay и Google Pay работают так же — разница только в том, как собираются данные карт.

Так выглядит на сайте:

6. Глобальный сайт, форма ввода данных карты.6. Глобальный сайт, форма ввода данных карты.

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

  • сделали криво дизайн API-сервиса:

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

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

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

    • не зашарили сразу код CSE/CST и в итоге его написали три раза: на Swift для iOS, на Kotlin для Android, на JS для веба (что такое CSE/CST, расскажу в следующем блоке);

    • всю недостающую функциональность заимплементили в бэкендах мобильного приложения и сайта, причём в каждом по-своему, и сейчас это ~20% кода в каждом из этих сервисов;

  • сделали дизайн сервиса и новой функциональности, опираясь на API одного платёжного сервиса:

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

  • не перенесли весь функционал в платёжный шлюз:

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

    • админка осталась в монолите — как следствие в неё было очень сложно вносить изменения;

  • для прохождения 3DS клиенты всё равно покидают наш сайт, потому что мы редиректим на страницу 3DS, а не показываем её в iframe.

Как иметь у себя форму и не работать с данными карт

Решение о том, что мы не обрабатываем и не храним данные карт клиентов, актуально до сих пор, поэтому рассказываю подробнее. У этой задачи есть два решения: CST и CSE.

CST — client-side tokenization, это когда мы отправляем данные карты клиента прямо с фронтенда в API платёжного сервиса, он возвращает нам идентификатор карты и уже его мы передаём на свой бэкенд и с ним обращаемся к API платёжного сервиса. Соответственно, если мы используем другой платёжный сервис, то и API нужно вызывать другое. Как правило, этот токен одноразовый.

# Request:
curl 'https://api.paymentsos.com/tokens' \
  --data-raw '{
	     "token_type": "credit_card",
	    "card_number": "5105105105105100",
	"expiration_date": "12-24",
	    "holder_name": "Dmitrii Kochnev",
	"credit_card_cvv": "123"}' \
  --compressed

# Response:
{
	          "token": "5957bf0c-aae4-497a-a0a3-3e38772f4a25",
	     "bin_number": "510510",
	  "last_4_digits": "5100",
	"expiration_date": "12/2024",
	    "holder_name": "Dmitrii Kochnev",
	...
}

### 7. Пример CST, кусочки запроса и ответа ###

Пример платёжного сервиса с CST — Zooz.

CSE — client-side encryption, это когда мы шифруем данные карты клиента публичным ключём платёжного сервиса, по алгоритму от платёжного сервиса прямо на фронтенде, в браузере или мобильном приложении и затем передаём на свой бэкенд криптограмму, с которой делаем дальнейшие вызовы API платёжного сервиса. Соответственно, если мы используем другой платёжный сервис, то и шифровать карту нужно будет по другому алгоритму. Как правило, эта криптограмма тоже одноразовая.

const jsencrypt = new JSEncrypt({ key: publicKey })
const data = [pan, year, month, cvv, publicId].join('@')
const cryptogram = jsencrypt.encrypt(data)

/// 8. Сниппет CSE, взял кусочки реального кода из либы платёжного сервиса ///

Пример платёжного сервиса — CloudPayments.

CSE хуже, чем CST, потому что даже зашифрованные данные карты — это всё равно данные карты, и у аудиторов возникает много вопросов относительно того, как мы работаем с этими данными (об этом ещё будет далее в статье).

Используя CST или CSE, мы не работаем с данным карты при первой оплате, и после первой оплаты получаем от платёжного сервиса постоянный идентификатор, который также называют токеном карты, который в дальнейшем используем для оплаты без ввода данных карты, или, как мы называем это, для оплаты сохранённой картой.

У CST и CSE есть пара проблем:

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

  2. Токен предназначен для одного платежа, и сделать несколько транзакций сразу — например, как у Яндекса, округлить сумму и сделать пожертвование в благотворительный фонд второй транзакцией — невозможно.

Нарефандили 10

В 2016 году приключилась такая история — мы вернули клиентам примерно 10 миллионов рублей за выполненные заказы. Вот тут есть разбор ситуации. Крутой факт, что тогда у компании было уже 150 пиццерий!

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

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

  • джоба осталась в монолите. Учитывая, что с монолитом работало большое количество людей, вероятность сломать что-то была выше, чем если бы она была выделена в отдельный сервис;

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

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

Постоянная команда

9. Канал команды в Slack.9. Канал команды в Slack.

В 2018 году задач по платежам и в частности по интеграции новых платёжных сервисов стало настолько много, что появился разработчик и аналитик, которые работали над платежами постоянно и официально появилась «команда платежей» aka адовые шлюзы.

Резервные провайдеры

В 2019 году в каждой стране, даже в России, с сотнями наших ресторанов, у нас был только один платёжный провайдер и мы сильно от него зависели — в случае проблем на его стороне нам не оставалось ничего, кроме как ждать, пока их пофиксят. И эти проблемы стали возникать, причём регулярно: каждые 2 недели по 2 часа что-нибудь работало медленно или было недоступно совсем.

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

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

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

  2. Сохранённые карты у одного платёжного провайдера нельзя использовать с другим платёжным провайдером, ведь по сути это ID, и он имеет смысл только с точки зрения того платёжного провайдера, у которого была сохранена карта.

Обе проблемы актуальны до сих пор, но есть предположения, как можно их решить.

  1. Для решения первой можно сделать отчёты на своей стороне, чтобы было понятно, на какую сумму и через какого провайдера проводились платежи. Но эти суммы нужно считать очень точно, иначе в этом не будет смысла.

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

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

Дринкит и Донер 42

В 2020 у нас появилось две новых концепции — Дринкит и Донер 42, и два новых мобильных приложения (или четыре, если считать iOS и Android отдельно). Естественно, нужно было быстро дать им API для оплаты.

Наш подход в разработке к этому моменту был таким: для каждого мобильного приложения или сайта пишем бэкенд [for frontend] (далее буду называть их BFF), в нашем случае они получаются достаточно толстыми, в частности в моменте интеграции с платёжным шлюзом. Соответственно, у нас появляется два новых BFF, куда нужно встроить платёжное API и четыре мобильных приложения, которые нужно интегрировать с этими API.

Чтобы сделать это быстро и не тратить время на написание одного и того же кода, мы решили сделать nuget-пакет для BFF, в нём зашерить по максимуму код, вплоть до API-контроллеров, и встроить его в BFF приложений Дринкит и Донер 42, а позже и в BFF сайта и приложения Додо Пиццы и таким образом унифицировать код. Для мобильных приложений тоже решили написать по библиотеке, на iOS и Android.

В итоге встроили все эти новые библиотеки только в Дринкит и Донер 42, встроить в Додо Пиццу оказалось чересчур сложно и найти время для этого не получилось, поэтому оставили там всё как есть. В 2022 году это так и работает: мы поддерживаем три разных версии кода интеграции оплаты в BFF, даже четыре, если взять во внимание тот факт, что BFF используют nuget немного по-разному, и как минимум три версии кода в мобильных приложениях.

Какие выводы сделали:

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

  • шарить MVC-контроллеры — плохо, потому что API физически находится внутри приложения, которое использует библиотеку, и у каждого такого приложения, помимо своего API, появляется набор эндпоинтов для оплаты: оно становится толще и неповоротливее и выше вероятность, что оно будет работать нестабильно;

  • шарить бизнес-логику с помощью библиотек — плохо, потому что это создаёт сложности в обновлении: нужно обновлять библиотеки и релизить все приложения, которые используют эти библиотеки, одновременно;

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

10. Dodo IS в 2020.10. Dodo IS в 2020.

Попытка найти готовое решение

В 2021 году мы вспомнили, что платежи — не наш основной бизнес, при этом они отнимают много ресурсов: уже есть команда из трёх бэкендеров, двух мобильных разработчиков, всё это требует специфических знаний и по сложности тянет на отдельный продукт. Мы вроде бы разрабатываем бизнес-модели и автоматизируем бизнес-процессы, а не PSP пилим. И мы решили найт какое-то готовое решение.

Требования были такими:

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

  • легко добавлять новые интеграции и можно делать это как самим, так и отдавать на аутсорс;

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

  • это стоит сопоставимых денег с тем, чтобы пилить всё самим;

  • разворачивается в нашем облаке.

По этим параметрам подходил rbkmoney — по крайней мере, мы сделали такой вывод, пообщавшись с ребятами из компании. Решение было опенсорсное, в микросервисах, и хотя стек отличался от нашего (у нас .NET, а там Erlang + Java, у нас MySQL, там Postgres), и ещё всякие мелочи, но в целом всё было достаточно знакомо. Очень сильно подкупало то, что решение в опенсорсе.

До того, как начинать сотрудничество, решили попробовать развернуть их систему самостоятельно. С горем пополам, примерно за 3–5 недель, усилиями двух человек мы частично развернули решение и, пока разворачивали, изучили весь код. Оказалось, что там далеко не всё, что нужно для самостоятельного допиливания в опенсорсе: некоторые приложения и библиотеки не были опубликованы, и т.к. система большая и сложная, то задача воссоздать недостающие части была неподъёмной, также не было доки, как писать провайдеров, не было примеров. Мы поняли, что встроить в эту систему наших провайдеров без серьёзных доработок вообще не получится. В итоге мы попали бы в зависимость от компании, а уровень коммуникации был слишком плох, чтобы вести какие-то дела — всё было очень медленно, не выходило получить нужную информацию и мы решили откинуть этот вариант.

Попробовали поискать другие варианты, но ничего толкового не нашли. В итоге разочаровались в этой идее и решили пилить всё и дальше самостоятельно. Зато пока ковырялись в чужом решении, которое казалось на пару порядков мощнее нашего, у нас появились новые идеи, как можно развивать своё решение, нашлись решения некоторых наших проблем и мы многому научились. Пример дизайн-решений, которые мы позаимствовали — плагинная система на микросервисах, спецификации и кодогенерация, акторы. Хоть мы и не получили тот результат, на который рассчитывали, время потратили не зря и опыт оказался полезным.

Как ускоряли написание новых интеграций

Наш бизнес растёт достаточно быстро, в планах на 2023 год запуск в 6 новых странах. Иногда мы запускаем бизнес сразу в нескольких странах, и нашей команде нужно поддерживать этот процесс, искать провайдеров, писать интеграции. Возникла идея, что можно отдавать написание новых интеграций с провайдерами на аутсорс.

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

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

Спецификацию решили писать на Зrotobuf, как транспорт, естественно, взяли gRPC. Выбирали между Protobuf/Thrift/Avro, и связка Protobuf+gRPC оказалась самой гибкой: есть инструменты кодогенерации под все нужные платформы, gRPC — быстрый, легковесный и имеет first-class поддержку в .NET и других платформах, и протокол общения очень гибкий, из коробки можно прикладывать метаданные к запросам (это нужно для трейсинга или авторизации).

В итоге система выглядит так:

11. Dodo IS в 2022.11. Dodo IS в 2022.

Мы пока не попробовали отдавать написание плагинов на аутсорс, но, скорее всего, попробуем в 2023 году.

Ещё к этому моменту мы выделили всю платёжную функциональность из монолита — джобу и админку, не стал уделять этому отдельного внимания, но решил пояснить изменения в картинке.

Как мы не заметили истёкший сертификат Apple Pay

На этом моменте мы подходим к тому, чем занимаемся сейчас, но прежде хочу поделиться ещё одной свежей историей из 2022 года. Мне очень нравится Apple Pay как с точки зрения клиента, так и с точки зрения разработки — по-моему, это самый понятный и простой в интеграции кошелёк. Но есть свои проблемы.

Для работы Apple Pay нужно менеджить:

  • процессинговые сертификаты — они нужны для работы Apple Pay в приложении и вебе, используются для расшифровки криптограм Apple Pay, меняются раз в 25 месяцев

  • merchant identity сертификаты — они нужны для работы Apple Pay web, меняются раз в 25 месяцев;

  • валидацию доменов — нужно верифицировать в админке Apple все домены, на которых есть оплата Apple Pay web, и тут есть важная особенность: если у домена закончится SSL-сертификат, то Apple автоматически «разверифаит» домены.

В нашем случае мы менеджим процессинговые сертификаты для каждого платёжного провайдера и десяток доменов.

Первая, казалось бы, безобидная проблема возникла у нас с тем, что стало проблематично купить нормальные SSL-сертификаты на российскую компанию. Мы пользовались короткоживущими сертификатами, и Apple просто засыпал нас письмами о том, что SSL-сертификаты истекают через два месяца, месяц, неделю на постоянной основе. Если действие SSL-сертификатов закончится и домены будут развалидированы, то Apple Pay в браузере перестанет работать.

12. Скрин из моей рабочей почты. Обратите внимание, что сообщения сгруппированы по топику, в некоторых топиках по 8 сообщений…12. Скрин из моей рабочей почты. Обратите внимание, что сообщения сгруппированы по топику, в некоторых топиках по 8 сообщений…

Важно сказать, что если успеть обновить SSL-сертификаты, кажется, за неделю до окончания, то Apple увидит обновлённый сертификат и успокоится. И мы рассчитывали, что так и произойдёт. Но оказалось, что у нас были проблемы с автоматизацией обновления SSL-сертификатов, иногда они просто протухали, и мы в экстренном порядке садились валидировать домены заново. Валидация сводится к тому, чтобы загрузить файлы от Apple на свои сайты, всё это очень сложно.

Дальше — больше: среди этих писем про SSL-сертификаты мы пропустили письмо о том, что у нас кончается процессинговый сертификат Apple у провайдера, который обслуживает почти все пиццерии в Европе. Без процессингового сертификата не будет работать Apple Pay в браузере и мобильных приложениях, то есть совсем. Мы ровно в последний день написали провайдеру, и поменяли сертификаты нам только через 5 дней. Почти неделю в Европе не работал Apple Pay.

В общем, никакого решения проблемы, кроме как купить более длинные SSL-сертификаты, мы делать пока что не стали. Надеемся, что писем счастья станет сильно меньше, менеджить эти сертификаты будет проще и мы больше так не обделаемся. Алерты, которые не требуют действий здесь и сейчас — зло.

Наши дни

Сейчас мы работаем в 16 странах, у нас почти 900 ресторанов, 15+ провайдеров и 150 платежей в минуту. Над платежами работает такая команда: 2 бэкендера, 1 фулстек, 1 продакт плюс 1 новенький продакт, итого — 5 человек. Так же к нам для проектной работы подключились 4 мобильных разработчика. Итого — 9. И сейчас немного о том, что у нас в работе прямо сейчас.

Делаем процессинг надёжным и быстрым

Как я уже говорил, у нас 15+ провайдеров, и все интеграции с ними разные, у каждой есть свои проблемы, к тому же есть и некоторый набор проблем во взаимодействии сервисов внутри Dodo IS, которые тоже нужно решать.

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

13. Как нужно исполнять команды.13. Как нужно исполнять команды.

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

Вторая проблема заключается в том, что одни платежи аффектят другие, то есть один медленный платёж может мешать прохождению других платежей. Поясню: мы не вызываем внешние сервисы, например, API провайдера, в рамках запросов к платёжному шлюзу, потому что API провайдера может отвечать долго — секунды, иногда минуты. Вместо этого мы кидаем команды в локальную очередь на Rabbit и затем исполняем. С учётом того, что платежей сотни в минуту, это очень серьёзная проблема.

Подобьём все вводные:

  • наше приложение всегда запущено в нескольких экземплярах;

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

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

  • взаимодействие с API провайдеров — медленное, секунды или минуты;

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

Проблема, к слову, стандартная при использовании очередей, и называется head-of-line blocking, или HoLB. Немного визуализации, чтоб было совсем понятно:

14. Head-of-line blocking.14. Head-of-line blocking.

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

Мы решили сделать независимые очереди команд для каждого платежа, а сами команды превратить в саги. В качестве базовой технологии выбрали акторный фреймворк Microsoft Orleans, саги написали сами.

В нашем решении:

  1. Очередь каждого платежа — актор.

  2. Команды в очереди исполняются последовательно.

  3. Каждая очередь исполняется независимо от остальных очередей.

  4. Каждая команда в очереди — сага.

  5. Операции в саге исполняются последовательно.

  6. Очередь каждого платежа существует только в одном экземпляре в кластере, нет конкурентного доступа между разными инстансами приложения.

  7. Акторы имеют мейлбокс и обрабатывают входящие сообщения последовательно, поэтому отпадает необходимость в распределённых блокировках.

  8. Акторы достаточно легковесные, одна машина может вмещать десятки и сотни тысяч акторов.

Концептуально решение выглядит вот так:

15. Очереди по платежам.15. Очереди по платежам.

Собираем код в кучу

На предыдущих картинках много красных блоков — это всё куски платёжной функциональности, которые протекли за пределы платёжного шлюза. Подробнее о проблемах.

  1. В каждом BFF проксируются все вызовы к платёжному шлюзу, это тысячи строк кода.

    Для решения проблемы мы делаем публичное платёжное API, с которым клиентские приложения будут взаимодействовать напрямую. Всё, что останется в BFF, —это один вызов платёжного шлюза, чтобы зарегистрировать платёж. Этот вызов содержит чувствительные данные, такие как сумма, пиццерия, клиент и заказ, поэтому его нужно вызывать с бэкенда. В ответ платёжный шлюз будет возвращать короткий авторизационный токен для доступа к API напрямую.

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

    Для решения этой проблемы делаем так, чтобы API платёжного шлюза отдавало данные в пригодном для использовании виде.

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

    Для решения проблемы переносим эту логику в платёжный шлюз.

16. Dodo IS в 202316. Dodo IS в 2023

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

Event Sourcing

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

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

С настройками системы чуть более интересно, тут человеческий фактор играет бОльшую роль. Кто-то мог просто случайно ввести неправильные значения доступов к API провайдера или испортить правильные, а проверить это при большом количестве настроек достаточно сложно. По сути нужно провести платёж, а зачастую несколько платежей разными способами. Проверки не всегда автоматизируются, если речь про Apple Pay или что-то такое. А решение проблемы зачастую может быть в том, чтобы просто восстановить предыдущие корректные значения. К слову, у нас этих настроек примерно 3 тысячи на сеть из 800+ ресторанов.

Если мы работаем с данными по принципу «изменили объект — обновили строчку в базе данных», то всё плохо: восстановить состояние сложно, данные теряются при перезаписи, остаётся использовать логи, а это, как мы выяснили, не самый надёжный источник информации.

В общем, для решения этих проблем мы решили применить event sourcing.

Что он нам даёт:

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

А также пара бонусов:

  • хранилище становится append-only, а это значит, что мы не можем закорраптить данные;

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

Про транзакции хочется сказать отдельно. Транзакции имеют тенденцию всасывать в себя всё больше и больше со временем. Хочет

© Habrahabr.ru