После таймаута приложение не знает, произошла ли оплата. Ответ мог потеряться уже после списания. В этой ситуации отключённая кнопка «Оплатить» защищает только от лишнего нажатия: запрос всё равно могут повторить приложение, посредник или сотрудник поддержки.
Идемпотентность означает, что повтор той же операции не создаёт дополнительного предусмотренного бизнес-эффекта. Она не обещает успеха и не требует побайтно одинакового ответа при каждой попытке. Конкретное поведение задаёт контракт API.
Ключ относится к намерению, а не к нажатию
Для одной платёжной попытки нужен устойчивый идентификатор. Приложение сохраняет его до отправки и использует при восстановлении связи. Генерировать новый ключ после каждого таймаута — значит сообщать серверу о новой операции.
Одинаковая сумма сама по себе не признак дубля: человек вправе сделать две разные покупки. Напротив, прежний ключ с другой суммой должен вызвать проверку несовпадения. Область ключа связывают с клиентом и операцией; знание чужого ключа не должно давать доступ к результату. Эти различия разобраны в Amazon Builders’ Library.
Проверить наличие записи недостаточно
Два одновременных запроса способны оба увидеть, что ключ ещё не зарегистрирован. Нужна атомарная регистрация и согласованность между отметкой о выполнении и локальным изменением данных. Для собственного хранилища это можно обеспечить транзакцией и уникальным ограничением.
Внешнее списание не включается в такую транзакцию автоматически. После обрыва связи сохраняют состояние «результат неизвестен», проверяют исходную операцию у провайдера и повторяют её только по его правилам. Заводить новый платёж лишь потому, что предыдущий ответил таймаутом, опасно.
Срок хранения ключей и обработку ошибок также проверяют в документации конкретного API. Например, Stripe описывает различия своих версий API; переносить поведение одной реализации на любую другую нельзя.
Три проверки перед запуском
- Ответ потерялся после успешного списания. Повтор возвращает состояние исходной операции, а не создаёт новую.
- Две копии запроса пришли одновременно. Обе не выполняют платёж независимо.
- Старый запрос пришёл после удаления ключа. Отдельная бизнес-проверка не позволяет незаметно повторить уже завершённый платёж.
Для каждого случая нужно смотреть и журнал операций, и результат у провайдера. Одного сообщения «дубликат отклонён» недостаточно. Если исход нельзя установить автоматически, поддержке нужны идентификатор, ответственный за сверку и явный запрет создавать заменяющий платёж без выяснения результата.