Budowa · Zaawansowane

Zgubione webhooki płatności: płatność przeszła, a sklep się nie dowiedział

Klient płaci, operator potwierdza, a powiadomienie do waszego sklepu ginie, spóźnia się albo przychodzi dwa razy. Pokazujemy, jak zaprojektować obsługę zamówień tak, żeby zgubione webhooki płatności nic nie kosztowały: żadnych opłaconych zamówień, które wyglądają na nieopłacone, żadnych zdublowanych zamówień, żadnego drugiego obciążenia.

6 min czytania

Osoba trzyma kartę bankową i kończy płatność na laptopie.

Zdjęcie: domena publiczna (CC0)

W tym poradniku

Gdzie powiadomienie o płatności może zawieść

W płatności kartą albo portfelem biorą udział trzy strony, które nie mają wspólnej bazy danych: przeglądarka klienta, operator płatności i wasz sklep. Operator poznaje wynik pierwszy. Wasz sklep dowiaduje się o nim na dwa sposoby: przeglądarka klienta zostaje przekierowana z powrotem na stronę powrotu, a operator wysyła powiadomienie między serwerami, nazywane zwykle webhookiem.

Żaden z tych kanałów nie jest sam w sobie niezawodny. Klient może zamknąć kartę przed przekierowaniem. Webhook może trafić na wasz serwer w trakcie wdrożenia, przekroczenia limitu czasu albo awarii bazy danych. Operatorzy ponawiają powiadomienia, których odbioru nie potwierdzono, więc to samo zdarzenie może też przyjść dwa razy albo dotrzeć po kilku godzinach, już po nowszym zdarzeniu dotyczącym tej samej płatności.

Praktyczny wniosek: webhooki są dostarczane „co najmniej raz, bez gwarancji kolejności”. Sklep, który zakłada „dokładnie raz, po kolei”, prędzej czy później pokaże opłacone zamówienie jako nieopłacone albo wyśle towar dwa razy. Jeśli planujecie integrację płatności, projektujcie z myślą o tym od pierwszego dnia.

Przypadki awarii webhooków płatności, co widać i co zapobiega szkodzie
Przypadek awarii Objaw w sklepie Zabezpieczenie
Powiadomienie nigdy nie dociera Zamówienie zostaje w stanie „oczekuje na płatność”, choć pieniądze pobrano Zadanie uzgadniające, które pyta operatora
Powiadomienie dociera dwa razy Dwa e-maile z potwierdzeniem, stan magazynu zmniejszony dwukrotnie Unikalny identyfikator zdarzenia i warunkowa zmiana statusu
Powiadomienie dociera z opóźnieniem Zamówienie anulowano jako nieopłacone, a potem pojawia się płatność Sprawdzenie stanu u operatora przed anulowaniem, obsługa przypadku „opłacone po anulowaniu”
Zdarzenia docierają w złej kolejności Zwrot jest przetwarzany, zanim wiadomo o płatności Maszyna stanów, która odrzuca niemożliwe przejścia i ponownie pobiera stan
Procedura obsługi ulega awarii po potwierdzeniu odbioru Zdarzenie przepadło, niczego nie przetworzono Trwałe zapisanie zdarzenia przed odpowiedzią
Sfałszowane powiadomienie Zamówienie oznaczone jako opłacone bez żadnej płatności Weryfikacja podpisu na surowej treści żądania
Sklep ponawia własne żądanie po przekroczeniu limitu czasu Klient zostaje obciążony albo dostaje zwrot dwa razy Klucz idempotentności w każdym wychodzącym żądaniu

Trwałe przetwarzanie: najpierw zapisać, szybko potwierdzić, pracować z kolejki

Punkt końcowy, który odbiera webhooki, powinien robić bardzo niewiele. Całe jego zadanie to cztery kroki:

  1. Odczytać surową treść żądania i zweryfikować podpis.
  2. Wstawić zdarzenie do tabeli zdarzeń, z identyfikatorem zdarzenia od operatora objętym ograniczeniem unikalności.
  3. Odpowiedzieć statusem 2xx, gdy tylko wstawienie zostanie zatwierdzone.
  4. Całą resztę zostawić workerowi, który czyta nieprzetworzone zdarzenia.

