Платежная система может доставить событие с задержкой или повторно. Если приложение сначала отключило доступ по локальному таймеру, а потом получило подтверждение успешного платежа, простое присваивание последнего статуса способно включить уже отмененную подписку или оставить оплаченного клиента без услуги.
Источник истины должен определяться типом события и состоянием подписки, а не временем доставки 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 без двойных продлений и ручной правки статусов.