Буквально сегодня свет увидел новый API Яндекс.Кассы, разработанный программистами для программистов. Набор протоколов стал единообразным, логичным и простым в освоении. Но статья не об этом – я хочу рассказать, как и почему в один прекрасный момент API решено было переписать с нуля.
Под катом вы найдете немного страданий перфекциониста, боли разработчиков и наш подход к применению лучших паттернов проектирования в специфическом финансовом продукте.
Исторически в Яндекс.Деньгах протоколы в API появлялись по мере необходимости и разрабатывались разными подразделениями – не стали исключением и сервисы Яндекс.Кассы.
Как получилось так, что нужен новый API
Яндекс.Касса – универсальное решение для онлайн-платежей – родилась в 2013 году в стенах Яндекс.Денег. С её помощью компании могут принимать оплату всеми популярными способами: из кошельков в Яндекс.Деньгах, с банковских карт, со счетов мобильных номеров или наличными через специальные терминалы.
Тогда Касса преимущественно состояла из протокола приема платежей через платежную форму. Этот протокол позволял через платежную форму на сайте контрагента перенаправить плательщика на сайт Яндекс.Денег и провести платеж.
Когда контрагентам потребовалось осуществлять возвраты, получать списки возвратов и платежей, для всего этого появился еще один новый протокол. Он работал на базе PKCS#7 криптопакетов и XML, а порог технического вхождения для него оказался достаточно высок. В общем, с реализацией справлялись только те, кто по-настоящему в этом нуждался.
Кроме того, в 2014 году появились протокол выплат и протокол эквайринга банковских карт для крупных контрагентов, которые прошли PCI DSS аудит и могут хранить данные банковских карт на своей стороне.
Все эти протоколы выполняли свои узконаправленные задачи, но, к сожалению, были разными с точки зрения интеграции. Если мерчант хотел принимать оплату всеми возможными способами, ему нужно было реализовать целых четыре протокола, которые сильно отличались друг от друга.
Именно поэтому мы поставили перед собой задачу значительно снизить технический порог входа в Яндекс.Кассу и сократить время интеграции с интернет-магазинами. Кроме того, новый API должен был не только учитывать лучшие мировые практики в отрасли, но и быть максимально удобным для разработчиков наших контрагентов.
Так появился новый API Яндекс.Кассы, который строился на трех основных принципах: единая модель данных для всего взаимодействия, однозначность признаков и статусов платежей, асинхронность при взаимодействии магазина с Яндекс.Кассой. Сейчас новый API покрывает все протоколы прошлой версии, кроме выплат – они появятся чуть позже.
Единая модель данных
При проектировании API мы использовали объектно-ориентированный подход на основные ценности REST-like протоколов. При этом старались использовать знакомые контрагентам и разработчикам сущности, которые уже существуют в их системах и процессах: платежи, возвраты, сохраненные способы оплаты (они же – привязки) и другие. Эту концепцию принято называть проблемно-ориентированным проектированием.
Структура объекта платежа неизменна от запроса к запросу и позволяет получать необходимую информацию в любой момент без необходимости хранить все это на своих серверах. Что особенно важно, это позволяет одинаково интерпретировать данные объекта в любое время. Создаете ли вы объект платежа (Payment) через POST, получаете его состояние через GET, подтверждаете или отменяете его – во всех ситуациях вы работаете с одним и тем же объектом Payment.
{
"id": "2171eeff-000f-50be-b000-02e31251204a",
"status": "pending",
"paid": false,
"amount": {
"value": "2.00",
"currency": "RUB"
},
"confirmation": {
"type": "redirect",
"confirmation_url": "https://money.yandex.ru/payments/kassa/confirmation?orderId=2171eeff-000f-50be-b000-02e31251204a"
},
"created_at": "2017-10-12T21:14:39.577Z",
"payment_method": {
"type": "bank_card",
"id": "2171eeff-000f-50be-b000-02e31251204a",
"saved": false,
"card": {
"last4": "7918",
"expiry_year": "2017",
"expiry_month": "07",
"card_type": "MasterCard"
}
}
}
Пример объекта платежа (Payment).
Из соображений единообразия все объекты платежа, возврата, а также платежные методы живут по одним законам и обладают похожими признаками и структурой.
Паттерны проектирования и единая дизайн-система позволяют сделать продукт предсказуемым и снижают порог входа. То чувство, когда вы уже освоили Платежи, а теперь пробуете реализовать Возвраты – и все работает без повторного изучения документации.
Однозначность признаков и статусов платежей
У платежных систем есть одна особенность, которая серьезно затрудняет реализацию протоколов по REST-like принципу: все сущности в рамках API обладают своим жизненным циклом. Он отличается от метода к методу и влияет на значения и структуру полей.
Мы стремились сделать количество статусов платежей и возвратов минимальным, чтобы клиенты Кассы могли реализовывать необходимую бизнес-логику только тогда, когда это действительно нужно. Для общего понимания разберем подробнее жизненный цикл платежа. Он состоит из трех состояний, как показано на рисунке:
Состояния платежа по новому жизненному циклу: waiting_for_capture присутствует не всегда – есть режим проведения платежа с автоматическим capture.
На стадии pending и waiting_for_capture платеж может быть отменен магазином или нами и тогда перейдет в статус canceled.
{
"id": "2171f067-000f-50be-b000-01cc0f0843c3",
"status": "waiting_for_capture",
"paid": true,
"amount": {
"value": "1000.00",
"currency": "RUB"
},
"created_at": "2017-10-12T21:21:08.594Z",
"payment_method": {
"type": "bank_card",
"id": "2171eeff-000f-50be-b000-02e31251204a",
"saved": false,
"card": {
"last4": "7918",
"expiry_year": "2017",
"expiry_month": "07",
"card_type": "MasterCard"
} },
"receipt_registration": "succeeded"
}
Ответ сервера при прохождении стадии waiting_for_capture.
Переход к последнему состоянию Succeeded означает, что платеж прошел и готов к перечислению на расчетный счет магазина. Можно смело отгружать товар.
Также мы даем контрагентам набор признаков, по которым можно получить дополнительную информацию: например, о необходимости подтверждения платежа пользователем, о внесении им денег или регистрации чека в облачной кассе для выполнения требований 54-ФЗ.
Клиент Яндекс.Кассы всегда может получить полную информацию о платеже по его идентификатору и принять решение о дальнейших действиях: показать страницу успеха, рассказать покупателю о проведении фискализации или выдать товар. При этом создание и проведение платежа всегда носит линейный характер и обладает фиксированным набором шагов:
- Создание платежа.
- Получение подтверждения пользователя.
- Регистрация чека в онлайн-кассе.
- Подтверждение готовности принять платеж.
- Получение денег на расчетный счет уже на следующий рабочий день.
Шаги 2-4 опциональны, но последовательность этих действий всегда неизменна.
Чтобы ознакомиться с реальными примерами, посмотрите в инструкциях раздел по проведению платежа «Быстрый старт».
Асинхронность при взаимодействии с Кассой
Начну издалека. API Яндекс.Кассы связывает магазин, покупателя, партнеров и эмитентов банковских карт (банк, который выпустил вашу карту), то есть в каждом платеже задействовано множество участников. Иногда ответ всех участников процесса занимает достаточно продолжительное время.
Если бы взаимодействие было синхронным, компаниям приходилось бы все это время держать соединение открытым, что требует больше ресурсов. И именно поэтому мы сделали API асинхронным.
Чтобы поддерживать асинхронность и защититься от задвоения платежей, мы предлагаем использовать ключ идемпотентности. Если Касса по какой-то причине не успеет провести платеж за отведенное время, клиент API получит в ответе HTTP-код 202 (Request Accepted) с просьбой повторить запрос с тем же ключом чуть позже. Даже при множественных запросах с одним и тем же ключом и одинаковым телом запроса операция всегда будет совершена только один раз.
curl https://payment.yandex.net/api/v3/payments \
-X POST \
-u <Идентификатор магазина>:<Секретный ключ> \
-H 'Idempotence-Key: <Ключ идемпотентности>' \
-H 'Content-Type: application/json' \
-d '{
"amount": {
"value": "2.00",
"currency": "RUB"
},
"payment_method_data": {
"type": "bank_card"
},
"confirmation": {
"type": "redirect",
"return_url": "https://www.merchant-website.com/return_url"
}
}'
Пример запроса с ключом идемпотентности.
При этом достаточно выбрать генератор случайных чисел с минимально возможной коллизией (рекомендуем использовать v4 UUID). Мы запоминаем каждый из идентификаторов на 24 часа, после чего можно воспользоваться им повторно. При этом вам не нужно хранить ключ идемпотентности вечно – достаточно получить постоянный идентификатор объекта Платежа или Возврата, сгенерированного Кассой. Впоследствии вся работа с объектом осуществляется через этот идентификатор.
Некоторые процессы оплаты в Яндекс.Кассе асинхронны по своей природе. Например, при подтверждении оплаты через SMS в Сбербанке Онлайн контрагент должен передать нам номер телефона своего покупателя, а затем ожидать подтверждения платежа пользователем, на которое может уйти от нескольких минут до часа.
Поэтому Касса передает контрагенту информацию о факте отправки SMS-сообщения и ожидает подтверждения пользователем. В это время клиент Кассы может периодически повторять запросы о статусе платежа либо подписаться на уведомления от сервера, которые направляются на указанный в личном кабинете Кассы URL при смене статуса платежа. В обоих случаях контрагент получает полный объект Платежа.
Все эти решения позволяют компаниям не подстраивать свои бизнес-процессы под Яндекс.Кассу, а гибко встраивать наш API в любые платежные сценарии.
Как мы расширяем API, или Сначала попробуем сами
При создании предыдущих протоколов мы столкнулись с проблемой расхождения документации и реализации, потому что исходники документации существовали отдельно. В новом API с этим тоже нужно было покончить, поэтому для сборки документов решили использовать Swagger. Сейчас на рынке есть множество решений all-in-one, которые позволяют импортировать Swagger-спецификации, но идеальных с точки зрения кастомизации и информационного дизайна не нашлось.
Поэтому документы конвертировали с использованием формата Markdown, а в качестве инструмента для отображения выбрали Slate. Для прототипа каждой новой функции теперь используется спецификация в формате Swagger и сценарии, которые описывают последовательности действий и возможные исходы.
Классический сценарий добавления новых функций в API выглядит следующим образом: менеджер продукта создает Pull Request в репозиторий с документацией, сопровождая его ссылкой на измененный пользовательский сценарий. Pull Request проходит ревью как аналитиков, так и команды разработчиков API.
PaymentMethodDataBankCard:
description: Данные для оплаты банковской картой
allOf:
- $ref: '#/definitions/PaymentMethodData'
- type: object
properties:
type:
description: Тип объекта.
type: string
enum:
- bank_card
card:
description: Данные банковской карты (необходимы, если вы собираете данные карты пользователей на своей стороне).
type: object
properties:
number:
description: Номер банковской карты
type: string
pattern: "[0-9]{13,19}"
example: '4444444444444448'
expiry_year:
description: Срок действия, год, YY
type: string
pattern: "[0-9]{4}"
example: '2017'
expiry_month:
description: Срок действия, месяц, MM
type: string
pattern: "[0-9]{2}"
example: '10'
csc:
description: Код CVC2 или CVV2, 3 или 4 символа, печатается на обратной стороне карты
type: string
pattern: "[0-9]{3,4}"
example: '012'
cardholder:
description: Имя владельца карты
type: string
pattern: "[a-zA-Z]{0,26}"
example: 'John Smith'
required:
- number
- expiry_year
- expiry_month
- csc
required:
- type
После того как все договорились о финальной версии спецификации и Pull Request получил все необходимые согласования, начинается реализация. Параллельно от ветки спецификации отводится еще одна, в которой копирайтеры исправляют тексты и объединяют их отдельным Pull Request.
Как только новая функция реализована разработчиком, она тестируется по этой спецификации и далее отправляется на продакшен.
Dogfooding и мониторинг в перерывах
Перед тем как отдавать новую функцию пользователям, мы обязательно устраиваем «dogfooding», то есть сами пробуем наш продукт. Для этого используется специально разработанный демо-магазин, где можно купить виртуальную Капибару и реализовать другие сценарии клиентов Кассы. Вспомогательная цель его использования – снизить порог вхождения для сотрудников-новичков и показать платежные сценарии любому желающему внутри компании.
Как только мы вычитаем документацию, протестируем новые функции и убедимся в их простоте и понятности – выполняется Merge Pull Request Swagger-спецификации в master-ветку, чтобы она автоматически собралась в публичную документацию и попала на сайт.
После выпуска новой функции в мир наступает бесконечно увлекательный момент поддержки и мониторинга. Как и другие подразделения Яндекс.Денег, мы используем Grafana. Наблюдения проводятся с помощью множества метрик, поэтому приведу тут только основные: запросы к API, отправленные уведомления, успешность прохождения платежей и их подтверждение (capture), частота возникновения определенных ошибок.
Вместе с запуском нового API появился обновленный портал документации и SDK на PHP. Начался и постепенный переход наших CMS-модулей на новый протокол – сейчас регистрация на новый протокол доступна для OpenCart 1.5 и WooComerce. К ноябрю 2017 к ним присоединятся Y.CMS для Opencart 2, Prestashop, Webasyst Shop-Script, Drupal (модуль для Commerce и Ubercart), JoomShopping и SimplaCMS.
Мы также запускаем библиотеку YandexCheckout.js, которая позволяет создавать формы для приема карточных платежей любых форм и размеров. Первыми такую библиотеку 6 лет назад предложил Stripe. С тех пор нечто похожее сделали другие платежные агрегаторы для решения проблемы оплаты картами прямо на сайте интенет-магазина. Мы учли лучшие наработки наших западных коллег и выпустили YandexCheckout.js. Теперь компаниям не придется проходить полный и дорогой аудит PCI DSS, чтобы принимать платеж на своей стороне — достаточно заполнить опросник SAQ D и пройти ASV сканирование (с чем мы тоже можем помочь). Сейчас эта опция доступна в индивидуальном порядке, но открытие ее для всех как раз в планах на будущее.
И на десерт: вместе с новым API мы запускаем полноценную песочницу. Прямо в вашем личном кабинете теперь можно обкатывать основные платежные сценарии с использованием тестовых карт и кошельков.
Нет, мы не предлагаем все сломать и сделать заново
Многих беспокоит, что будет со старым протоколом и как скоро мы предложим всем перейти на новый. Могу вас успокоить – работу со старым протоколом принудительно ограничивать не планируем. То есть для всех, у кого процесс налажен и кого он полностью устраивает, ничего не изменится. Но если вы все равно хотите попробовать новые возможности – наши менеджеры могут создать для вас новый магазин с обновленным API.
Если же вы сами разрабатываете новую площадку или давно ждали возможностей нового API – при регистрации будет использован уже обновленный API Яндекс.Кассы. В начале 2018 года также планируется публичный запуск возможности перевода старых магазинов на новый протокол.
Напоследок две самые важные ссылки:
Если у вас остались вопросы или опасения относительно нового API – приглашаю в комментарии к статье.