Бэкенд, с которым работает плагин
English version: backend.md
В разделе сервер мерчанта описаны готовые маршруты, сессии, идемпотентность и проверка уведомлений в примере сервера. Здесь — общая идея.
Плагин в браузере обращается к API вашего сервера, а ваш сервер — к платёжному провайдеру. В примере сервера есть маршруты для Stripe, Adyen и PayPal на официальных SDK для песочницы, плюс отдельный локальный симулятор. Точные маршруты каждого провайдера описаны в README его пакета.
браузер: плагин → ваш API → API провайдераЭта страница — про правую часть стрелки: что должен делать ваш сервер. Она короткая, потому что контракт плагина короткий.
Почему не обращаться к провайдеру из браузера
Три причины, начиная с самой срочной:
- Секретный ключ.
sk_...у Stripe,x-api-keyу Adyen,userName/passwordу банка. Всё, что может отправить браузер, может прочитать и отправить кто угодно. - Настоящий результат. Браузер сообщает, где он побывал, а не что произошло.
?status=successв адресе возврата может набрать кто угодно. - CORS. Большинство API провайдеров вообще не отвечают браузерам, и это сделано намеренно.
Маршруты
Плагину нужен один и тот же небольшой набор маршрутов, как бы ни был устроен провайдер внутри.
| Метод плагина | Ваш маршрут | Что делает |
|---|---|---|
createIntent | POST /payments | начинает платёж, возвращает его id |
confirm | POST /payments/:id/confirm | принимает платёжный инструмент, возвращает результат |
resume | POST /payments/:id/resume | принимает то, что вернулось после шага, возвращает результат |
getIntent | GET /payments/:id | текущее состояние; это главный источник правды |
cancel | POST /payments/:id/cancel | необязательный |
Самый важный — getIntent. Движок обращается к нему, когда ответ не окончательный, его вызывает опрос статуса, и на него же попадает покупатель, который перезагрузил страницу.
Что нельзя возвращать
Возвращайте только то, что нужно показать на странице оплаты:
- сумму, валюту и статус;
- сообщение банка об отказе — его собственными словами;
- следующий шаг, если провайдер его попросил.
Никогда не возвращайте необработанный ответ провайдера, свои API-ключи, номер карты и вообще ничего, чего вы не хотите видеть в памяти браузера. Номер карты проверяется тестами соответствия для каждого плагина в этом репозитории: они платят картой, а потом ищут её номер во всём возвращённом объекте.
Пример: карточный процессинг
Express, один провайдер, ничего лишнего. Плагин вызывает эти маршруты; в браузере не остаётся никаких секретов.
import express from 'express'
import Stripe from 'stripe'
const app = express()
app.use(express.json())
// Ключ живёт здесь и больше нигде.
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const toPayment = (intent) => ({
id: intent.id,
amount: intent.amount,
currency: intent.currency.toUpperCase(),
status: toStatus(intent.status),
// Действие, которое плагин превратит в PaymentAction.
nextAction: intent.next_action ?? null,
error: intent.last_payment_error
? { code: intent.last_payment_error.code, message: intent.last_payment_error.message }
: null,
})
app.post('/payments', async (req, res) => {
// Цена берётся из вашего каталога, а не из браузера. Клиент, который может назвать сумму
// сам, рано или поздно назовёт 1.
const plan = await plans.byId(req.body.planId)
const intent = await stripe.paymentIntents.create(
{ amount: plan.amount, currency: plan.currency, automatic_payment_methods: { enabled: true } },
// Пробрасывается от плагина: двойной клик не должен списать дважды.
{ idempotencyKey: req.get('Idempotency-Key') },
)
res.status(201).json(toPayment(intent))
})
app.post('/payments/:id/confirm', async (req, res) => {
const intent = await stripe.paymentIntents.confirm(
req.params.id,
{ payment_method_data: toPaymentMethod(req.body.instrument), return_url: req.body.returnUrl },
{ idempotencyKey: req.get('Idempotency-Key') },
)
res.json(toPayment(intent))
})
app.post('/payments/:id/resume', async (req, res) => {
// Браузер вернулся из банка. Его слова — подсказка; ответ вот здесь.
const intent = await stripe.paymentIntents.retrieve(req.params.id)
res.json(toPayment(intent))
})
app.get('/payments/:id', async (req, res) => {
res.json(toPayment(await stripe.paymentIntents.retrieve(req.params.id)))
})Обратите внимание, чего resume не делает: он не читает req.body.evidence.params.status. Данные из браузера говорят, какой платёж вернулся. А что с ним случилось, говорит провайдер.
Важные правила
Сумма берётся из вашего каталога. Плагин присылает planId, а не цену.
Передавайте ключ идемпотентности дальше. Плагин создаёт один ключ на попытку и отправляет его со всеми запросами этой попытки. Передайте его провайдеру. Именно это делает повтор безопасным.
Проверяйте, кто спрашивает. Эти маршруты двигают деньги. Им нужна та же проверка сессии или токена, что и остальному API, а платёж должен принадлежать тому, кто про него спрашивает.
Решают вебхуки, а странице сообщает опрос. Когда отложенный платёж завершается, провайдер вызывает ваш вебхук. Это событие вы считаете достоверным и сохраняете. Страница оплаты узнаёт об этом потому, что getIntent начинает возвращать новый статус. Ни плагину, ни движку не нужно знать, что вебхуки существуют.
Возвращайте сообщение банка. «На карте недостаточно средств» — вот что нужно услышать покупателю. «Платёж не прошёл» отправит его в вашу поддержку, а не в его банк.
Для страницы оплаты, полей провайдера, кошелька и перевода
Те же маршруты, плюс по одному дополнению:
- Страница оплаты на сайте провайдера — зарегистрируйте заказ и верните адрес страницы с подписанными параметрами. Когда покупатель вернётся, не смотрите на параметры в адресе, а заново прочитайте заказ у провайдера.
- Поля провайдера — обменяйте токен на списание. Токен приходит из фрейма провайдера, а в деньги его превращает ваш сервер.
- Кошелёк — данные кошелька отправляет провайдеру ваш сервер. Для Apple Pay нужен ещё маршрут проверки мерчанта (merchant validation), который по своей природе работает на сервере.
- Мгновенный перевод — запросите у провайдера код, верните его и дайте движку опрашивать
getIntent, пока ваш вебхук не отметит платёж как оплаченный.
Как сделано в этом репозитории
У демо нет сервера. Поддельный бэкенд на MSW сразу отвечает в формате провайдера, поэтому плагины можно проверить от начала до конца без сервера. Везде, где настоящая интеграция обращалась бы к вашему API, демо обращается к поддельному. Код плагина в обоих случаях одинаковый, поэтому он всё равно полезен как пример.