Синхронизация подписок с App Store и Google Play ломается не на оплате, а на статусах
У магазина и вашего биллинга почти всегда разные представления о жизни подписки: там — auto-renew, grace, billing retry, paused, revoked; у вас — active, past_due, canceled, expired. Если не построить явную матрицу переходов, система начнет делать опасные допущения: продлевать доступ после отмены, отключать пользователя во время grace-периода или терять выручку из-за ложного expire.
Ключевая ошибка — считать webhook источником истины без проверки очередности и идемпотентности. События могут приходить повторно, с задержкой и в другом порядке. Поэтому у каждого входящего сигнала должен быть event_id, дедупликация, журнал сырого payload и детерминированный state machine, который принимает только допустимые переходы. Идемпотентность в биллинге — это не рекомендация, а базовый вопрос выживания системы.
Отдельно проектируйте расхождения между entitlement и payment state. Доступ к продукту должен опираться не на один флаг, а на вычисляемое право: активна ли подписка, истек ли оплаченный период, есть ли grace, не было ли refund/revoke. Для этого полезны два слоя: ingestion событий и reconciliation job, который регулярно сверяет локальное состояние с магазином и закрывает “висячие” кейсы после сетевых сбоев.
Если нет ретраев с backoff, версии состояния и аудита изменений, вы получите revenue leakage в двух направлениях: недооткрытый доступ и незакрытый доступ после отмены. Давайте разберем, что происходит с транзакцией в момент сетевого сплита шлюза: именно там чаще всего рождаются фантомные активные подписки и спорные списания.
Стройте синхронизацию как конвейер с дедупликацией, проверкой порядка и периодической сверкой — тогда подписка будет жить по правилам, а не по шуму интеграции.
Подписки: биллинг-лаб
@subscriptions_billing_lab_arb
Синхронизация подписок с App Store и Google Play ломается не на оплате, а на статусах
Этот пост опубликован в Telegram-канале Подписки: биллинг-лаб. Подписаться можно по ссылке: @subscriptions_billing_lab_arb.