Снимок показывает живой запрос ровно в момент, когда сработал вебхук. Платёжный шлюз прислал статус PENDING. В коде для этого статуса нет обработчика. Проверка идемпотентности видит, что ключа ещё нет, помечает платёж как обработанный, но только после этого смотрит на состояние. А состояние оказалось PENDING — непонятное для системы. В итоге платёж не записан в DB, и при этом не выброшено ни одного исключения. Ошибка происходит молча: обработчик просто считает, что всё в порядке, хотя ничего не сохранил.
Разберём лог по шагам. Снимок из webhooks.ts:78, сделанный в 02:50:14 UTC, зафиксировал следующее. Поле status равно PENDING — это то, что шлюз отправил, а обработчика для такого значения нет. Поле duplicate равно null — значит, запрос виден впервые и свободно проходит через проверку на дубликаты. Дальше видно главное: вызов db.insert так и не происходит, то есть платёж не сохраняется в базу. А вот redis.set вызывается, хотя записи в DB не было. В результате idempotency key попадает в Redis, и платёж навсегда блокируется: повторные попытки будут отсекаться как дубликаты, потому что ключ уже там лежит. Никакой повторный вебхук с тем же ключом уже не сможет обработать платёж.
Идемпотентность нужна, чтобы защититься от повторных запросов: если шлюз пришлёт один и тот же вебхук дважды, второй раз код должен понять, что платёж уже обработан. Для этого и используется ключ в Redis. Но здесь ключ записывается до того, как проверен статус. Корень проблемы — порядок операций. Как только шлюз присылает PENDING, код считает, что платёж уже обработан, хотя на самом деле ничего не сохранил. Платёж остаётся в подвешенном состоянии: в DB его нет, ошибки нет, повторить нельзя. Платёж теряется тихо.