Прямой ответ
Интеграция платёжного шлюза сводится к четырём элементам: создание платежа на стороне сервиса, передача пользователя на stronę подтверждения, получение результата и фиксация состояния в собственной базе.
Базовая схема взаимодействия:
Сервис ──1. создание платежа──▶ Провайдер
Сервис ◀──2. идентификатор платежа──
Пользователь ──3. подтверждение──▶ Провайдер
Провайдер ──4. уведомление──▶ Сервис
Сервис ──5. проверка статуса──▶ Провайдер
Шаг пятый обязателен: уведомление приходит по сети и может быть подделано либо не доставлено, поэтому состояние платежа проверяется запросом к провайдеру.
import httpx
r = httpx.post(
"",
auth=(SHOP_ID, SECRET_KEY),
json={"amount": {"value": "1500.00", "currency": "RUB"},
"reference": order_id, "return_url": RETURN_URL},
timeout=10.0,
)
r.raise_for_status()
payment_id = r.json()["id"]
Собственная таблица платежей хранит состояние независимо от провайдера:
CREATE TABLE payments (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL,
provider_id text,
status text NOT NULL,
amount_minor bigint NOT NULL,
currency char(3) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
Сумма хранится в минимальных единицах валюты — копейках, — поскольку десятичные дроби в двоичном представлении дают ошибку округления. О жизненном цикле статусов — в статье про состояния платежа.
Что определяет надёжность интеграции
Различия между провайдерами касаются формата запросов, но надёжность определяется четырьмя решениями, не зависящими от конкретного API.
Один источник состояния. Состояние платежа хранится в собственной базе и обновляется по проверенному ответу провайдера. Уведомление без проверки не меняет состояние, поскольку источник сообщения не подтверждён.
Идемпотентность создания платежа. Повторный запрос с тем же ключом возвращает существующий платёж, а не создаёт новый; о механизме — в статье про ключ идемпотентности.
Разделение синхронного и асинхронного результата. Ответ на создание платежа означает принятие, а не завершение оплаты. Завершение приходит уведомлением либо проверяется опросом.
Обработка отказа провайдера. Таймаут не означает отказ платежа: операция могла выполниться после разрыва соединения. В таком случае состояние остаётся неопределённым и уточняется запросом.
Отдельная задача — согласованность записи. Запись в собственную базу и запрос к провайдеру не образуют транзакцию, поэтому сбой между ними оставляет расхождение. Устраняется исходящей очередью либо периодической сверкой.
Ключи доступа хранятся вне кода и вне репозитория; о хранении секретов — в статье про управление секретами.
Что проверить перед выводом интеграции в продуктив
Переход от тестового контура к продуктивному сопровождается четырьмя проверками, пропуск которых приводит к сбоям уже на реальных платежах.
Замена ключей и адресов. Тестовые ключи не работают в продуктовом контуре, а адрес API отличается. Ключи хранятся в системе секретов, поэтому замена выполняется без изменения кода.
Проверка всех веток обработки. Тестовый контур позволяет воспроизвести отказ, таймаут и отмену. Каждая ветка проверяется до перехода, поскольку в продуктиве воспроизвести отказ по запросу невозможно.
Настройка уведомлений у провайдера. Адрес обработчика уведомлений указывается в настройках провайдера отдельно для каждого контура. Отсутствие настройки означает, что уведомления не поступают и состояние не обновляется.
Согласование статусов. Таблица сопоставления статусов провайдера с внутренними статусами проверяется на реальном наборе операций, поскольку тестовый контур может возвращать не весь набор.
Отдельный элемент — порядок отката. Интеграция выводится за переключателем, поэтому возврат к предыдущему способу оплаты выполняется изменением настройки, а не развёртыванием. О постепенном включении — в статье про флаги функций.
Практика: внедрение платёжной интеграции
1. Начинайте с тестового контура провайдера. Тестовые ключи и тестовые карты позволяют проверить все ветки обработки без реальных списаний.
2. Фиксируйте состояние в собственной базе. Таблица платежей с историей изменений позволяет разбирать расхождения без обращения к панели провайдера.
3. Храните суммы в минимальных единицах. Копейки как целое число исключают ошибку округления при сложении и сравнении.
4. Проверяйте подпись уведомлений. Уведомление без проверки подписи принимается от любого отправителя, что позволяет подделать успешную оплату.
5. Устанавливайте таймаут на запросы к провайдеру. Запрос без таймаута удерживает поток приложения при замедлении провайдера.
6. Повторяйте запросы с осторожностью. Повтор создания платежа без ключа идемпотентности приводит к двойному списанию.
7. Логируйте идентификаторы операций. Идентификатор платежа у провайдера и внутренний идентификатор связывают запись в журнале с записью в базе.
8. Проверяйте состояние при возврате пользователя. Возврат на страницу результата не означает успех: пользователь мог закрыть страницу подтверждения.
Рекомендации:
- Не отправляйте данные карты через свой сервер без необходимости. Хостинг платёжной формы у провайдера снижает объём требований безопасности; о требованиях — в статье про PCI DSS.
- Разделяйте суммы и валюту. Хранение суммы без валюты приводит к ошибкам при работе с несколькими валютами.
- Учитывайте задержку уведомлений. Уведомление может прийти через минуты после оплаты, поэтому интерфейс показывает промежуточное состояние.
- Проверяйте обработку частичного отказа. Отказ после списания требует отдельного сценария, а не общего сообщения об ошибке.
- Документируйте сопоставление статусов. Статусы провайдера отображаются во внутренние статусы по явной таблице, а не по совпадению названий.
Практический сценарий: двойное списание при повторной отправке формы
Контекст: пользователь столкнулся с двумя списаниями за один заказ.
- Диагностика. В журнале провайдера оказались два платежа с одинаковой суммой и разными идентификаторами, созданные с интервалом в несколько секунд. В собственной базе присутствовала одна запись платежа.
- Причина. Форма оплаты отправлялась повторно при отсутствии ответа, а запрос создания платежа не содержал ключа идемпотентности. Первый запрос выполнился на стороне провайдера, но ответ не дошёл до сервиса из-за таймаута, поэтому повтор создал второй платёж.
- Действия. (а) Добавили ключ идемпотентности, формируемый из идентификатора заказа; (б) сохранили идентификатор провайдера в базе до ожидания ответа, чтобы повторный запрос находил существующий платёж; (в) при таймауте перевели платёж в неопределённое состояние с последующим уточнением запросом, а не повторным созданием; (г) выполнили возврат по второму платежу; (д) добавили проверку на дубли платежей по заказу в периодическую сверку.
- Результат. Повторная отправка формы возвращает существующий платёж, а расхождение обнаруживается сверкой.
Вывод: запрос создания платежа должен быть идемпотентным, поскольку таймаут не означает отказ операции. Ключ идемпотентности из идентификатора заказа исключает двойное списание, а неопределённое состояние при таймауте требует уточнения запросом, а не повторного создания.
FAQ
Нужно ли хранить данные карты в своей базе? Нет, если используется хостинг формы у провайдера либо токенизация. Хранение данных карты расширяет объём требований безопасности.
Что делать при таймауте запроса к провайдеру? Перевести платёж в неопределённое состояние и уточнить статус запросом. Повторное создание платежа без ключа идемпотентности приводит к двойному списанию.
Можно ли доверять уведомлению об успешной оплате? Нет без проверки подписи и последующего запроса состояния. Уведомление — сигнал к проверке, а не источник истины.
Как проверять интеграцию без реальных списаний? Тестовым контуром провайдера с тестовыми ключами и картами, покрывая успешный сценарий, отказ и таймаут.
Источники: Stripe API, PayPal REST API, PCI Security Standards Council — ссылки в шапке.