Wszystkie artykuły
Deep dive8 min czytania

Cofnięcie dostępu po zwrocie: krok, którego Apple i Google nie wykonają za ciebie

Apple i Google mogą przetworzyć zwrot pieniędzy, podczas gdy klient nadal zachowuje zakupiony produkt. Oto dokładnie, kiedy dostęp jest odbierany automatycznie, kiedy musi to zrobić twój serwer, i jedno powiadomienie, którego większość integracji nigdy nie obsługuje.

Smartfon świecący na biurku w nocy, ilustrujący moment, w którym dostęp do zwróconego zakupu nie został jeszcze cofnięty

Najważniejsze wnioski

  • W Google Play zwrot pieniędzy za zamówienie sam w sobie nie usuwa dostępu użytkownika. Dostęp jest odbierany tylko wtedy, gdy w Play Console zaznaczono opcję "Remove entitlement", albo gdy wywołanie API zawiera parametr revoke=true, co nie jest wartością domyślną.
  • Zwrot wykonany bez opcji revoke nigdy nie pojawi się w Voided Purchases API. Zadanie odbierające dostęp, które czyta wyłącznie ten kanał danych, po cichu pominie każde takie zamówienie.
  • Apple może automatycznie cofnąć licencję dla non-consumable i subskrypcji, ale consumable jest uznawany za dostarczony w chwili jego zużycia, więc StoreKit nie ma żadnego mechanizmu, by go cofnąć. Twój serwer musi sam odjąć ten stan konta.
  • Apple może też cofnąć własny zwrot pieniędzy. Powiadomienie REFUND_REVERSED oznacza, że wcześniej przyznany zwrot został unieważniony, revocationDate wraca do null, a każdy dostęp lub stan konta, który usunąłeś, musi zostać przywrócony.
  • Pole voidedReason od Google przyjmuje dziewięć wartości, od remorse i accidental_purchase po fraud, friendly_fraud i unacknowledged_purchase, więc ten sam kanał danych, który mówi ci, kiedy odebrać dostęp, mówi też, które cofnięcia to nadużycia, a które to błąd w twojej własnej integracji.
  • Każdy dzień, w którym zwrócony zakup zachowuje dostęp, to dzień, w którym płacisz za moc obliczeniową, pamięć masową i wywołania API stojące za nim, z pieniędzy, które już oddałeś.

Zatwierdzenie zwrotu i odebranie dostępu to dwa oddzielne kroki, i tylko jeden z nich dzieje się automatycznie. Deweloperzy, którzy zakładają, że przetworzony zwrot oznacza cofnięty dostęp do zakupu, mylą się w przypadku obu wielkich sklepów, na różne sposoby i z różnych powodów. Dokładna wiedza o tym, gdzie kończy się automatyzacja, jest tym, co pozwala cofnąć dostęp po zwrocie, zamiast odkryć miesiące później, że nigdy do tego nie doszło.

Dlaczego zwrot pieniędzy domyślnie nie cofa dostępu

Apple i Google traktują pieniądze i dostęp jako dwie odrębne operacje. Strona pieniężna to polityka sklepu, gdy zwrot zostanie zatwierdzony, jest ostateczny i księgi zostają zaktualizowane. Strona dostępu jest opcjonalna, albo należy do dewelopera, albo do obu, w zależności od platformy i typu zakupu. Żaden sklep nie zakłada, że chcesz, aby dostęp zniknął w chwili sfinalizowania zwrotu, ponieważ wiele uzasadnionych przypadków (częściowy zwrot, gest dobrej woli, uprzejmość wobec klienta) w ogóle nie powinno odbierać produktu.

Google Play: pole wyboru, którego nikt nie zauważa

W Play Console zwrot pieniędzy za zamówienie to dwuczęściowy formularz, wybierasz kwotę zwrotu, a następnie osobno decydujesz, czy zaznaczyć "Remove entitlement". Pozostawienie tego pola niezaznaczonego to domyślna ścieżka wybierana przez większość konsultantów wsparcia, ponieważ większość próśb o zwrot to gesty dobrej woli, w których klient zachowuje to, co kupił. Problem w tym, że nic na ekranie potwierdzenia zwrotu nie mówi, że w ten sam sposób klient zachowuje subskrypcję lub przedmiot w aplikacji po zwrocie, który miał być ostateczny.

Ścieżka API ma tę samą pułapkę

Ten sam wybór istnieje w kodzie. Endpoint orders.refund przyjmuje opcjonalny parametr zapytania revoke; gdy ma wartość true, dostęp do przedmiotu zostaje natychmiast zakończony, a w przypadku subskrypcji odnawialnej anulowane zostają wraz z nim wszystkie przyszłe płatności. Pominięcie tego parametru sprawia, że domyślnie przyjmuje wartość false: zwrot zostaje zaksięgowany, rekord zakupu zostaje zaktualizowany, a użytkownik nadal korzysta z produktu dokładnie tak jak wcześniej.

