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ą.
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

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.
// 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.
// 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.