Все статьи
Deep dive8 мин чтения

Отзыв доступа после возврата средств: шаг, который Apple и Google не делают за вас

И Apple, и Google могут оформить возврат средств, пока клиент сохраняет доступ к покупке. Здесь точно объясняется, когда доступ отзывается автоматически, когда это должен делать ваш сервер, и какое уведомление почти никто не обрабатывает.

Смартфон светится на столе ночью, иллюстрируя момент, когда доступ к возвращённой покупке ещё не отозван

Главное

  • В Google Play возврат средств по заказу сам по себе не отменяет доступ пользователя. Доступ отзывается только если в Play Console отмечена опция "Remove entitlement" или если вызов API передаёт revoke=true, а это не поведение по умолчанию.
  • Возврат средств, оформленный без опции отзыва, вообще никогда не появится в Voided Purchases API. Процесс отзыва доступа, который читает только этот источник данных, незаметно пропустит все такие заказы.
  • Apple может автоматически отозвать лицензию для неисчезающих покупок и подписок, но расходуемая покупка считается доставленной в момент использования, поэтому у StoreKit нет механизма для её отзыва. Вашему серверу придётся самому списывать баланс.
  • Apple также может отменить собственный возврат средств. Уведомление REFUND_REVERSED означает, что ранее одобренный возврат был отменён, revocationDate снова становится null, а любой доступ или баланс, которые вы убрали, нужно восстановить.
  • Поле voidedReason у Google принимает девять значений, от remorse и accidental_purchase до fraud, friendly_fraud и unacknowledged_purchase, поэтому один и тот же источник данных, который говорит вам, когда отзывать доступ, также показывает, какие отзывы связаны с мошенничеством, а какие с ошибкой в вашей собственной интеграции.
  • Каждый день, в течение которого возвращённая покупка сохраняет доступ, это день, за который вы платите за вычисления, хранение и вызовы API, стоящие за ней, деньгами, которые вы уже вернули.

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

Почему возврат средств не отзывает доступ по умолчанию

И Apple, и Google рассматривают деньги и доступ как две разные операции. Денежная сторона, это политика магазина: как только возврат одобрен, он окончателен и реестр обновляется. Сторона доступа опциональна, либо это задача разработчика, либо и то и другое, в зависимости от платформы и типа покупки. Ни один из магазинов не предполагает, что вы хотите убрать доступ сразу же в момент завершения возврата, потому что многие законные случаи (частичный возврат, жест доброй воли, поддержка клиента) вообще не должны приводить к отзыву продукта.

Google Play: флажок, который никто не замечает

В Play Console возврат средств по заказу, это форма из двух частей: выбрать сумму возврата, а затем отдельно решить, отмечать ли "Remove entitlement". Оставить этот флажок снятым, это путь по умолчанию для большинства агентов поддержки, потому что большинство запросов на возврат, это жесты доброй воли, при которых клиент сохраняет купленное. Проблема в том, что ничто на экране подтверждения возврата не сообщает вам, что именно так клиент сохраняет подписку или внутриигровой предмет после возврата, который вы хотели сделать окончательным.

У пути через API та же ловушка

Тот же выбор существует и в коде. Эндпоинт orders.refund принимает необязательный параметр запроса revoke; когда он равен true, доступ к товару прекращается немедленно, а для регулярной подписки вместе с этим отменяются все будущие платежи. Если параметр опустить, по умолчанию он становится false: возврат оформляется, запись о покупке обновляется, а пользователь продолжает пользоваться продуктом точно так же, как и раньше.

ДействиеДоступ отозван?Появляется в Voided Purchases API?
Возврат в Play Console, "Remove entitlement" не отмеченНетНет
Возврат в Play Console, "Remove entitlement" отмеченДа, немедленноДа
API orders.refund, параметр revoke не указан (по умолчанию false)НетНет
API orders.refund, revoke=trueДа, немедленноДа

Именно последняя строка ловит интеграции врасплох. Voided Purchases API существует, чтобы вы могли построить конвейер отзыва доступа без ручной проверки Play Console, но он перечисляет только те заказы, которые были действительно аннулированы с отзывом. Возврат средств, обработанный без отмеченного флажка или без revoke=true, невидим для этого источника данных по замыслу, а не из-за ошибки.

Серверная стойка с одним красным индикатором состояния среди рядов зелёных, символизирующая незаметное решение об отзыве доступа

Чтение поля voidedReason Когда покупка действительно отображается как аннулированная, поле voidedReason сообщает причину по шкале из девяти значений: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback и unacknowledged_purchase. Это не просто учёт. Remorse и accidental_purchase, это обычные возвраты. Fraud, friendly_fraud и chargeback стоит направлять в очередь проверки на мошенничество, а не по простому пути отзыва и забвения, потому что тот же аккаунт статистически с большей вероятностью повторит это снова.

Один операционный нюанс важен, если вы работаете с этим источником данных напрямую: purchases.voidedpurchases.list ограничен 6000 запросов в день и 30 запросами в любое окно из 30 секунд. Дизайн, который опрашивает в плотном цикле, быстро упирается в этот потолок; предполагаемый паттерн, это запланированное получение данных с токенами пагинации, а не непрерывный опрос.

Apple: App Store может отозвать лицензию, но не расходуемую покупку

App Store Server Notifications V2 от Apple отправляют уведомление REFUND в момент завершения возврата средств, одинаково для расходуемых, нерасходуемых покупок, автоматически продлеваемых подписок и подписок без автопродления. То, что происходит после этого уведомления, целиком зависит от типа покупки, потому что собственный механизм отзыва StoreKit покрывает только два типа из четырёх.