DziałanieDostęp odebrany?Widoczne w Voided Purchases API?
Zwrot w Play Console, "Remove entitlement" niezaznaczoneNieNie
Zwrot w Play Console, "Remove entitlement" zaznaczoneTak, natychmiastTak
API orders.refund, parametr revoke pominięty (domyślnie false)NieNie
API orders.refund, revoke=trueTak, natychmiastTak

To właśnie ten ostatni wiersz jest tym, co łapie integracje w pułapkę. Voided Purchases API istnieje po to, byś mógł zbudować pipeline odbierania dostępu bez ręcznego sprawdzania Play Console, ale wymienia on tylko zamówienia, które faktycznie zostały unieważnione wraz z cofnięciem. Zwrot przetworzony bez tego pola wyboru, albo bez revoke=true, jest niewidoczny w tym kanale danych z założenia, nie przez błąd.

Szafa serwerowa z jedną czerwoną kontrolką statusu wśród rzędów zielonych, przedstawiająca ciche pominięcie decyzji o cofnięciu dostępu

Odczytywanie pola voidedReason Gdy zakup rzeczywiście pojawia się jako unieważniony, pole voidedReason mówi dlaczego, w skali dziewięciu wartości: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback i unacknowledged_purchase. To nie jest tylko księgowość. Remorse i accidental_purchase to zwykłe zwroty. Fraud, friendly_fraud i chargeback to wartości warte skierowania do kolejki weryfikacji nadużyć zamiast zwykłej ścieżki cofnij-i-zapomnij, ponieważ to samo konto statystycznie częściej powtarza takie zachowanie.

Jeśli budujesz coś bezpośrednio na tym kanale danych, liczy się jeden szczegół operacyjny: purchases.voidedpurchases.list ma limit 6000 zapytań dziennie i 30 zapytań w dowolnym oknie 30 sekund. Rozwiązanie, które odpytuje w ciasnej pętli, szybko osiąga ten limit; zamierzonym wzorcem jest zaplanowane pobieranie z tokenami paginacji, a nie ciągłe odpytywanie.

Apple: App Store może cofnąć licencję, ale nie consumable

App Store Server Notifications V2 od Apple wysyła powiadomienie REFUND w chwili sfinalizowania zwrotu, zarówno dla consumable, non-consumable, subskrypcji auto-renewable, jak i non-renewing. To, co dzieje się po tym powiadomieniu, zależy całkowicie od typu zakupu, ponieważ własny mechanizm cofania StoreKit obejmuje tylko dwa z czterech typów.

Dlaczego consumable są wyjątkiem

Non-consumable lub subskrypcja mają stały stan uprawnienia, który systemy Apple mogą wyłączyć. Consumable go nie ma: gdy tylko zostanie zużyty, jest traktowany jako dostarczony, i w StoreKit nie zostaje już nic do cofnięcia. Jeśli klient kupił 500 coins, wydał je, a następnie otrzymał zwrot, te coins z punktu widzenia Apple już nie istnieją. Jedyną księgą, która nadal je posiada, jest twoja. Powiadomienie REFUND to twój jedyny sygnał, by odnaleźć pasujący original transaction ID i odjąć ten stan konta z własnych rekordów; nic po stronie Apple nie zrobi tego za ciebie.

PowiadomienieCo oznaczaCo musi zrobić twój serwer
REFUNDApple zwróciło pieniądze za zakup typu consumable, non-consumable, auto-renewable lub non-renewingNatychmiast cofnij dostęp do non-consumable i subskrypcji; sam odejmij stan konta consumable
REFUND_DECLINEDApple odrzuciło prośbę klienta o zwrotNic. Dostęp nigdy nie był zagrożony
REFUND_REVERSEDApple cofnęło zwrot, który wcześniej przyznałoPrzywróć dostęp lub stan konta, który usunąłeś na skutek powiadomienia REFUND
CONSUMPTION_REQUESTApple chce twoich dowodów, zanim zdecyduje o zwrocieOdpowiedz w ciągu 12 godzin; to powiadomienie pojawia się przed REFUND, nie po nim

Powiadomienie, którego prawie nikt nie obsługuje

REFUND_REVERSED to powiadomienie, które większość integracji całkowicie pomija, ponieważ opisuje sytuację, w której Apple cofa własną decyzję, przypadek, na który mało który deweloper projektuje rozwiązanie za pierwszym razem. Gdy nadchodzi, revocationDate transakcji wraca do null: z punktu widzenia Apple zakup znów jest ważny. Jeśli twój serwer usunął uprawnienie, anulował rekord subskrypcji albo wyzerował stan konta consumable, gdy nadeszło pierwotne REFUND, REFUND_REVERSED to sygnał, by to wszystko przywrócić.