Jeśli potwierdzicie odbiór, zanim zdarzenie zostanie zapisane, a proces padnie, operator uzna, że je macie, i nigdy nie wyśle go ponownie. Jeśli właściwą pracę (oznaczenie zamówienia jako opłaconego, wystawienie faktury, wywołanie magazynu) wykonacie przed potwierdzeniem, wolna zależność sprawi, że żądanie przekroczy limit czasu, operator je ponowi, a wy przetworzycie tę samą płatność równolegle.

Jeśli baza danych nie działa, odpowiedzcie statusem 5xx, żeby operator ponowił próbę później. Nigdy nie odpowiadajcie 2xx na zdarzenie, którego nie udało się zapisać.

Worker pobiera zdarzenia z tabeli albo z kolejki, którą ta tabela zasila. Kolejki też dostarczają co najmniej raz, więc worker musi dać się bezpiecznie uruchomić dwa razy na tym samym zdarzeniu. Po ustalonej liczbie nieudanych prób przenieście zdarzenie do stanu „dead letter” i zaalarmujcie człowieka.

Idempotentność: żeby „oznacz zamówienie jako opłacone” dało się bezpiecznie powtórzyć

Idempotentna operacja to taka, której dwukrotne wykonanie daje ten sam skutek co jednokrotne. Osiąga się to trzema technikami stosowanymi razem.

Usuwajcie duplikaty według identyfikatora zdarzenia

Ograniczenie unikalności na identyfikatorze zdarzenia od operatora nie pozwala zapisać tej samej dostawy dwa razy. Nie pomaga, kiedy dwa różne zdarzenia opisują tę samą płatność.

Zmieniajcie stan warunkowo

Nie piszcie „ustaw status na opłacone”. Zapiszcie przejście: ustaw status na opłacone tam, gdzie zgadza się identyfikator zamówienia, a status to nadal „oczekuje na płatność”. Baza danych podaje, ile wierszy się zmieniło. Skutki uboczne uruchamiają się tylko wtedy, gdy zmienił się dokładnie jeden wiersz: zmniejszenie stanu magazynu, utworzenie faktury, dodanie e-maila z potwierdzeniem do kolejki. Drugie uruchomienie zmienia zero wierszy i nic nie robi.

Skutki uboczne zapisujcie w tej samej transakcji co zmianę statusu, jako wiersze w tabeli outbox, którą po kolei obsługuje proces wysyłający. W przeciwnym razie awaria między „opłacone” a „wyślij e-mail” zostawia zamówienie, które jest opłacone i nigdy niepotwierdzone.

Używajcie kluczy idempotentności w żądaniach wychodzących

Ten sam problem istnieje w drugą stronę. Kiedy wasz sklep prosi operatora o pobranie albo zwrot płatności, a odpowiedź nie przychodzi w limicie czasu, nie wiecie, czy operacja się odbyła. Większość API płatności przyjmuje klucz idempotentności: unikalną wartość, którą generujecie raz dla zamierzonej operacji i wysyłacie bez zmian przy każdym ponowieniu, dzięki czemu operator wykonuje ją raz. Zapiszcie klucz przy zamówieniu przed pierwszą próbą, a nie w pamięci.

Zanim zaufacie jakiemukolwiek powiadomieniu, porównajcie też jego kwotę i walutę z zamówieniem. Poprawne zdarzenie na niewłaściwą kwotę to nie jest opłacone zamówienie.

Uzgadnianie: pytać, zamiast czekać

