Wszystkie artykuły
Deep dive8 min czytania

Zarówno Apple, jak i Google mogą dostarczyć ten sam zwrot na Twój serwer więcej niż raz, a zduplikowane powiadomienia o zwrotach kosztują Cię, jeśli reagujesz na każde z nich

Apple ponawia powiadomienie o zwrocie do pięciu razy, a Google Play opiera się na Pub/Sub z dostarczaniem at-least-once, więc ten sam zwrot może dotrzeć na Twój serwer więcej niż raz. Oto jak obsługiwać zduplikowane powiadomienia o zwrotach bez podwójnego odejmowania salda i bez dwukrotnego spalania limitu API.

Wiele identycznych papierowych kopert ułożonych na ciemnym biurku, jedna odsunięta na bok, jako obraz zduplikowanych powiadomień o zwrotach docierających na Twój serwer

Najważniejsze wnioski

  • Apple ponawia App Store Server Notification V2 pięć razy, po 1, 12, 24, 48 i 72 godzinach od ostatniej próby, za każdym razem, gdy Twój serwer nie odpowie statusem HTTP między 200 a 206. Licząc pierwszą próbę, jeden zwrot może dotrzeć nawet sześć razy.
  • Każde prawdziwe ponowienie Apple niesie tę samą notificationUUID, więc to pole, a nie identyfikator transakcji, jest Twoim kluczem deduplikacji.
  • Real-time Developer Notifications od Google Play opierają się na Cloud Pub/Sub, które gwarantuje dostarczanie at-least-once i brak kolejności, więc ta sama wiadomość może dotrzeć dwa razy lub w złej kolejności. Google każe Ci sprawdzić unikalność messageId, zanim cokolwiek przetworzysz.
  • Okresowe powiadomienia CONSUMPTION_REQUEST od Apple nie są ponowieniami. Apple wysyła coraz to nowe przez cały otwarty okres zwrotu, każde z inną notificationUUID, więc deduplikacja po notificationUUID poprawnie zachowuje każde z nich.
  • Ta sama transactionId Apple może nieść więcej niż jedną decyzję, na przykład REFUND_DECLINED, po którym później następuje REFUND, więc deduplikacja wyłącznie po identyfikatorze transakcji odrzuca odrębne zdarzenie, którego potrzebowałeś.
  • Odrzucenie duplikatu przez zwrócenie 4xx lub 5xx sprawia tylko, że sklep ponawia je. Deduplikuj we własnej bazie danych i zawsze zwracaj status sukcesu.
  • Program obsługi zwrotów, który nie jest idempotentny, działa podwójnie przy drugim dostarczeniu. Odejmuje saldo dwa razy, cofa wypłatę dwa razy lub spala rozliczany limit Play Developer API i App Store Server API, ponownie sprawdzając zwrot, który już zamknął.

Twój serwer otrzyma to samo zdarzenie zwrotu więcej niż raz, i oba sklepy zaprojektowały to celowo. Apple ponawia App Store Server Notification do pięciu razy, gdy Twój punkt końcowy nie odpowie czysto. Google Play dostarcza swoje Real-time Developer Notifications przez Cloud Pub/Sub, które obiecuje dostarczanie at-least-once i nic o kolejności. Pytanie nigdy nie brzmi więc, czy duplikat dotrze. Brzmi ono, co robi Twój kod za drugim razem, gdy widzi ten sam zwrot. Zrób to źle, a odejmiesz saldo dwa razy, cofniesz wypłatę dwa razy lub spalisz rozliczany limit API, ponownie sprawdzając zwrot, który już zamknąłeś. Oto jak zduplikowane powiadomienia o zwrotach faktycznie do Ciebie docierają, które powtórzenia są prawdziwymi duplikatami, a które tylko tak wyglądają, i jak je obsługiwać, aby drugie dostarczenie było darmowe.

Powiadomienie o zwrocie jest dostarczane co najmniej raz, co nie jest tym samym co dokładnie raz

Oba sklepy traktują dostarczone powiadomienie jako obietnicę, którą wciąż próbują spełnić, a nie pojedynczy strzał, który oddają i zapominają. To dobre dla niezawodności, bo powiadomienie, które przegapisz podczas wdrożenia, i tak dotrze do Ciebie później. To pułapka dla poprawności, bo mechanizm gwarantujący, że w końcu otrzymasz zdarzenie, gwarantuje też, że czasem otrzymasz je dwa razy. Twój program obsługi musi być idempotentny, co oznacza, że drugie i trzecie dostarczenie jednego zwrotu nie zmieniają niczego, czego pierwsze już nie zmieniło.

