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.

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.
| 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:
- Odczytać surową treść żądania i zweryfikować podpis.
- Wstawić zdarzenie do tabeli zdarzeń, z identyfikatorem zdarzenia od operatora objętym ograniczeniem unikalności.
- Odpowiedzieć statusem 2xx, gdy tylko wstawienie zostanie zatwierdzone.
- 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ą.
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
Chcecie mieć to załatwione bez robienia tego samodzielnie?
Napiszcie, nad czym pracujecie. W ciągu jednego dnia roboczego odpowie wam człowiek.
