Платежная система может доставить событие с задержкой или повторно. Если приложение сначала отключило доступ по локальному таймеру, а потом получило подтверждение успешного платежа, простое присваивание последнего статуса способно включить уже отмененную подписку или оставить оплаченного клиента без услуги.

Источник истины должен определяться типом события и состоянием подписки, а не временем доставки webhook. Обработчик хранит идентификатор события, версию платежа и бизнес-время, выполняет идемпотентный переход и периодически сверяется с API платежной системы.

Что проверить в первую очередь

Сначала зафиксируйте точный сценарий, время ошибки и последнее известное рабочее состояние. Не меняйте несколько настроек одновременно: один контролируемый шаг должен подтверждать или исключать одну гипотезу. Перед работой с данными и конфигурацией подготовьте резервную копию и понятный способ отката.

  • Соберите timeline: создание счета, попытка списания, локальное отключение и получение webhook.
  • Проверьте event id, payment id, subscription id, тип и фактическое время каждого события.
  • Уточните, откуда приложение берет право доступа: платеж, период подписки или отдельную entitlement-запись.
  • Проверьте подпись webhook и журнал повторных доставок.

Почему возникает проблема

Внешний симптом обычно появляется на границе нескольких компонентов: интерфейса, backend, базы, фоновой очереди или внешнего сервиса. Поэтому важно найти первое место, где состояние становится неверным, а не исправлять последнее сообщение об ошибке.

  • События применяются в порядке доставки, хотя провайдер не гарантирует этот порядок.
  • Один webhook обрабатывается несколько раз после timeout ответа.
  • Локальный cron отключает доступ, не проверяя незавершенную попытку оплаты.
  • Статус платежа и право на услугу хранятся в одном поле и перетирают друг друга.
  • Система не выполняет сверку зависших и противоречивых состояний.

Пошаговая диагностика

Диагностику проводите на тестовой записи или отдельном окружении. В журналах скрывайте токены, пароли и персональные данные. Для каждого шага сохраняйте измеримый результат: идентификатор события, код ответа, версию записи, состояние процесса или контрольную сумму.

  • Восстановите хронологию по неизменяемым event id и created_at провайдера.
  • Повторно отправьте одно тестовое событие и проверьте отсутствие второго начисления периода.
  • Доставьте события намеренно в обратном порядке на тестовой подписке.
  • Проверьте транзакцию: запись события и изменение entitlement должны фиксироваться атомарно.
  • Сравните локальное состояние с API провайдера для выборки спорных подписок.

Статус платежа и право доступа — разные сущности

Платеж описывает денежную операцию, подписка — договорный период, entitlement — фактическое право пользоваться функцией. Их разделение делает запоздавшие события управляемыми.

  • Webhook сначала сохраняется как уникальное событие, затем применяется к state machine.
  • Бизнес-время события сравнивается с уже обработанной версией, а не с временем HTTP-доставки.
  • Успешный платеж создает или продлевает период доступа только один раз.
  • Отмена автопродления не обязана немедленно отнимать уже оплаченный период.
  • Dispute, refund и chargeback обрабатываются отдельными переходами с собственными правилами.

Как исправить проблему

Исправление лучше разбить на небольшие обратимые изменения. Сначала устраните подтвержденную причину, затем повторите исходный сценарий и проверьте соседние функции. Массовую обработку данных запускайте на ограниченной выборке с отчетом и только после сверки расширяйте на весь объем.

  • Добавьте уникальный индекс по provider + event_id и идемпотентный обработчик.
  • Вынесите entitlement в отдельную модель с valid_from, valid_until и причиной изменения.
  • Применяйте только допустимые переходы state machine с учетом версии и времени события.
  • Измените cron: он учитывает pending платежи и предоставляет короткий grace period по правилам бизнеса.
  • Добавьте reconciliation-задачу для подписок в промежуточном или противоречивом статусе.

Безопасный порядок внедрения

  • Сохраните затрагиваемые данные, конфигурацию и текущие журналы, заранее проверив способ отката.
  • Повторите проблему на тестовом объекте без реальных списаний, рассылок и изменений клиентских данных.
  • Внесите одно логическое изменение и зафиксируйте его в системе контроля версий или журнале работ.
  • Не отключайте авторизацию, валидацию, шифрование и другие защитные механизмы ради быстрого исчезновения ошибки.
  • После выкладки контролируйте логи, метрики и полный пользовательский сценарий, а не только один успешный запрос.

Как проверить результат

Разовый успешный тест недостаточен. Повторите операцию, проверьте крайние значения, параллельные действия и восстановление после перезапуска или временного сбоя. Для важного сценария сохраните автоматический тест либо короткий регрессионный чек-лист.

  • Прямой, повторный и обратный порядок событий дают одинаковый итоговый доступ.
  • Один платеж не продлевает подписку дважды после retry.
  • Оплаченный период сохраняется при отмене автопродления согласно правилам.
  • Зависший webhook виден в очереди и безопасно переобрабатывается.

Типичные ошибки при исправлении

  • Считать последний доставленный webhook самым новым бизнес-событием.
  • Отвечать провайдеру до фиксации события и затем терять обработку при сбое.
  • Использовать email клиента как ключ платежной сущности.
  • Вручную менять статус без записи причины, версии и связи с платежом.

Как предотвратить повторение

Профилактика строится вокруг явных контрактов, повторяемых релизов и наблюдаемости. Система должна не только работать сейчас, но и позволять быстро увидеть нарушение инварианта при следующем обновлении, росте нагрузки или сбое внешнего сервиса.

  • Храните входящие события неизменяемо и контролируйте возраст очереди.
  • Тестируйте повторы, задержку, обратный порядок и частичный сбой транзакции.
  • Разделяйте деньги, подписку и entitlement в модели.
  • Ежедневно сверяйте локальные активные подписки с провайдером.

Что подготовить для технического разбора

  • Описание ожидаемого и фактического поведения, а также точную последовательность действий.
  • Время проблемы, идентификатор тестового объекта и версии затронутых компонентов.
  • Фрагменты журналов до и после ошибки без секретов и персональных данных.
  • Перечень последних изменений и уже выполненных проверок.
  • Безопасный доступ к тестовой среде или способ воспроизвести сбой без влияния на клиентов.

Частые вопросы

Можно ли просто запросить текущий статус платежа в API?

Да как часть сверки, но webhook все равно нужно сохранять и обрабатывать идемпотентно. API-запрос может временно не отвечать и не объясняет историю переходов.

Нужен ли grace period?

Это бизнес-решение. Технически он полезен при временной задержке платежа, но должен иметь четкий срок и не заменять корректную обработку событий.

Когда нужна помощь специалиста

Если запоздавшие webhooks включают или отключают доступ неправильно, я могу восстановить цепочку событий, внедрить идемпотентную state machine и reconciliation без двойных продлений и ручной правки статусов.