Operatorzy po pewnym czasie przestają ponawiać, a punkt końcowy, który przez dzień był źle skonfigurowany, przegapił wszystko. Uzgadnianie to siatka bezpieczeństwa: zaplanowane zadanie, które pyta operatora o stan faktyczny.

  • Otwarte zamówienia: dla każdego zamówienia, które „oczekuje na płatność” dłużej niż kilka minut, zapytajcie operatora według zapisanego identyfikatora płatności i przepuśćcie odpowiedź przez tę samą procedurę obsługi, z której korzysta webhook.
  • Kierunek odwrotny: raz dziennie pobierzcie listę udanych płatności u operatora za dany okres i sprawdźcie, czy każdej odpowiada zamówienie w stanie opłaconym, na tę samą kwotę.
  • Przed anulowaniem: zamówienie anuluje się jako nieopłacone dopiero wtedy, gdy operator potwierdzi, że żadna płatność się nie powiodła ani nie jest w toku.

Jeśli uzgadnianie i worker webhooków wywołują to samo przejście warunkowe, nie ma znaczenia, które z nich dotrze pierwsze.

Poniższe liczby to przykład poglądowy, a nie pomiary: sklep z 10 000 udanych płatności w miesiącu, z których 150 (1,5%) nie dotarło jako jedno czyste powiadomienie przy pierwszej próbie. 96 + 42 + 12 = 150.

Przykład poglądowy: 150 z 10 000 płatności z niedoskonałym powiadomieniem

Dzięki usuwaniu duplikatów i uzgadnianiu wszystkie 150 kończy w tym samym stanie co pozostałe 9850.

Projekt odzyskiwania bez zdublowanych zamówień i obciążeń

Zdublowane zamówienia mają zwykle jedną przyczynę: zamówienie powstaje w dwóch miejscach, raz na stronie powrotu i raz przez webhook. Lekarstwem jest utworzenie go raz, przed płatnością.

  • Tworzcie zamówienie w stanie „oczekuje na płatność” przed przekierowaniem do operatora i przekazujcie operatorowi jego identyfikator jako referencję płatności
  • Traktujcie stronę powrotu jako wskazówkę: uruchamia zapytanie o status, nigdy nie oznacza zamówienia jako opłaconego na podstawie parametrów adresu URL
  • Weryfikujcie podpisy na surowej treści i trzymajcie sekrety poza repozytorium kodu
  • Zapisujcie każde zdarzenie pod unikalnym identyfikatorem zdarzenia, zanim potwierdzicie odbiór
  • Każdą zmianę statusu wykonujcie jako przejście warunkowe, a skutki uboczne uruchamiajcie tylko wtedy, gdy zmienił się wiersz
  • Wysyłajcie klucz idempotentności z każdym pobraniem, zwrotem i utworzeniem płatności
  • Uruchamiajcie uzgadnianie otwartych zamówień co kilka minut, a pełne porównanie codziennie
  • Alarmujcie człowieka przy zdarzeniach w stanie „dead letter” i przy płatnościach bez pasującego zamówienia

Płatność, która przychodzi do już anulowanego zamówienia, wymaga reguły biznesowej, a nie tylko kodu: otwórzcie zamówienie ponownie, jeśli towar wciąż jest w magazynie, a w przeciwnym razie automatycznie zwróćcie pieniądze i poinformujcie klienta. Postanówcie to z góry. W działającym sklepie internetowym najszybszy audyt to dwukrotne odtworzenie zapisanego webhooka w środowisku testowym i sprawdzenie, co się stanie. Te same wzorce dotyczą każdej integracji API, która odbiera zdarzenia z innego systemu.

Pytania, które słyszymy często

Nie. Klient może nigdy na nią nie dotrzeć, a parametry jej adresu URL można zmienić. Używajcie jej do uruchomienia zapytania o status po stronie serwera i do pokazania wyniku, a zamówienie niech oznacza jako opłacone webhook albo zadanie uzgadniające.

Status 2xx. Operator musi tylko wiedzieć, że macie zdarzenie. Odpowiedź z błędem wywołałaby kolejne ponowienia czegoś, co już macie.

Dla otwartych zamówień zwykle co kilka minut, zaczynając kilka minut po utworzeniu zamówienia, żeby najpierw dotarły zwykłe powiadomienia. Pełne porównanie płatności u operatora z zamówieniami może działać raz dziennie.

Chcecie mieć to załatwione bez robienia tego samodzielnie?

Napiszcie, nad czym pracujecie. W ciągu jednego dnia roboczego odpowie wam człowiek.