Apple ponawia pięć razy w ciągu trzech dni

Gdy Apple wysyła App Store Server Notification V2, oczekuje, że Twój serwer odpowie statusem HTTP w zakresie od 200 do 206. Wszystko inne, 4xx lub 5xx, mówi Apple, że dostarczenie się nie powiodło, i Apple ponawia. Harmonogram jest stały: pięć ponowień, po 1, 12, 24, 48 i 72 godzinach od poprzedniej próby. Licząc pierwszą próbę, jedno zdarzenie zwrotu może dotrzeć nawet sześć razy, rozłożone na mniej więcej tydzień. Każde z tych ponowień niesie tę samą notificationUUID. To pole jest Twoim kluczem deduplikacji. Jeśli już zapisałeś notificationUUID, dostarczenie, które trzymasz, jest powtórzeniem, a poprawną reakcją jest nie zapisywać nic nowego i mimo to zwrócić 200.

Google Play opiera się na Pub/Sub, które obiecuje at-least-once i nic nie mówi o kolejności

Real-time Developer Notifications od Google Play są publikowane do tematu Cloud Pub/Sub. Gwarancja dostarczania Pub/Sub to at-least-once i nie ma żadnej gwarancji kolejności. To oznacza, że ta sama wiadomość może zostać dostarczona do Twojego punktu końcowego więcej niż raz, a dwie wiadomości dla tego samego zakupu mogą dotrzeć w złej kolejności. Własne wytyczne Google są jednoznaczne: rozpakuj pole data w base64, odczytaj messageId i sprawdź, że jeszcze go nie widziałeś, zanim cokolwiek przetworzysz. Zduplikowana messageId to powtórzenie, które pomijasz. Dwa różne powiadomienia o jednym zakupie powinny mimo to trafić do tego samego rekordu, więc oprzyj swój zapisany stan także na purchaseToken i pozwól, by późniejsze zdarzenie zaktualizowało wiersz utworzony przez wcześniejsze.

PlatformaModel dostarczaniaDeduplikacja poSygnał sukcesuJeśli nie potwierdzisz
App Store Server Notifications V2Do 6 prób: pierwsza plus 5 ponowień po 1, 12, 24, 48, 72 godzinachnotificationUUIDHTTP 200 do 206Apple ponawia według stałego harmonogramu, potem przestaje
Google Play RTDN przez Pub/SubAt-least-once, brak gwarancji kolejnościPub/Sub messageId, powiązane z encją po purchaseTokenHTTP 200 na push lub jawne ackPub/Sub ponawia po wygaśnięciu terminu ack

Powtórzenia, które nie są duplikatami

Nie każde powiadomienie, które wygląda jak już widziane, jest ponowieniem. Dwa zachowania Apple wysyłają naprawdę nowe zdarzenia, które dzielą jeden zakup, ale każde musi zostać przetworzone, a scalenie ich naiwną deduplikacją odrzuca informacje, których potrzebowałeś.

Apple wysyła świeże CONSUMPTION_REQUEST, a nie ponowienia

Podczas otwartego żądania zwrotu za produkt konsumpcyjny Apple nie wysyła jednego CONSUMPTION_REQUEST i nie czeka. Wysyła nowe okresowo przez cały otwarty okres zwrotu, aż zwrot zostanie zamknięty. Pracownicy Apple potwierdzili, że nie są to ponowienia, a znakiem rozpoznawczym jest pole, po którym deduplikujesz: każdy świeży CONSUMPTION_REQUEST niesie inną notificationUUID. Deduplikacja oparta na notificationUUID robi więc automatycznie właściwą rzecz. Scala prawdziwe ponowienia i zachowuje każdy odrębny monit. Czego nie wolno Ci robić, to deduplikować po identyfikatorze transakcji i typie powiadomienia, bo to uciszyłoby każdy CONSUMPTION_REQUEST po pierwszym i kosztowałoby Cię 12-godzinne okno dowodowe na tych porzuconych.

Jedna transakcja może nieść więcej niż jedną decyzję

