Платёжный контур ломается не на «создали оплату», а на краевых случаях: вебхук пришёл дважды, пришёл с задержкой, прилетел не с того IP, а локальная БД уже живёт своей жизнью.
Как обычно собирают защиту:
1. **Создание платежа**
- стартуем с `capture=false`, чтобы не захватывать деньги раньше проверки;
- сразу фиксируем ожидаемые сумму, валюту и `metadata`.
2. **Проверка входящего вебхука**
- не верим событию на слово;
- проверяем IP;
- сначала пишем событие в **event log**, потом уже отдаём его в обработчик.
3. **Idempotency**
- capture подтверждаем только через стабильный `idempotency key`;
- повторный запрос не должен создавать второй захват или второй платёж.
4. **Валидация**
- успешный платёж считаем успешным только если совпали сумма, валюта и metadata;
- если статус в webhook и в системе расходится — доверяем не событию, а факту из платёжки.
5. **Аварийный путь**
- нужен ручной `confirm`, который умеет дочитать фактический статус из ЮKassa и синхронизировать локальную базу.
Смысл простой: вебхук — это не истина, а сигнал. Истина — это сверка, журнал и idempotency. Без этого платёжная система не работает надёжно, она просто выглядит работающей.
PR Lab
@PRLabPro