Почему расходуемые покупки, это исключение

Нерасходуемая покупка или подписка имеет постоянное право доступа, которое системы Apple могут отключить. У расходуемой покупки этого нет: как только она использована, она считается доставленной, и в StoreKit больше нечего отзывать. Если клиент купил 500 монет, потратил их и получил возврат средств, монеты уже исчезли с точки зрения Apple. Единственный реестр, где они ещё числятся, это ваш. Уведомление REFUND, это ваш единственный сигнал найти соответствующий исходный идентификатор транзакции и списать этот баланс из своих собственных записей; ничего на стороне Apple не сделает это за вас.

УведомлениеЧто это значитЧто должен сделать ваш сервер
REFUNDApple вернула средства за расходуемую, нерасходуемую, автопродлеваемую или неавтопродлеваемую покупкуНемедленно отозвать доступ к нерасходуемым покупкам и подпискам; самостоятельно списать баланс расходуемой покупки
REFUND_DECLINEDApple отклонила запрос клиента на возврат средствНичего. Доступ никогда не был под угрозой
REFUND_REVERSEDApple отменила уже одобренный возврат средствВосстановить доступ или баланс, которые вы убрали из-за уведомления REFUND
CONSUMPTION_REQUESTApple хочет получить ваши доказательства перед тем, как принять решение о возвратеОтветить в течение 12 часов; это срабатывает до REFUND, а не после него

Уведомление, которое почти никто не обрабатывает

REFUND_REVERSED, это то, что большинство интеграций полностью пропускают, потому что оно описывает, как Apple отменяет собственное решение, случай, который мало кто из разработчиков продумывает с первого раза. Когда оно приходит, revocationDate в транзакции снова становится null: с точки зрения Apple покупка снова действительна. Если ваш сервер удалил право доступа, отменил запись о подписке или обнулил баланс расходуемой покупки при получении исходного REFUND, REFUND_REVERSED, это сигнал вернуть всё это обратно.

Как закрыть этот пробел, шаг за шагом

  • В Google Play сделайте revoke=true поведением по умолчанию для каждого пути возврата, будь то через Play Console или API, если только конкретный случай поддержки не требует, чтобы клиент сохранил доступ.
  • Опрашивайте purchases.voidedpurchases.list по расписанию с токенами пагинации, а не в плотном цикле, и оставайтесь в пределах 6000 запросов в день и 30 запросов за 30 секунд.
  • Направляйте значения voidedReason fraud, friendly_fraud и chargeback в очередь проверки, а не по пути автоматического отзыва и закрытия.
  • Для Apple подпишитесь на App Store Server Notifications V2 и отзывайте доступ к нерасходуемым покупкам и подпискам в момент, когда уведомление REFUND называет их.
  • Храните исходный идентификатор транзакции для каждой расходуемой покупки, чтобы уведомление REFUND могло найти точный баланс для списания без ручного поиска.
  • Обрабатывайте REFUND_REVERSED явно и фиксируйте каждый отзыв доступа привязанным к его идентификатору транзакции или заказа, чтобы отмена могла точно восстановить то, что было убрано.

Во что на самом деле обходится этот пробел

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

Именно поэтому RefundHalt рассматривает отзыв доступа как часть конвейера возврата средств, а не как нечто второстепенное: событие REFUND и изменение доступа фиксируются как единое действие, а событие REFUND_REVERSED автоматически восстанавливает состояние, вместо того чтобы ждать, пока кто-то заметит, что клиент всё ещё заблокирован от того, что Apple ему уже вернула.

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

Отзывает ли возврат средств за покупку в Google Play доступ автоматически?
Нет. Доступ отзывается только если в Play Console отмечена опция "Remove entitlement" или если вызов API orders.refund включает revoke=true. Ни один из этих вариантов не является поведением по умолчанию, поэтому возврат может быть оформлен, пока клиент продолжает пользоваться продуктом.
Что происходит, когда расходуемая внутриигровая покупка возвращается в App Store?
Apple отправляет уведомление REFUND, но у StoreKit нет способа отозвать расходуемую покупку, потому что она считается доставленной сразу после использования. Сервер разработчика должен найти исходную транзакцию и списать баланс вручную.
Что такое уведомление REFUND_REVERSED и нужна ли для него особая обработка?
REFUND_REVERSED означает, что Apple отменила уже одобренный возврат средств; revocationDate снова становится null, и покупка вновь считается действительной. Любой доступ или баланс, убранные из-за исходного REFUND, нужно восстановить, а большинство интеграций вообще никогда не слушают это уведомление.
Почему возвращённый заказ Google Play может не появиться в Voided Purchases API?
API перечисляет только заказы, аннулированные с отзывом доступа. Если возврат средств был обработан без опции отзыва, этот заказ вообще не возвращается данным эндпоинтом, поэтому конвейер, полагающийся только на него, полностью пропустит такие возвраты.
Как часто можно опрашивать Voided Purchases API от Google?
Эндпоинт ограничен 6000 запросов в день и 30 запросами в любое окно из 30 секунд, поэтому он предназначен для запланированного получения данных с токенами пагинации, а не для непрерывного опроса.

Источники и дополнительное чтение

RefundHalt

Автопилот возвратов для App Store и Google Play

Читайте дальше

Следующий запрос на возврат уже в пути.

Настройте RefundHalt за то время, что уходит на чтение очередного письма в поддержку о возврате, который вы не успели оспорить.