Сверка платежей после пропущенного уведомления
Webhook ускоряет получение состояния, но не должен быть единственным способом узнать результат. Уведомление может не дойти, а процесс — остановиться после создания провайдера и до сохранения providerId. В этих случаях локальный pending не означает, что платеж всё ещё ожидает действия пользователя.
Для «Пространства» добавим сверку: фоновая либо ручная команда перечитывает незавершённые попытки и применяет тот же ApplyPaymentAsync. Она не создает второй заказ и не выдаёт право собственным упрощённым кодом. Полная реализация находится в BillingService.cs архива; автоматические циклы и ручные запросы при подготовке не выполнялись.
Сначала восстановить неизвестную связь
Локальный платеж может содержать provider key, но ещё не иметь providerId. Такое состояние возникает после потерянного ответа создания. Сверка повторяет CreateAsync с прежним ключом и прежним снимком денег. Fixture-адаптер возвращает тот же объект, потому что ключ хранится долговечно.
Нельзя просто выбрать «последний платеж с похожей суммой». Две компании могут платить одинаковую цену одновременно. Сумма и время являются полезными диагностическими признаками, но не надёжной идентичностью. Связь определяется сохранённым ключом конкретной попытки и ответом провайдера.
После получения providerId сервис сохраняет реестр маршрутизации. Если этот commit снова оборвётся, следующий повтор использует прежний ключ. Операция восстановления не меняет заказ, его цену и период. Так каждая незавершённая стадия может быть продолжена по сохранённым данным без участия браузера.
Для реального провайдера нужно учитывать срок хранения ключа идемпотентности и доступные методы поиска. Локальный fixture хранит ключ весь срок урока. Это осознанное упрощение, которое нельзя обещать внешнему API без проверки его документации. Когда срок ключа истёк, повтор создания требует отдельного решения, а не автоматического нового платежа.
Снимок применяется одним методом
Когда providerId известен, сверка вызывает GetAsync и передаёт результат в ApplyPaymentAsync. Проверки суммы, валюты, перехода и выдачи entitlement остаются прежними. Таким образом, webhook и сверка могут соревноваться за один платеж, но блокировка строки делает их эффекты согласованными.
Ниже — законченный фрагмент оркестрации из ReconcileAsync. intent восстановлен из локальной строки платежа и заказа; запись связи выполняется отдельным методом. Листинг не является всем сервисом, его типы доступны в архиве.
var snapshot = payment.ProviderId is null
? await provider.CreateAsync(intent, ct)
: await provider.GetAsync(payment.ProviderId, ct);
await BindProviderAsync(payment, snapshot, ct);
await ApplyPaymentAsync(payment.OrganizationId, payment.Id, snapshot, ct);
Если snapshot pending, сверка сохраняет момент последней проверки и назначает следующую. Она не увеличивает период и не считает число успешных GET оплатой. Если succeeded, она выполняет тот же финансовый commit. Если canceled, завершает попытку без доступа.
Старый локальный succeeded обычно не требует частого перечитывания для выдачи права. Отдельные возвраты будут иметь собственную сверку. Смешивание их в одном цикле «если что-то отличается, переписать status» опасно: возврат является новой операцией, а не отменой факта успешного платежа.
Повторы не должны перегружать провайдера
Для незавершённых записей сохраняется next_check_at и число попыток. Временная ошибка увеличивает интервал с ограничением. Конкретные лимиты внешнего сервиса определяет real-адаптер; учебный fixture не имеет сетевого rate limit, но сохраняет форму состояния. Без такого расписания общий цикл будет бесконечно проверять одну и ту же ошибку.
Резкое накопление pending-операций требует диагностики. Это может быть недоступный провайдер, ошибка callback-маршрута, неправильное связывание либо нормальная задержка подтверждения. Возраст состояния и last_error помогают различить причины. Автоматическое canceled после любого заданного времени способно противоречить уже состоявшейся оплате.
Число попыток нельзя использовать как доказательство отказа денег. Оно отражает работу нашего сервиса, а не решение провайдера. После предела автоматических попыток запись может перейти в needs_review, сохранив неопределённость. В локальном курсе оператор вручную вызывает сверку и видит её предметный результат; финансовый итог не выдумывается.
Выборка кандидатов относится к техническому control plane и содержит только адреса операций. Для чтения денег и применения состояния открывается ограниченный сеанс нужной компании. Общий scheduler не становится публичным API владельца «Юга». Его отдельная конфигурация и права продолжают границу worker из SaaS-курса.
Сверка и закрытая компания
Платеж может завершиться после deleting или deleted. Сверка должна сохранить финансовую правду, но не открывать заново проекты и не отменять жизненный цикл. Если доступ уже не может быть предоставлен, продукту потребуется отдельная предметная процедура, например уведомление оператора и возврат. Наш пример показывает запись результата и не обещает автоматическое юридическое решение.
По этой причине финансовые строки не удаляются процедурой очищения проектов. Иначе поздний providerId не найдёт намерение, сумма не будет проверена, а оператор не сможет объяснить оплату. Срок финансового хранения определяется отдельно; курс не выводит его из срока экспорта или членства.
Диагностический отчёт сверки содержит paymentId, прежнее и новое состояние, момент и причину ошибки. Он не должен дублировать карты или реальные credentials. В нашем локальном адаптере таких данных вообще нет. Если real-ответ содержит чувствительные поля, адаптер преобразует его в минимальную внутреннюю модель до записи журнала.
Потеря уведомления в ручном сценарии
Создайте pending fixture, переведите его в succeeded, но не вызывайте webhook. Ожидаемо заказ пока остаётся open. Затем вызовите owner-маршрут reconcile для того же paymentId. Он должен получить снимок, сделать заказ paid и выдать один период. Повтор reconcile не продлевает период повторно.
Другой сценарий останавливает связывание сразу после создания fixture. В базе приложения остаётся provider key без providerId. Сверка должна восстановить тот же объект, а не создать новый. Это показывает отличие восстановления стадии от новой попытки оплаты. В курсе результаты описаны как ожидаемые, а не как выполненный эксперимент.
Теперь и доставка уведомления, и периодическое чтение приводят к одному подтверждённому состоянию. Следующий урок определит подписку и границы календарного периода, чтобы успешная оплата давала конкретный срок, а не неопределённое «тариф активен».