В фокусе статьи
Исходная задача: один контракт между заказом и провайдером
Backend не должен передавать провайдеру произвольную сумму из браузера или менять заказ без проверяемого payment state.
В кейсе сервер сначала валидирует заказ, сумму, валюту и доступность действия, затем создает payment attempt с внутренним id и idempotency key. Только после этого формируется безопасный redirect или checkout URL.
Внутренний order id, payment attempt id и provider payment id сохраняются вместе. Это дает возможность повторить запрос безопасно, а поддержку и учет не заставляет угадывать связь между операциями.
- не принимать финальную сумму платежа как доверенный frontend-параметр;
- разделять order, payment attempt и refund как разные сущности;
- фиксировать валюту и правила округления на backend;
- отвечать на повторный create-payment тем же результатом, если ключ идемпотентности совпадает.
Решение: конечный автомат и идемпотентные webhook-обработчики
Webhook может прийти несколько раз, позже return URL или в другом порядке, поэтому бизнес-статус меняется только по допустимым переходам.
Backend проверяет подпись, сумму, валюту, correlation id и источник события. Затем он определяет, допускает ли текущий payment state полученный переход. Например, paid нельзя заменить на failed поздним callback, а повторный paid не должен запускать вторую выдачу заказа.
Laravel job или Django worker может выполнять вторичные действия асинхронно: уведомления, CRM sync, фискальный запрос или выдачу доступа. Критичный webhook-ответ остается быстрым, а сбой вторичного сервиса не теряет подтвержденную оплату.
- сохранять raw event и нормализованный результат проверки;
- отделять прием webhook от тяжелых фоновых действий;
- вводить retry policy и ручной review для неразрешимых расхождений;
- не выполнять необратимое действие до подтвержденного paid.
Контроль после запуска: возвраты, наблюдаемость и сверка
У команды должен быть журнал для платежных попыток, возвратов, retries и расхождений с отчетом провайдера.
Возврат начинается с проверки исходного платежа, доступной суммы и роли оператора. После отправки запроса backend записывает отдельный refund record, а финальный результат подтверждает тем же безопасным механизмом статусов.
Для эксплуатации важны структурированные логи, метрики очередей и ежедневная сверка с отчетом провайдера. Они позволяют увидеть не только ошибку API, но и платеж, который прошел у провайдера, но не дошел до внутренней системы.
- не логировать карточные данные или секреты;
- выдавать оператору поиск по order id, payment id и correlation id;
- разделять инициированный и подтвержденный возврат;
- отдельно отслеживать webhook retries и задачи ручного review.
FAQ
Нужен ли отдельный payment attempt, если уже есть заказ?
Да. Заказ и попытка оплаты имеют разный жизненный цикл: один заказ может содержать несколько попыток, отмену или возврат.
Как обрабатывать повторный webhook?
Проверить подпись и идентификатор события, сохранить его для аудита, затем применить только допустимый идемпотентный переход состояния.
Можно ли выполнять CRM sync прямо внутри webhook?
Лучше принять и проверить событие быстро, а CRM sync запускать через устойчивую очередь с retry и журналом ошибок.