---
title: "Zdarzeniowo z założenia: jak jedna płatność staje się lojalnością, retencją i paragonem"
description: "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ą."
url: "https://bookatu.com/pl-pl/blog/event-driven-by-design-the-post-payment-ripple"
language: "pl"
section: "Blog"
category: "Technologia"
author: "Zespół inżynierów Bookatu"
published: "2026-06-24"
updated: "2026-06-24"
tags: ["inżynieria", "architektura zdarzeniowa", "architektura", "stripe", "idempotencja", "plany rozwoju"]
---

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

> **W skrócie**
>
> - Jedna płatność powinna się rozchodzić falą: lojalność, retencja, paragon i księga wynikają z niej wszystkie.
> - Każdy skutek w dole strumienia zależy od prawdziwej zmiany stanu, więc ponowienie ze strony Stripe nigdy nie odpala go dwa razy.
> - Webhook jest szwem. Dziś to porządny rozdział wewnątrz procesu, a plany zamieniają ten szew w trwałą szynę zdarzeń.

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ę.](https://bookatu.com/blog/eng-event-driven.jpg)

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