Одно из направлений разработки в Dodo — интернет-платежи. Для компании это скорее утилити-функция, чем основной бизнес, но всё же нам приходится делать кучу всего, чтобы дать клиентам лучший UX, и у нас накопился опыт, которым хочется поделиться.
Меня зовут Дмитрий Кочнев, я разработчик в команде интернет-платежей и в статье расскажу о том, какой путь проделала компания в этом направлении, какое положение дел сейчас и какие планы. Статья написана в формате мини-историй, возможно, некоторые из них превратятся в отдельные статьи с более глубоким разбором.
Сразу отмечу, что речь пойдёт только об интернет-эквайринге — о торговом эквайринге тут ничего нет. Разница между ними в том, что интернет-эквайринг используется, когда вы оплачиваете что-то в интернете, на сайте или в приложениях, а торговый эквайринг используется в магазинах и ресторанах, подробнее тут.
Термины онлайн-оплата, интернет-платежи, интернет-эквайринг в рамках статьи — синонимы. Так же синонимы платёжный сервис, платёжный провайдер и провайдер.
Интро
Чтобы в полной мере понимать материал статьи, нужно знать несколько моментов:
Команда Dodo Engineering занимается разработкой сетевых бизнес-моделей, а также автоматизирует их с помощью софта.
Софт, который мы пилим, включая интернет-платежи, — одна большая система, называем её Dodo IS, работает по SaaS-модели.
Каждый партнёр-франчайзи управляет своими ресторанами независимо от других франчайзи или управляющей компании: ведёт свою бухгалтерию, получает деньги от клиентов напрямую.
-
Бизнес-структура компании выглядит так:
Развитие направления интернет-платежей непосредственно связано с развитием основного бизнеса, поэтому для полноты повествования я ссылаюсь на реальные проекты, которые привели к каким-либо изменениям в работе с платежами, и объясняю логику принятия решений — по-крайней мере так, как её понимаю я.
От начала времён
Я пришёл в компанию в 2015 году, поэтому начало времён в случае статьи — 2015 год.
Первые интернет-платежи
В 2015 у нас была пара десятков пиццерий и сайт, на котором можно было создать заказ и оплатить его онлайн. И был один вариант оплаты онлайн — через редирект на страницу платёжного сервиса.
Вот так это работало:
На сайте клиент жмёт кнопку размещения заказа.
Сайт отправляет его на страницу платёжного сервиса.
В платёжном сервисе клиент вводит данные карты, проходит 3DS.
Платёжный сервис отправляет его обратно на сайт.
В фоне платёжный сервис присылает вебхук с результатом оплаты.
Сайт показывает страницу заказа.
В это время вся функциональность платежей сводилась к паре десятков классов в нашем монолитном приложении: сайт, платежи и всё остальное были частью монолита. Выделенных разработчиков для платежей, естественно, не было, так как функционал примитивный.
Кажется, что тут ничего интересного нет, но на самом деле именно в этот момент в компании приняли одно из важнейших решений, которое определило вектор развития платежей в дальнейшем и которое актуально до сих пор: мы не агрегируем деньги на своих счетах и отдаём эту работу платёжным сервисам — партнёры-франчайзи получают деньги напрямую.
Для того чтобы это было возможно, франчайзи заключают договоры с платёжными сервисами напрямую, мы же даём возможность ввести свои настройки, условно логины и пароли, для доступа к платёжному сервису в админке.
Плюсы этого решения:
расчётами занимается внешняя компания,
требуется минимум разработки.
О минусах дальше будет подробнее, а сейчас коротко:
сложно сводить бухгалтерию, часть денег может быть в одном сервисе, часть в другом;
бывают сложности в коммуникациях с платёжными сервисами.
Свои карточные формы
Время шло, мы решили сделать новый сайт — вне монолита, кастомизируемый под разные страны, в новом дизайне и с лучшим UX. Проект назвали «глобальный сайт». Это тот сайт, который вы сейчас видите на dodopizza.ru, dodopizza.by, dodopizza.de и других доменах. Чуть позже мы начали работать над своим мобильным приложением, для этого форкнули бэкенд сайта и сделали из него API для мобильного приложения.
Идеи по улучшению UX, они же функциональные требования, касающиеся оплаты, были такие:
формы для ввода данных карт в нашем сайте и приложениях,
новые способы оплаты — сохранённые карты, Google Pay, Apple Pay.
Также были и нефункциональные требования:
не обрабатывать и не хранить у себя данные карт клиентов,
собрать платёжный функционал в отдельном сервисе,
заимплементить сервис по дизайн-доку.
Мы решили сделать формы у себя, чтобы клиенты не покидали приложения в процессе заказа — так рассчитывали повысить конверсию. А не обрабатывать и не хранить у себя данные карт клиентов решили, потому что были молоды и неопытны, времени разбираться, как сделать правильно, не было и мы боялись облажаться.
В то время ещё не было выделенной команды для работы над платежами, поэтому для работы над проектом собрали новую проектную мини-команду из двух человек. Они не имели опыта работы с платежами — было непонятно, как платежи могут работать и выглядеть в принципе.
Это было нормальным решением, особенно с учётом того, что был дизайн-док, но в процессе разработки оказалось, что предложенный дизайн сервиса покрывает лишь старый функционал — редиректную оплату — и совершенно не учитывает проблем, связанных с новым функционалом, новыми способами оплаты. Пришлось импровизировать. Сервис, в котором собрали функциональность платежей, назвали платёжный шлюз.
В итоге Dodo IS пришла к такому виду:
На диаграмме здесь и далее будут только те части системы, которые интересны с точки зрения оплаты. Красным отмечены куски платёжной функциональности, которые размазались по системе, стрелочки добавлены только для связей, относящихся к платежам. Красные куски потенциально требуют изменений каждый раз, когда надо добавить нового провайдера или поправить какую-то функциональность.
Так работает оплата новой картой на сайте:
Сохранённые карты, Apple Pay и Google Pay работают так же — разница только в том, как собираются данные карт.
Так выглядит на сайте:
Ретроспективно, когда проект завершился, мы получили такой список проблем:
-
сделали криво дизайн 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 есть пара проблем:
Токены и криптограммы одного платёжного сервиса не подходят к API другого провайдера. Мы это не предусмотрели сразу и позже эта проблема дала о себе знать.
Токен предназначен для одного платежа, и сделать несколько транзакций сразу — например, как у Яндекса, округлить сумму и сделать пожертвование в благотворительный фонд второй транзакцией — невозможно.
Нарефандили 10
В 2016 году приключилась такая история — мы вернули клиентам примерно 10 миллионов рублей за выполненные заказы. Вот тут есть разбор ситуации. Крутой факт, что тогда у компании было уже 150 пиццерий!
Вкратце перескажу: мы не перенесли джобу из монолита в отдельный сервис вместе с остальным платёжным функционалом, у джобы остались конфиги в монолите и она работала на данных в монолитной базе данных. В один из дней на дев налили реальные данные о заказах и платежах, но обрезанные, и, так как доступы к API платёжного сервиса были общие на деве и проде, джоба обнаружив платежи, к которым нет заказов, просто их все поотменяла.
Собственно, на мой взгляд, главные проблемы были такие:
джоба осталась в монолите. Учитывая, что с монолитом работало большое количество людей, вероятность сломать что-то была выше, чем если бы она была выделена в отдельный сервис;
недооценивали силу джобы. Так как не было отдельной команды по платежам, то платежи были частью чей-то ответственности, и, возможно, скоуп был слишком широким, чтобы уделять достаточно внимания всем фичам.
На тот момент мы решили проблему административно, т.е. стали более аккуратно работать с данными и конфигами, лучше тестировать.
Постоянная команда
В 2018 году задач по платежам и в частности по интеграции новых платёжных сервисов стало настолько много, что появился разработчик и аналитик, которые работали над платежами постоянно и официально появилась «команда платежей» aka адовые шлюзы.
Резервные провайдеры
В 2019 году в каждой стране, даже в России, с сотнями наших ресторанов, у нас был только один платёжный провайдер и мы сильно от него зависели — в случае проблем на его стороне нам не оставалось ничего, кроме как ждать, пока их пофиксят. И эти проблемы стали возникать, причём регулярно: каждые 2 недели по 2 часа что-нибудь работало медленно или было недоступно совсем.
Тогда мы задумались о том, что пора бы обзавестись резервными платёжными провайдерами, положили задачи в бэклог и продолжили мириться с проблемами. Подтолкнуло эти задачи в разработку то, что платёжные сервисы стали сами предлагать более низкие комиссии и выгода стала существенной.
Так, в 2020 году, мы обзавелись первым резервным провайдером в России и, казалось бы, проблема решена, но на деле оказалось всё не так просто. Однажды, когда пришлось переключиться на резервного провайдера, мы обнаружили две новых проблемы:
Партнёрам сложно сводить бухгалтерию в случае, когда мы делаем короткие переключения между провайдерами.
Сохранённые карты у одного платёжного провайдера нельзя использовать с другим платёжным провайдером, ведь по сути это ID, и он имеет смысл только с точки зрения того платёжного провайдера, у которого была сохранена карта.
Обе проблемы актуальны до сих пор, но есть предположения, как можно их решить.
Для решения первой можно сделать отчёты на своей стороне, чтобы было понятно, на какую сумму и через какого провайдера проводились платежи. Но эти суммы нужно считать очень точно, иначе в этом не будет смысла.
Для решения второй можно хранить данные карт у себя, чтобы иметь возможность передавать их в нужный платёжный сервис
Возможно, если бы мы считали все убытки от тех инцидентов, задачу решили бы раньше, а возможно и нет, кто знает. Нужно понимать, что потери могут быть не только прямые, когда клиенты пытались, но не смогли оплатить заказы, но и репутационные — клиенты могли уйти туда, где оплата в тот момент работала, и это достаточно сложно посчитать.
Дринкит и Донер 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 и всем, чем нужно, не размазывать ответственность и минимизировать жёсткость связей между сервисами.
Попытка найти готовое решение
В 2021 году мы вспомнили, что платежи — не наш основной бизнес, при этом они отнимают много ресурсов: уже есть команда из трёх бэкендеров, двух мобильных разработчиков, всё это требует специфических знаний и по сложности тянет на отдельный продукт. Мы вроде бы разрабатываем бизнес-модели и автоматизируем бизнес-процессы, а не PSP пилим. И мы решили найти какое-то готовое решение.
Требования были такими:
решение в основном пилится сторонними разработчиками, но мы тоже можем что-то пилить под себя;
легко добавлять новые интеграции и можно делать это как самим, так и отдавать на аутсорс;
решение позволяет осуществить плавный переход, т.е. поддерживает все те интеграции, которые у нас есть сейчас;
это стоит сопоставимых денег с тем, чтобы пилить всё самим;
разворачивается в нашем облаке.
По этим параметрам подходил rbkmoney — по крайней мере, мы сделали такой вывод, пообщавшись с ребятами из компании. Решение было опенсорсное, в микросервисах, и хотя стек отличался от нашего (у нас .NET, а там Erlang + Java, у нас MySQL, там Postgres), и ещё всякие мелочи, но в целом всё было достаточно знакомо. Очень сильно подкупало то, что решение в опенсорсе.
До того, как начинать сотрудничество, решили попробовать развернуть их систему самостоятельно. С горем пополам, примерно за 3-5 недель, усилиями двух человек мы частично развернули решение и, пока разворачивали, изучили весь код. Оказалось, что там далеко не всё, что нужно для самостоятельного допиливания в опенсорсе: некоторые приложения и библиотеки не были опубликованы, и т.к. система большая и сложная, то задача воссоздать недостающие части была неподъёмной, также не было доки, как писать провайдеров, не было примеров. Мы поняли, что встроить в эту систему наших провайдеров без серьёзных доработок вообще не получится. В итоге мы попали бы в зависимость от компании, а уровень коммуникации был слишком плох, чтобы вести какие-то дела — всё было очень медленно, не выходило получить нужную информацию и мы решили откинуть этот вариант.
Попробовали поискать другие варианты, но ничего толкового не нашли. В итоге разочаровались в этой идее и решили пилить всё и дальше самостоятельно. Зато пока ковырялись в чужом решении, которое казалось на пару порядков мощнее нашего, у нас появились новые идеи, как можно развивать своё решение, нашлись решения некоторых наших проблем и мы многому научились. Пример дизайн-решений, которые мы позаимствовали — плагинная система на микросервисах, спецификации и кодогенерация, акторы. Хоть мы и не получили тот результат, на который рассчитывали, время потратили не зря и опыт оказался полезным.
Как ускоряли написание новых интеграций
Наш бизнес растёт достаточно быстро, в планах на 2023 год запуск в 6 новых странах. Иногда мы запускаем бизнес сразу в нескольких странах, и нашей команде нужно поддерживать этот процесс, искать провайдеров, писать интеграции. Возникла идея, что можно отдавать написание новых интеграций с провайдерами на аутсорс.
До этого момента у нас был один солюшен платёжного шлюза, в котором мы писали весь код, в том числе интеграции с провайдерами. Кода было много и всё достаточно сильно переплеталось. Мы решили чётко очертить границы плагинов — выделить их в stateless-микросервисы, написать для них спецификацию и документацию. По задумке, мы можем дать внешним разработчикам документацию провайдера, нашу спецификацию и документацию, и они смогут написать нам плагин.
Stateless-микросервисы: мы решили сделать плагины простыми, чтобы они не имели зависимостей вроде баз данных или очередей, у нас сейчас 20 плагинов, и управлять 20 базами данных не очень хочется.
Спецификацию решили писать на Protobuf, как транспорт, естественно, взяли gRPC. Выбирали между Protobuf/Thrift/Avro, и связка Protobuf+gRPC оказалась самой гибкой: есть инструменты кодогенерации под все нужные платформы, gRPC — быстрый, легковесный и имеет first-class поддержку в .NET и других платформах, и протокол общения очень гибкий, из коробки можно прикладывать метаданные к запросам (это нужно для трейсинга или авторизации).
В итоге система выглядит так:
Мы пока не попробовали отдавать написание плагинов на аутсорс, но, скорее всего, попробуем в 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 в браузере перестанет работать.
Важно сказать, что если успеть обновить 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, которые тоже нужно решать.
Одна из проблем заключается в том, что если с платежом производится какая-то асинхронная операция, например, мы пытаемся захолдировать деньги на счету клиента, и мы пока не получили результат операции, то нельзя начинать делать никакую другую операцию с платежом, например, возврат, иначе мы потеряем состояние платежа и эта отмена никогда не произойдёт. При этом, если у нас вызвали ту же отмену, то эту операцию нельзя просто отклонить — если у нас уже попросили отмену, то вряд ли вызывающая сторона может передумать — её нужно поставить в очередь.
Сначала мы решали эту проблему с помощью распределённых блокировок платежа, т.е. когда один поток выполняет операцию над платежом, другой ждёт, пока освободится блокировка. Распределённая она потому, что у нас есть несколько экземпляров приложения, и с одним платежом могут работать разные экземпляры приложения в кластере.
Вторая проблема заключается в том, что одни платежи аффектят другие, то есть один медленный платёж может мешать прохождению других платежей. Поясню: мы не вызываем внешние сервисы, например, API провайдера, в рамках запросов к платёжному шлюзу, потому что API провайдера может отвечать долго — секунды, иногда минуты. Вместо этого мы кидаем команды в локальную очередь на Rabbit и затем исполняем. С учётом того, что платежей сотни в минуту, это очень серьёзная проблема.
Подобьём все вводные:
наше приложение всегда запущено в нескольких экземплярах;
мы используем распределённые блокировки через базу данных для контроля concurrency над платежом;
используем очереди на RabbitMQ для выполнения команд, и RabbitMQ раскидывает сообщения раундробином;
взаимодействие с API провайдеров — медленное, секунды или минуты;
взаимодействие с API провайдеров предполагает получение разных асинхронных уведомлений с помощью поллинга или http-коллбеков, и этих уведомлений может быть много для каждого платежа.
Проблема, к слову, стандартная при использовании очередей, и называется head-of-line blocking, или HoLB. Немного визуализации, чтоб было совсем понятно:
Проблему можно решать увеличением количества экземпляров консюмеров, количества сообщений, которые достаются за раз, но только до поры, до времени: вскоре начнут кончаться подключения к базе данных, которые нужны, чтобы сделать распределённые блокировки, и консюмеры будут простаивать на блокировках большую часть времени вместо того, чтобы делать полезную работу.
Мы решили сделать независимые очереди команд для каждого платежа, а сами команды превратить в саги. В качестве базовой технологии выбрали акторный фреймворк Microsoft Orleans, саги написали сами.
В нашем решении:
Очередь каждого платежа — актор.
Команды в очереди исполняются последовательно.
Каждая очередь исполняется независимо от остальных очередей.
Каждая команда в очереди — сага.
Операции в саге исполняются последовательно.
Очередь каждого платежа существует только в одном экземпляре в кластере, нет конкурентного доступа между разными инстансами приложения.
Акторы имеют мейлбокс и обрабатывают входящие сообщения последовательно, поэтому отпадает необходимость в распределённых блокировках.
Акторы достаточно легковесные, одна машина может вмещать десятки и сотни тысяч акторов.
Концептуально решение выглядит вот так:
Собираем код в кучу
На предыдущих картинках много красных блоков — это всё куски платёжной функциональности, которые протекли за пределы платёжного шлюза. Подробнее о проблемах.
-
В каждом BFF проксируются все вызовы к платёжному шлюзу, это тысячи строк кода.
Для решения проблемы мы делаем публичное платёжное API, с которым клиентские приложения будут взаимодействовать напрямую. Всё, что останется в BFF, —это один вызов платёжного шлюза, чтобы зарегистрировать платёж. Этот вызов содержит чувствительные данные, такие как сумма, пиццерия, клиент и заказ, поэтому его нужно вызывать с бэкенда. В ответ платёжный шлюз будет возвращать короткий авторизационный токен для доступа к API напрямую.
-
API платёжного шлюза даёт данные, например, о доступных способах оплаты заказа в очень сыром виде, который не пригоден для использования без сложной обработки, опять же — тысячи строк кода.
Для решения этой проблемы делаем так, чтобы API платёжного шлюза отдавало данные в пригодном для использовании виде.
-
Некоторая логика, например, фильтрация доступных способов для оплаты заказа делается средствами BFF, что приводит к тому, что BFF содержат кучу кода, который, по идее, должен быть в платёжном шлюзе и снова тысячи строк кода.
Для решения проблемы переносим эту логику в платёжный шлюз.
Такое количество технического долга мы накопили из-за недостатка опыта и ресурсов.
Event Sourcing
Вопрос денег очень чувствительный для многих людей, в том числе и для меня: очень неприятно оказываться в ситуации, когда у тебя списали деньги с карты и не привезли заказ, или когда набрал полную корзину товаров, а оплата не проходит. Бывает и такое, что клиентам деньги вернулись автоматически, но клиенты этого не поняли и задают вопросы. Причин, по которым такие проблемы могут возникнуть великое множество — от кривых настроек системы до проблем с проведением операций на стороне провайдера. Надо уметь их решать, причём быстро.
Первым делом при возникновении любых проблем хочется понять, как платёж или настройки оказались в том или ином состоянии и воссоздать историю изменений. Обычно для этого используются логи, но с логами есть очень большая проблема: их нужно не забыть написать и не упустить ничего, что может быть важно, они могут потеряться да и просто быть в неконсистентном состоянии.
С настройками системы чуть более интересно, тут человеческий фактор играет бОльшую роль. Кто-то мог просто случайно ввести неправильные значения доступов к API провайдера или испортить правильные, а проверить это при большом количестве настроек достаточно сложно. По сути нужно провести платёж, а зачастую несколько платежей разными способами. Проверки не всегда автоматизируются, если речь про Apple Pay или что-то такое. А решение проблемы зачастую может быть в том, чтобы просто восстановить предыдущие корректные значения. К слову, у нас этих настроек примерно 3 тысячи на сеть из 800+ ресторанов.
Если мы работаем с данными по принципу «изменили объект — обновили строчку в базе данных», то всё плохо: восстановить состояние сложно, данные теряются при перезаписи, остаётся использовать логи, а это, как мы выяснили, не самый надёжный источник информации.
В общем, для решения этих проблем мы решили применить event sourcing.
Что он нам даёт:
полную историю изменений всех объектов, а значит, возможность просмотреть и восстановить состояние любого объекта в любой момент времени.
А также пара бонусов:
хранилище становится append-only, а это значит, что мы не можем закорраптить данные;
нам больше не нужны транзакции, а значит, код становится проще.
Про транзакции хочется сказать отдельно. Транзакции имеют тенденцию всасывать в себя всё больше и больше со временем. Хочется сохранить ещё какой-нибудь объект, почему бы не сделать это всё в той транзакции, которая уже есть?
В данный момент, после успешного холда мы публикуем нотификацию, что платёж к некоторому заказу прошёл и можно приступать к его приготовлению. Чтобы нотификации работали надёжно, мы пишем изменение состояния платежа и нотификацию в базу в транзакции, по сути используем паттерн outbox. В чём, собственно, проблема?
В том, что размываются границы аггрегатов, ответственность репозиториев: они теперь и платежи сохраняют, и нотификации, да и просто надо не забыть эти нотификации отправить везде, куда нужно!
С event sourcing мы просто подписываемся на своё хранилище, и когда возникает событие «платёж захолдирован» — отправляем нотификацию. Не нужны транзакции, код проще и каждый кусок кода имеет одну ответственность.
Вообще тема очень обширная, планирую написать об этом отдельную статью.
PCI DSS
PCI DSS — это стандарт защиты карточных данных. Он применяется ко всем компаниям, которые хранят или обрабатывают данные карт. Чем больше платежей обрабатывает компания, тем выше требования. До одного миллиона платежей в год достаточно проводить ASV-сканирование и заполнять опросник самостоятельно, свыше — провайдеры начинают требовать от компании прохождения аудита с внешним сертифицированным аудитором.
У нас за прошлый год получилось приблизительно 35 миллионов платежей, не все из них успешные. И несмотря на то, что мы не храним и не обрабатываем данные карт в сыром виде, у нас есть формы ввода данных карт, а также мы пропускаем криптограммы (в случае CSE) через код своих систем, т.е. влияем на безопасность карточных данных, к нам применимы многие требования стандарта, и сейчас мы как раз проходим аудит, после которого станем сертифицированным L1 мерчантом.
Почему мы не делали раньше, ведь у нас сильно больше транзакций, чем по стандарту? Потому что осознавали риски, были готовы иметь с ними дело и от нас просто не требовали сертификации. Но это не значит, что мы не делали ничего, — мы делали самостоятельный аудит и периодически консультировались о том, как быть дальше. Интересный инсайт с консультаций: PCI DSS работает по договорённости, т.е. вполне возможна ситуация, что мерчант процессит больше платежей, чем определяет стандарт и не имеет сертификации, просто, в какой-то момент её станут требовать платёжные сервисы, от которых этого будут требовать банки, от которых, в свою очередь, будут требовать платёжные системы (Visa/MC/НСПК).
Будущее
То, о чём я говорю в этом разделе, — наиболее прожаренные идеи, которые наверняка будут сделаны. Но есть и немного моих фантазий.
Унифицировать CSE/CST
Ввод карты для нас подразумевает имплементацию CSE/CST при интеграции каждого нового провайдера. Так как формы ввода данных карт у нас нативные, то приходится писать код CSE/CST под три платформы — веб, iOS, Android, а затем их релизить. И если с вебом всё просто, то iOS и Android релизить уже сложнее.
Какие тут проблемы:
дублирование кода CSE/CST три раза;
добавление нового провайдера требует мобильной разработки и релиза приложений.
Мы пытались решить проблему дублирования кода в лоб с помощью Kotlin Multiplatform. Идея была в том, чтобы писать код один раз на Kotlin и компилировать под все нужные платформы, но в тот момент, когда мы пробовали технологию, она была в альфа-версии, работала лишь частично, а код получался очень развесистым — 5 МБ для веба и, кажется, 20 МБ под iOS. Но это решение всё равно требовало бы релиза приложений.
Позже мы придумали другое решение — унести форму ввода данных карты в веб и показывать её на всех платформах, в частности, в вебвью на мобильных платформах и в айфрейме в вебе. Таким образом, нам нужно будет писать код только один раз на JS, и будут не нужны релизы мобильных приложений. Есть опасения, что так как форма станет не нативной, то может немного пострадать UX, но я уверен, что это можно сделать без ущерба для UX. К тому же платежей со вводом данных карты (NewCard) значительно меньше, чем платежей по сохранённой карте (SavedCard) или SberPay. То есть эти изменения заденут небольшое число пользователей, но при этом мы освободим очень много времени для других задач и сможем сделать жизнь лучше всем.
Вот так будет выглядеть система в результате:
Совсем не останется красных проблемных блоков!
Сузить скоуп PCI DSS
Одна из проблем, которые создаёт CSE, — то, что в скоуп PCI DSS попадает платёжный шлюз целиком, то есть все его компоненты. Т.е. компоненты обрабатывают данные карт и к ним применяются повышенные требования безопасности и требуется проводить и проходить их аудит. А попадают они туда, потому что криптограммы — это хоть и зашифрованные, но всё равно данные карт.
У нас есть приблизительно следующий набор компонентов:
формы ввода данных карт,
плагины для интеграции с провайдерами,
ядро процессинга,
бэкенд админки,
фронтенд админки,
публичное платёжное API,
джобы и многое другое.
Понятное дело, что формы ввода данных карт исключить из скоупа не получится — они напрямую влияют на безопасность данных карт. То же самое и с плагинами для интеграции с провайдерами: мы не можем обойти момент передачи данных карты провайдеру. Что касается всех остальных компонентов, то им вообще не нужны данные карт, потому что всё, что они делают, — передают их провайдеру без какой-либо обработки. Это значит, что все эти компоненты можно исключить из скоупа PCI DSS и всё, что нужно для сделать, — сохранить данные карт в отдельном компоненте и работать в этих сервисах со ссылкой на данные карт.
То есть для решения этой задачи нужно добавить в платёжный шлюз ещё один компонент — токенизатор, который будет работать по принципу CST, по сути это и будет CST, только на нашей стороне. Этот токенизатор может сохранять настоящие данные карт, криптограммы карт, токены Apple Pay или что-то ещё — назовём это платёжным инструментом — и возвращать ID платёжного инструмента. Позже при необходимости можно достать эти данные из токенизатора, запросив их по ID.
Итого в скоупе PCI DSS будет всего три компонента:
Формы ввода данных карт.
Плагины для интеграций.
Токенизатор.
Всё остальное будет вне скоупа PCI DSS и может разрабатываться, как обычно.
СБП
У нас до сих пор нет, но мы очень хотим. Что нам мешает? То, что наши текущие платёжные провайдеры не поддерживают СБП. А если у вас появился вопрос, что нам мешает написать ещё интеграций, — всё то же самое, что и в случае с резервными провайдерами: в частности, сложности с бухгалтерией.
На мой взгляд, СБП имеет огромный потенциал, потому что это очень удобно для клиентов. Думаю, что если он у нас появится, то быстро станет основным способом оплаты в России.
Ждём, надеемся и верим, что какой-нибудь из наших провайдеров даст нам этот способ оплаты, и мы дадим его нашим клиентам!
Более крутая админка и отчёты
Наша текущая админка очень слабая, в ней буквально есть пара функций: настройка доступов к провайдеру, управление (вкл/выкл) способами оплаты. Это создаёт очень много проблем нам, нашим партнёрам и нашему саппорту:
-
сейчас приходится собирать отчёты в админках, которые дают нам платёжные провайдеры, приходится искать деньги в разных местах.
Для решения проблемы нам нужны точные отчёты в своей админке. Тогда можно строить намного более гибкие отчёты, прикрепляя данные о клиентах, заказах и вообще всё, что угодно;
-
для выполнения ручных операций вроде возвратов приходится пользоваться админками провайдеров — опять же, неудобно, приходится искать платежи в разных местах, а искать их сложно, потому что в системе провайдера нет данных о наших клиентах, заказах и прочее.
Для решения проблемы в своей админке нужно дать функциональность для поиска платежей и выполнения с ними операций;
-
приходится учить людей пользоваться разными админки, управлять доступами к этим админкам, а бывает, что провайдер и вовсе не имеет административного интерфейса, и приходится разбираться с проблемами с привлечением программистов — просто
геморройтойл.Для решения проблемы перестать пользоваться чужими админками совсем.
Надеюсь, что мы когда-нибудь сделаем это!
Автоматизация тестирования
Сейчас оплату мы тестируем в основном вручную — делаем смок-тесты и используем для этого песочницы платёжных провайдеров. Хочется же сделать несколько e2e-тестов, которые покрывали бы большую часть функционала и которые можно было бы запускать в билде и быть максимально уверенным в том, что всё работает.
После решения проблем с размазанной ответственностью и дублированием кода CSE/CST эту задачу, кажется, можно будет решить совсем просто, и автоматическими тестами можно будет покрыть почти всё.
Если решать проблему прямо сейчас, то тесты будут слишком хрупкими и создут больше проблем, чем принесут пользы как минимум потому, что будут включать в себя много движущихся частей, в частности BFF. Конечно, мы уже пробовали.
Чаевые
Сейчас функционал чаевых не относится к нашей команде, но они лежат где-то в области интернет-платежей, и я бы хотел забрать их в свою команду, но для этого нужно больше ресурсов, а их, как всегда, нет.
Хоть мы над этим и не работаем, но мысли возникают, и хочется ими поделиться.
Как сейчас |
Как хотелось бы |
---|---|
Чаевые доступны как пилот только в России |
Чаевые доступны во всём мире |
Плохой UX: функционал плохо интегрирован в систему и выглядит, как что-то сбоку, для оплаты чаевых рисуется сайт в вебвью, приходится ещё раз вводить данные карты, всё совершенно в другом стиле и неудобно. |
По UX оплата чаевых не отличается от оплаты заказа. |
Способы оплаты не зависят от нас. |
Хочется, чтобы способы оплаты были синхронизированы с основным приложением: сохранённые карты, SberPay и так далее. |
Закинуть чаевые можно, только когда прилетает пуш после доставки заказа и всё. |
Хочется, чтобы в истории заказов можно было это сделать в любой момент. |
Предложение пообщаться
Я думаю, что немного сомневаться в своих решениях и ресёрчить новые возможности просто необходимо на постоянной основе. У меня накопилась целая куча вопросов относительно интернет-платежей, ответы на которые я пока для себя не нашёл. Если вы можете ответить на них, проконсультировать, рассказать, как это работает, показать код — обязательно пишите мне в телегу @aurokk — буду рад пообщаться!
Итак, список вопросов:
В соответствии с PCI DSS хранить CVC нельзя. Как работают платежи с помощью сохранённой карты? Почему банкам не нужен CVC в этом случае?
Сейчас мы интегрируемся с PSP, а PSP интегрируются с банками и платёжными системами. «АПИшки» PSP — зоопарк, у всех всё по разному и редко сделано хорошо. А насколько API банков лучше? Есть ли какие-то банковские стандарты по разработке API и насколько банки их придерживаются?
Если интегрироваться с банками напрямую, то можно ли получить выигрыш в скорости разработки относительно интеграции с PSP?
Помимо интеграций с банками нужно ли будет ещё интегрироваться и с платёжными системами VISA/MC/НСПК?
Можно ли интегрироваться с банками и сохранить схему расчётов такую, как у нас сейчас, чтобы средства зачислялись напрямую на счета франчайзи, или нужно будет агрегировать средства на своих счетах и потом самим рассчитываться с франчайзи?
Можно ли получить выигрыш в комиссии эквайринга или всё наоборот? Кажется, что если в цепочке оплаты мерчант → PSP → банк-эквайер пропадает звено PSP, то комиссия должна быть ниже, но, с другой стороны, PSP — крупные клиенты банков и, возможно, банки дают им максимально низкую комиссию, настолько, что комиссии при интеграции напрямую могут оказаться выше. Как на самом деле?
Как искать и выбирать банки-эквайеры для интеграции напрямую?
Что ещё нужно знать? Возможно, моё представление всего процесса слишком наивно и для интеграции напрямую надо делать ещё миллион всяких вещей, менеджить терминалы, например.
Кстати, о терминалах: что это такое и кто их менеджит, PSP или банк-эквайер? Это то же самое, что MID, т.е. виртуальная сущность, или есть какой-то физический девайс, в который это мапится?
Насколько всё это отличается от страны к стране, от региона к региону, например, в странах Европы, в странах Азии?
Как можно сделать несколько транзакций внутри одного платежа без участия пользователя, например, один для оплаты заказа, один в благотворительность, один в чаевые?
Знаю, статья получилось огромной, информации очень много, многие темы раскрыты поверхностно — я так и задумывал. Хочется понять, какие аспекты наиболее интересны читателям и написать ещё статей с подробностями.
Возможно, я перепутал последовательность событий или цифры немного, но думаю, что это не так важно, как то, что вы дошли до конца! Ставьте лайки и пишите комменты, если вам понравилось, а если не понравилось, то не пишите.
Комментарии (9)
Zivaka
09.01.2023 21:02+1Вроде бы у Тинька была схема, когда оплаты автоматически могут распределяться по разным компаниям в зависимости от условий. Как раз для агентов по сути)
А с СБП есть еще неочевидный, но, на мой взгляд, крайне неудобный момент, что деньги сразу зачисляются на расчетный счет компании и там же сразу списывается комиссия. Потом статистику не очень удобно по платежам сводить, так как приход от эквайера не совпадает с фактическим оборотом по отчету.
villiwalla
12.01.2023 13:41По итогу, заказ с сайта 3 дня назад. Пиццу не привезли, не смогли определить адрес хоть он и был указан, «отменили» заказ, деньги не вернулись, результат после оплаты редиректит на апиху а не страницу заказа.
aurokk Автор
12.01.2023 15:55Да, кринж, починим :-)
Напиши, пожалуйста, в лс какой-нибудь какую-нибудь инфу о заказе, подтолкну возврат :-)
persona
Используется только номер карты, выставляются нужные флаги повторной авторизации и тогда CVC не нужен (вообще он и при первичной авторизации не особо обязателен, но высокий риск что банк отклонит авторизацию). Либо использовать родную токенизацию визы/мастеркард.
Очень странный вопрос для человека, который 7 лет в платежах - ISO 8583 (https://en.wikipedia.org/wiki/ISO_8583). Но у каждого экваера могут быть свои ньюансы использования.
Нужно получить Merchant ID.
aurokk Автор
Спасибо за ответы. Вообще, я про исо знаю, но пока не приходилось поработать с апи сделанным по исо. Отсюда и все эти вопросы :-)
persona
Протокол старый и страшный, но библиотеки под него должны быть чтобы не писать реализацию с нуля.
Докину ещё мыслей.
Это в первую очередь зависит от умения договариваться. Ну и конечно объемы. Можно найти выходящего на рынок игрока который демпингует.
Ещё важно, что во многих странах местный экваер будет давать больший процент апрувнутых авторизаций, тогда даже учитывая более высокий процент это может быть выгоднее. А где-то вообще картами почти не платят, а работают местные платежные системы.
Так что на глобальном рынке очень важно иметь гибкий "роутинг" настраиваемый бизнес-правилами и проводить много экспериментов.