Входящее уведомление и повтор доставки
У платежа меняется состояние без нового действия браузера. Провайдер уведомляет сервис через webhook, но сообщение может повториться либо прийти после других сообщений. Кроме того, публичный HTTP-адрес может вызвать посторонний клиент. Поэтому обработчик не должен выдавать доступ просто по переданному status: succeeded.
В локальном «Пространстве» уведомление содержит идентификатор fixture и eventId. Сервис находит сохранённую связь с компанией, затем запрашивает актуальный снимок через адаптер и применяет его знакомым методом. Настоящая интеграция потребовала бы отдельной проверки подлинности по правилам выбранного провайдера. Код расположен в BillingEndpoints.cs архива; уведомления при подготовке не доставлялись.
Входящая область определяется сохранённой связью
У webhook нет пользовательского членства и выбранной вкладки. Он не должен принимать organizationId из произвольного JSON и открывать для него системный сеанс. Область находится по реестру provider_routes, который создаётся после связывания providerId с локальным paymentId.
Этот реестр содержит только технический маршрут: providerId, компания и платеж. Прикладные финансовые строки остаются под RLS. Такой минимальный общий индекс отличается от общей выборки всех платежей с их суммами и именами клиентов. Доступ к реестру нужен серверу, но он не публикуется в API кабинета.
Если связь пока не записана, обработчик не может достоверно применить объект. Возможен промежуток, когда провайдер уже создал платеж, а локальное связывание не завершилось. Сервис сохраняет возможность повторного получения либо отвечает временным отказом согласно своему контракту. Сверка creation key позже восстановит связь и обработку.
Ниже приведено целое тело локального сигнала. Оно не является форматом настоящего уведомления ЮKassa. Поле state отсутствует намеренно: актуальное состояние берётся из адаптера.
{"eventId":"local-event-0001","providerId":"fixture-payment-1"}
Проверка источника зависит от провайдера
Для loopback fixture-адреса пример проверяет локальный доступ и ограничивает среду Development. Это учебный способ доставки, а не промышленная подпись. Нельзя написать в real-адаптере проверку выдуманного заголовка подписи и считать уведомления защищёнными.
Первичная документация ЮKassa о входящих уведомлениях описывает подтверждение получения и способы проверки актуального объекта. При переносе нужно следовать её текущему контракту, включая проверку статуса запросом к API. Наш интерфейс GetAsync специально даёт место для такого авторитетного чтения.
Доверие к сетевому адресу также требует правильной настройки proxy. Заголовок X-Forwarded-For, полученный от произвольного клиента, сам по себе не доказывает IP провайдера. Проверка транспортного источника и проверка объекта имеют разные границы. В курсе не настраивается внешний reverse proxy, поэтому локальное ограничение нельзя объявлять готовым production-решением.
Разбор тела должен иметь предел размера и понятный отказ для неверного JSON. Webhook не нужен полный экспорт компании или произвольные вложения. Ограничение помогает защищать ресурсы, но не заменяет проверку подлинности. Даже маленькое сообщение может быть ложным.
Inbox фиксирует завершённое применение
Таблица inbox содержит provider, eventId и результат обработки. Ключ события помогает распознать повтор доставки. Однако основная финансовая идемпотентность остаётся в ApplyPaymentAsync: провайдер может сообщить об одном объекте разными событиями, либо сверка применит снимок без исходного webhook.
Запись inbox и применение результата выполняются согласованно. Нельзя сначала навсегда пометить событие processed, а затем отдельно менять платеж: процесс может остановиться между действиями, и повтор будет пропущен. В полном коде финальная отметка сохраняется вместе с финансовым commit. Незавершённое получение не считается обработанным.
Если snapshot всё ещё pending, можно подтвердить обработку данного сигнала без выдачи права. Следующее событие либо сверка принесёт другой снимок. EventId не означает, что платеж обязательно перешёл в финальное состояние. Наш обработчик журналирует фактически прочитанный status, а не предполагаемый смысл имени события.
Одновременные повторы сериализуются блокировкой платежа и уникальностью inbox. Второй запрос после завершения первого возвращает успех без второго эффекта. Если оба пришли до создания inbox, финансовый метод всё равно не выдаст период дважды. Так защиту можно объяснить на нескольких уровнях, а не полагаться на одно удачное сообщение.
Когда возвращать успешное подтверждение
Сервис отвечает успешным HTTP-кодом после того, как результат сохранён. Для локального примера это 200. Если база недоступна, временный отказ позволяет повторить доставку. Отвечать 200 до сохранения только ради короткого времени ответа опасно: провайдер может считать событие принятым и прекратить повтор, а продукт потеряет изменение.
Для настоящего большого потока можно быстро сохранять сырой сигнал в надёжную очередь и подтверждать именно сохранение. Тогда асинхронный обработчик отдельно применит его к платежу. Это другой технический путь с тем же предметным требованием: после успешного подтверждения у сервиса должно остаться долговечное основание для продолжения.
Не возвращайте в webhook-ответе детали заказa компании. Отправителю достаточно подтверждения либо временной ошибки. Диагностика содержит eventId, providerId и код обработки, а не имена проектов. Иначе публичный технический путь станет неожиданным API чтения финансовой истории.
Для ручного наблюдения отправьте один сигнал три раза после succeeded fixture. Ожидаются успешные подтверждения, один оплаченный период и одна отметка eventId. Затем отправьте новое событие о том же объекте: финансовый результат не увеличивается. Повтор с неверным providerId не должен открыть системный сеанс произвольной компании.
Совпавший eventId проверяет идентичность
Inbox не должен считать любое совпадение eventId достаточным успехом. Если тот же ключ появился с другим providerId, это конфликт идентичности сигнала. Полный обработчик сравнивает сохранённую связь и откатывает применение в общей транзакции. Иначе ложное сообщение могло бы занять ключ настоящего события, а последующая доставка была бы пропущена. Для разных провайдеров пространство идентификаторов разделяется полем provider. Локальный fixture использует только один источник, но составной ключ уже показывает место этого разделения. Если внешний сервис не предоставляет устойчивого eventId, выберите свой сохраняемый отпечаток сигнала и всё равно оставьте финансовую идемпотентность на уровне платежного перехода.
Следующий урок добавляет периодическую сверку. Она закроет случаи, когда уведомление не пришло, связывание прервалось либо состояние осталось неопределённым после таймаута.