Journal
Teknik3 min läsning

Händelsestyrt från grunden: så blir en betalning till lojalitet, återkommande kunder och ett kvitto

En bokningsplattform står och faller med konsekvens. Så här sprider sig en enda Stripe-betalning ut i lojalitetspoäng, kundvård, ett kvitto och en post i huvudboken, varför varje effekt kräver en verklig tillståndsändring, och vart vi tar arkitekturen härnäst.

BTBookatus teknikteam

När en kund betalar för en bokning, en handpenning, ett paket eller ett medlemskap ska den enda händelsen få hela systemet att röra sig. Deras lojalitetssaldo ska växa. De ska räknas som aktiva igen, inte bortfallna. De ska få ett kvitto. Och pengarna ska landa i bokföringen för avstämning. Får du en av dem fel tappar produkten tyst i förtroende: en medlem debiteras men deras poäng dyker aldrig upp, eller ett omförsök från Stripe dubbelräknar en försäljning.

Ett tag var var och en av dem sin egen ö. Webhooken markerade saker som betalda och stannade där. Så vi gjorde betalningen till en händelse, och lät resten av systemet reagera på den.

Mönstret: fläk ut, men bara vid en verklig tillståndsändring

Diagram: en Stripe-betalning träffar en idempotent webhook, som anropar applyPaidRipple och fläker ut till lojalitet, kundvård, kvitto och huvudbok.
En betalning, fyra effekter. Webhooken är fogen. Ringarna på vattnet gör sitt bästa och blockerar aldrig pengaflödet.

Stripe skickar checkout.session.completed till en webhook. Hanteraren gör den pengakritiska skrivningen först (markera handpenningen som betald, vända fakturan till betald, bevilja paketkrediten) och anropar sedan en enda delad funktion, applyPaidRipple, för engagemangsdelen. Ringarna gör sitt bästa: varje steg är inkapslat så att ett fel loggas och sväljs, eftersom en ostabil mejlleverantör aldrig får hindra oss från att registrera en verklig betalning.

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.

Idempotens är hela grejen

Stripe kommer att leverera samma händelse mer än en gång. Det är en funktion, inte en bugg, och det betyder att varje hanterare måste vara säker att köra två gånger. Två lager skyddar oss. För det första registreras händelsens id i en tabell över behandlade händelser med en unik begränsning, så att en ny leverans av exakt samma händelse kortsluts. För det andra, och viktigare, kräver varje väg en egen genuin tillståndsändring innan ringarna sprids: ett booleskt värde som bara är sant första gången raden faktiskt vänder.

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.

Nyckeln för dubblettkontroll är inget trevligt tillägg. Den är skillnaden mellan att en medlem tackas en gång och debiteras två gånger.

Vart det här är på väg

I dag är ringarna en utfläkning i samma process, vilket är lagom mycket maskineri för var vi står: det är enkelt, transaktionellt och lätt att resonera om. Men webhooken är medvetet en fog, och färdplanen gör den fogen till en hållbar händelsebuss. Formen förblir densamma, leveransen blir starkare.

  • En kö framför varje effekt (lojalitet, kundvård, meddelanden), så att en långsam konsument aldrig håller upp webhooken och omförsök sker automatiskt med gradvis längre väntan.
  • En utkorgstabell som skrivs i samma transaktion som pengarna och sedan skickas vidare till bussen, så att en effekt aldrig går förlorad mellan "betald" och "publicerad".
  • Att dela ut de tyngsta konsumenterna (aviseringar, poängsättning av kundvård) i egna driftsättbara tjänster när belastningen växer, medan kärnskrivningen för bokningar förblir en enda, starkt konsekvent enhet.
  • Ett hanterat händelse- och kölager i vårt moln, så att skalning blir en konfigurationsändring i stället för en migrering.

Lärdomen som håller genom alltihop: håll pengavägen starkt konsekvent och synkron, och gör allt som hänger på den händelsestyrt, idempotent och bästa möjliga. En betalning är ett faktum. Allt gott som följer av den är en reaktion, och reaktioner ska vara säkra att spela upp igen.

utvecklinghändelsestyrtarkitekturstripeidempotensfärdplan
Vill du sätta det här i verket?

Bookatu ger dig en bokningssida i din egen stil, handpenning, medlemskap, presentkort och påminnelser, med 0% provision på dina bokningar.

Börja gratis