Dziennik
Technologia3 min czytania

Zdarzeniowo z założenia: jak jedna płatność staje się lojalnością, retencją i paragonem

Platforma rezerwacyjna stoi albo upada na spójności. Oto jak pojedyncza płatność ze Stripe rozchodzi się na punkty lojalnościowe, retencję, paragon i wpis w księdze, dlaczego każdy skutek zależy od realnej zmiany stanu i dokąd zmierzamy z tą architekturą.

ZIZespół inżynierów Bookatu

Kiedy klient płaci za rezerwację, zaliczkę, pakiet albo członkostwo, to jedno zdarzenie powinno ruszyć cały system. Jego saldo lojalnościowe ma urosnąć. Ma znów liczyć się jako aktywny, a nie uśpiony. Ma dostać paragon. A pieniądze mają wylądować w księgach do uzgodnienia. Pomyl jedno z tych, a produkt po cichu traci zaufanie: klient zostaje obciążony, a jego kredyty nigdy się nie pojawiają, albo ponowienie ze Stripe liczy sprzedaż dwa razy.

Przez jakiś czas każdy z tych elementów był osobną wyspą. Webhook oznaczał rzeczy jako opłacone i na tym kończył. Zrobiliśmy więc z płatności zdarzenie i pozwoliliśmy reszcie systemu na nie reagować.

Wzorzec: rozdzielaj, ale tylko przy realnej zmianie stanu

Diagram: płatność ze Stripe trafia do idempotentnego webhooka, który wywołuje applyPaidRipple i rozdziela ją na lojalność, retencję, paragon i księgę.
Jedna płatność, cztery skutki. Webhook jest szwem, a fala działa na zasadzie najlepszych starań i nigdy nie blokuje ścieżki pieniędzy.

Stripe wysyła checkout.session.completed do jednego webhooka. Handler najpierw robi zapis krytyczny dla pieniędzy (oznacza zaliczkę jako opłaconą, przestawia fakturę na opłaconą, przyznaje kredyt z pakietu), a potem wywołuje jedną wspólną funkcję, applyPaidRipple, dla strony zaangażowania. Fala działa na zasadzie najlepszych starań: każdy krok jest opakowany tak, że awaria trafia do logów i zostaje połknięta, bo kapryśny dostawca poczty nigdy nie może nam przeszkodzić w zapisaniu prawdziwej płatności.

ts
// In the webhook, after the money-critical write succeeds:if (fresh) {  await applyPaidRipple({    orgId,    customerId,    amountCents,    loyaltyReason: "Package purchase",    receiptTo: email,    receiptDedupeKey: `payment_receipt:package:${session.id}`,  });}// applyPaidRipple: loyalty -> lastVisitAt bump -> transactional receipt,// each in its own try/catch. The ledger write runs unconditionally below.

Idempotencja to cała gra

Stripe dostarczy to samo zdarzenie więcej niż raz. To funkcja, a nie błąd, i oznacza, że każdy handler musi być bezpieczny przy dwukrotnym uruchomieniu. Chronią nas dwie warstwy. Po pierwsze, identyfikator zdarzenia trafia do tabeli przetworzonych zdarzeń z unikalnym ograniczeniem, więc ponowne dostarczenie dokładnie tego samego zdarzenia kończy się od razu. Po drugie, i ważniejsze, każda ścieżka uzależnia swoją falę od własnej realnej zmiany stanu: od flagi, która jest prawdziwa tylko wtedy, gdy wiersz faktycznie się przestawia po raz pierwszy.

ts
// markInvoicePaid flips 'open' -> 'paid' and returns true ONLY on that// real transition. A redelivery sees 'paid' already and returns false.const flipped = await markInvoicePaid(invoiceId, paymentIntentId, amountTotal);if (flipped) {  await applyPaidRipple({ /* ...loyalty, retention, receipt */ });}// So even if the event-id dedupe is bypassed, the ripple still fires once.

Klucz deduplikacji to nie miły dodatek. To różnica między klientem, któremu raz podziękowano, a klientem obciążonym dwa razy.

Dokąd to zmierza

Dziś fala to rozdział wewnątrz procesu, czyli tyle maszynerii, ile trzeba na tym etapie: prosto, transakcyjnie i łatwo o tym myśleć. Ale webhook jest świadomie szwem, a plany zamieniają ten szew w trwałą szynę zdarzeń. Kształt zostaje ten sam, dostarczanie robi się mocniejsze.

  • Kolejka przed każdym skutkiem (lojalność, retencja, wiadomości), żeby wolny konsument nigdy nie blokował webhooka, a ponowienia były automatyczne z narastającym odstępem.
  • Tabela outbox zapisywana w tej samej transakcji co pieniądze, a potem przekazywana na szynę, żeby żaden skutek nie zginął między 'opłacone' a 'opublikowane'.
  • Wydzielenie najcięższych konsumentów (powiadomienia, ocena retencji) do własnych wdrażanych usług w miarę wzrostu obciążenia, przy czym rdzeń zapisu rezerwacji zostaje jedną, silnie spójną jednostką.
  • Zarządzana warstwa zdarzeń i kolejek w naszej chmurze, żeby skalowanie było zmianą konfiguracji, a nie migracją.

Wniosek, który trzyma się przez to wszystko: ścieżkę pieniędzy trzymaj silnie spójną i synchroniczną, a wszystko, co na niej wisi, rób zdarzeniowo, idempotentnie i na zasadzie najlepszych starań. Płatność jest faktem. Wszystko dobre, co z niej wynika, jest reakcją, a reakcje powinny być bezpieczne przy powtórzeniu.

inżynieriaarchitektura zdarzeniowaarchitekturastripeidempotencjaplany rozwoju
Gotowy, żeby to wykorzystać?

Bookatu daje Ci firmową stronę rezerwacji, zadatki, karnety, karty podarunkowe i przypomnienia, przy 0% prowizji od Twoich rezerwacji.

Zacznij za darmo