Pojedyncza transactionId może w ciągu swojego życia wygenerować więcej niż jeden wynik zwrotu. Apple może wysłać REFUND_DECLINED, a potem, później, REFUND dla tej samej transakcji, a deweloperzy zgłaszają otrzymywanie trzech lub więcej powiadomień związanych ze zwrotem dla jednego identyfikatora transakcji. Każde jest odrębnym zdarzeniem z własną notificationUUID. Jeśli Twoim kluczem deduplikacji jest identyfikator transakcji, druga decyzja wygląda jak duplikat pierwszej i nigdy nie dowiesz się, że zwrot ostatecznie przyznano. Identyfikator transakcji grupuje zdarzenia. Nie identyfikuje ich.

Chwytak robota podnoszący jedną zduplikowaną paczkę z linii transportowej do bocznego pojemnika, obraz symbolizujący deduplikację powtarzających się powiadomień o zwrotach

Ile naprawdę kosztuje Cię duplikat

Powiadomienie o zwrocie to nie lampka statusu. Wyzwala prawdziwe działania: odbierasz dostęp, odejmujesz saldo produktu konsumpcyjnego, cofasz wypłatę dla twórcy, wywołujesz App Store Server API lub Play Developer API, aby potwierdzić stan. Uruchom którekolwiek z nich po raz drugi na duplikacie, a koszt jest realny.

Prześledź pieniądze. Dwukrotne odebranie dostępu jest nieszkodliwe, bo dostęp już zniknął. Dwukrotne odjęcie salda nie jest: użytkownik, który kupił pakiet monet i zwrócił go, może zostać zepchnięty na ujemne saldo, które Twój zespół wsparcia musi potem rozplątywać ręcznie. Dwukrotne cofnięcie wypłaty odbiera pieniądze, które już raz zwróciłeś, i teraz jesteś twórcy winien przeprosiny i korektę. A każdy duplikat, który ponownie przetwarzasz względem API sklepu, zużywa limit, którego ochrony Google wyraźnie Ci zaleca, więc lawina ponownych dostarczeń Pub/Sub podczas awarii może wepchnąć Cię w ograniczanie przepustowości dokładnie w tym dniu, w którym najmniej możesz sobie na to pozwolić.

Okna dowodowe zwrotu podnoszą stawkę po stronie Apple. Jeśli naiwna deduplikacja ucisza powtarzane CONSUMPTION_REQUEST, które Apple wysyła przez otwarty okres zwrotu, możesz przegapić to, na które musiałeś odpowiedzieć, a CONSUMPTION_REQUEST, na które nie odpowiesz w ciągu 12 godzin, to zwrot, który Apple często przyznaje domyślnie. To nie jest podwójne obciążenie. To utracona sprzedaż plus moc obliczeniowa, wywołania API, pamięć i wypłaty, które już wydałeś, dostarczając zakup, z których żadnego zwrot nie odzyskuje.

Tryb awariiCo idzie nie takIle kosztuje
Ponowne odjęcie salda przy zduplikowanym REFUNDSaldo produktu konsumpcyjnego użytkownika staje się ujemneRęczny czas wsparcia na uzgodnienie i złe doświadczenie klienta
Dwukrotne cofnięcie wypłatyOdbierasz pieniądze, które już raz zwróciłeśKorekta u twórcy i porządkowanie księgowości
Ponowne przetwarzanie względem API sklepuZduplikowane wywołania spalają limit Play Developer lub App Store Server APIOgraniczanie przepustowości podczas awarii, która spowodowała ponowne dostarczenie
Nadmierna deduplikacja CONSUMPTION_REQUESTPorzucasz odrębny monit o zwrot jako fałszywy duplikatPrzegapione 12-godzinne okno, więc Apple przyznaje zwrot domyślnie

Jak obsługiwać zduplikowane powiadomienia o zwrotach bez podwójnego działania

Wzorzec jest taki sam w obu sklepach, z innym kluczem. Zapisz dostarczenie, sprawdź klucz, zanim zadziałasz, zadziałaj raz i zawsze powiedz sklepowi, że je otrzymałeś.

  • Deduplikuj po identyfikatorze dostarczenia sklepu, a nie po transakcji. Użyj notificationUUID dla Apple i Pub/Sub messageId dla Google Play. Zapisz go z ograniczeniem unikalności, aby równoczesny duplikat przegrał wyścig zamiast działać podwójnie.
  • Uczyń działanie downstream idempotentnym na własnych warunkach. Oparcie na identyfikatorze dostarczenia zatrzymuje ponowne przetwarzanie, ale napisz też efekt tak, aby odbieranie, odejmowanie lub cofanie najpierw sprawdzało bieżący stan i było bezpieczne do dwukrotnego uruchomienia.
  • Najpierw zapisz, potem potwierdź. Zapisz zdarzenie do bazy danych, zanim zwrócisz 200 lub potwierdzisz wiadomość Pub/Sub. Jeśli potwierdzisz najpierw, a zapis się nie powiedzie, sklep uzna wiadomość za dostarczoną i nigdy jej nie wyśle ponownie, a teraz straciłeś ją na dobre.
  • Zawsze zwracaj status sukcesu, nawet dla duplikatu. 200 do 206 dla Apple, 200 na push Pub/Sub dla Google. Odrzucenie powtórzenia błędem sprawia tylko, że sklep wysyła je ponownie.
  • Grupuj po encji, identyfikuj po zdarzeniu. Oprzyj swój zapisany stan zakupu na purchaseToken lub originalTransactionId, aby dostarczenia w złej kolejności aktualizowały jeden wiersz, ale traktuj każdą notificationUUID lub messageId jako własne zdarzenie, bo jeden zakup zasadnie generuje kilka.