Jak zamknąć tę lukę, krok po kroku

  • W Google Play ustaw revoke=true jako domyślne dla każdej ścieżki zwrotu, niezależnie czy przechodzi ona przez Play Console czy przez API, chyba że konkretny przypadek wsparcia wymaga, aby klient zachował dostęp.
  • Odpytuj purchases.voidedpurchases.list zgodnie z harmonogramem, z tokenami paginacji, a nie w ciasnej pętli, i utrzymuj się poniżej 6000 żądań dziennie oraz 30 na 30 sekund.
  • Kieruj wartości voidedReason takie jak fraud, friendly_fraud i chargeback do kolejki weryfikacji, zamiast automatycznie cofać dostęp i zamykać sprawę.
  • U Apple zasubskrybuj App Store Server Notifications V2 i cofaj dostęp do non-consumable oraz subskrypcji, gdy tylko powiadomienie REFUND je wymieni.
  • Zapisuj original transaction ID przy każdym zakupie typu consumable, aby powiadomienie REFUND mogło znaleźć dokładny stan konta do odjęcia bez ręcznego wyszukiwania.
  • Obsługuj REFUND_REVERSED w sposób jawny i loguj każde cofnięcie dostępu wraz z jego transaction lub order ID, aby cofnięcie zwrotu mogło precyzyjnie przywrócić to, co zostało usunięte.

Ile tak naprawdę kosztuje ta luka

Nic z tego nie dotyczy uczciwości wobec klienta; chodzi o to, co nadal działa po tym, jak pieniądze zniknęły. Zwrócona subskrypcja, która nadal wywołuje twoje API, przechowuje dane i zużywa moc obliczeniową, to nie problem moralny, to pozycja kosztowa: płacisz rachunek za infrastrukturę za zakup, za który już oddałeś przychód. Sklepy nie śledzą tego kosztu za ciebie, ponieważ z ich strony zwrot jest zakończony w chwili, gdy pieniądze się przemieszczają. Strona dostępu w całości należy do rachunku, który musisz kontrolować sam.

To ten sam powód, dla którego RefundHalt traktuje cofanie dostępu jako część pipeline'u zwrotów, a nie jako coś dodanego na końcu: zdarzenie REFUND i zmiana dostępu są rejestrowane jako jedna akcja, a zdarzenie REFUND_REVERSED automatycznie przywraca stan, zamiast czekać, aż ktoś zauważy, że klient wciąż nie ma dostępu do czegoś, co Apple mu oddało.

Często zadawane pytania

Czy zwrot pieniędzy za zakup w Google Play automatycznie cofa dostęp?
Nie. Dostęp jest odbierany tylko wtedy, gdy w Play Console zaznaczono "Remove entitlement" albo wywołanie API orders.refund zawiera revoke=true. Żadne z nich nie jest wartością domyślną, więc zwrot może zostać zaksięgowany, podczas gdy klient nadal korzysta z produktu.
Co się dzieje, gdy zakup typu consumable w aplikacji zostaje zwrócony w App Store?
Apple wysyła powiadomienie REFUND, ale StoreKit nie ma sposobu, by cofnąć consumable, ponieważ jest on uznawany za dostarczony w chwili zużycia. Serwer dewelopera musi odnaleźć oryginalną transakcję i ręcznie odjąć stan konta.
Czym jest powiadomienie REFUND_REVERSED i czy wymaga specjalnej obsługi?
REFUND_REVERSED oznacza, że Apple cofnęło zwrot, który wcześniej przyznało; revocationDate wraca do null, a zakup znów jest ważny. Każdy dostęp lub stan konta usunięty przez pierwotne REFUND musi zostać przywrócony, a większość integracji w ogóle nie nasłuchuje tego powiadomienia.
Dlaczego zwrócone zamówienie z Google Play może nie pojawić się w Voided Purchases API?
API wymienia tylko zamówienia, które zostały unieważnione wraz z cofnięciem dostępu. Jeśli zwrot został przetworzony bez opcji revoke, zamówienie w ogóle nie zostanie zwrócone przez ten endpoint, więc pipeline, który opiera się wyłącznie na nim, całkowicie pominie takie zwroty.
Jak często mogę odpytywać Voided Purchases API od Google?
Endpoint ma limit 6000 zapytań dziennie i 30 zapytań w dowolnym oknie 30 sekund, więc jest przeznaczony do zaplanowanego pobierania z tokenami paginacji, a nie do ciągłego odpytywania.

Źródła i materiały dodatkowe

RefundHalt

Autopilot zwrotów dla App Store i Google Play

Czytaj dalej

Kolejny wniosek o zwrot jest już w drodze.

Skonfiguruj RefundHalt w czasie potrzebnym na przeczytanie kolejnej wiadomości od pomocy technicznej o zwrocie, którego nie udało Ci się zakwestionować.