Krótka lista kontrolna, zanim zaufasz swojemu webhookowi zwrotów

  • Dostarczenia Apple są deduplikowane po notificationUUID, a powtórzenie nie zapisuje nic nowego, ale mimo to zwraca 200.
  • Dostarczenia Google Play są deduplikowane po Pub/Sub messageId, sprawdzanej przed jakimkolwiek przetwarzaniem.
  • Stan zakupu jest oparty na purchaseToken lub originalTransactionId, więc zdarzenia w złej kolejności trafiają do jednego rekordu.
  • Każdy efekt uboczny zwrotu, odebranie, odjęcie lub cofnięcie, jest bezpieczny do uruchomienia więcej niż raz.
  • Twój program obsługi zapisuje zdarzenie, zanim je potwierdzi, nigdy po.
  • Powtarzane CONSUMPTION_REQUEST są traktowane jako odrębne monity, a nie duplikaty, więc żadne otwarte okno zwrotu nie zostaje porzucone.

Przepuść celowo duplikat przez własny webhook i patrz, jak za drugim razem nic nie zmienia. To cały test. Program obsługi zwrotów, który można bezpiecznie trafić dwa razy, to taki, o który przestajesz się martwić w chwili, gdy sklep postanawia trafić go sześć razy.

Często zadawane pytania

Dlaczego mój serwer otrzymuje to samo powiadomienie o zwrocie z App Store więcej niż raz?
Ponieważ Apple ponawia App Store Server Notification V2 do pięciu razy, po 1, 12, 24, 48 i 72 godzinach od ostatniej próby, za każdym razem, gdy Twój serwer nie odpowie statusem HTTP między 200 a 206. Każde ponowienie niesie tę samą notificationUUID, więc możesz je rozpoznać i pominąć.
Którego pola powinienem użyć do deduplikacji App Store Server Notifications?
Użyj notificationUUID. Prawdziwe ponowienie zawsze powtarza tę samą notificationUUID, podczas gdy każde naprawdę nowe zdarzenie, w tym każdy świeży CONSUMPTION_REQUEST, dostaje inną, więc deduplikacja po notificationUUID pomija powtórzenia bez odrzucania odrębnych zdarzeń.
Czy powtarzane powiadomienia CONSUMPTION_REQUEST to duplikaty, które powinienem ignorować?
Nie. Apple wysyła nowe powiadomienia CONSUMPTION_REQUEST okresowo przez otwarty okres zwrotu, a pracownicy Apple potwierdzają, że nie są to ponowienia. Każde ma własną notificationUUID, więc przetwórz każde z nich. Ich porzucenie grozi przegapieniem 12-godzinnego okna, które Apple daje Ci na odpowiedź.
Jak deduplikować Google Play Real-time Developer Notifications?
Odczytaj Pub/Sub messageId z każdego powiadomienia i sprawdź je względem już przetworzonych, zanim zadziałasz, bo Pub/Sub dostarcza at-least-once i może wysłać tę samą wiadomość więcej niż raz. Google wyraźnie to zaleca, aby uniknąć podwójnego przetwarzania i zmarnowanego limitu API.
Czy powinienem zwrócić błąd, aby odrzucić zduplikowane powiadomienie o zwrocie?
Nie. Zwrócenie 4xx lub 5xx mówi sklepowi, że dostarczenie się nie powiodło, więc i tak ponawia. Deduplikuj we własnej bazie danych i zawsze zwracaj status sukcesu, HTTP 200 do 206 dla Apple lub potwierdzenie 200 dla push Google Play.

Ź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ć.