Bu dosya sistemin şeklini gösteriyor. Gerekçeler decisions.md'de, kurallar
CLAUDE.md'de, şema ledger-schema.md'de. Kodu okumaya nereden başlanacağı
README.md'de.
Diyagramlardaki exchange, kuyruk ve hesap adları koddan alındı; uydurulmuş ad yok.
flowchart LR
client["Bireysel<br/>mobil uygulama"]
wclient["Bireysel web<br/>tarayıcı"]
bclient["İşyeri<br/>sistemi"]
bwclient["İşyeri paneli<br/>tarayıcı"]
staff["Backoffice paneli<br/>tarayıcı"]
subgraph public["public ingress"]
papi["<b>personal-mobile-api</b><br/>ön API"]
pwapi["<b>personal-web-bff</b><br/>ön API, web'in BFF'i"]
bapi["<b>business-api</b><br/>ön API, entegrasyon"]
bwapi["<b>business-web-bff</b><br/>ön API, panelin BFF'i"]
end
subgraph internal["iç ağ"]
boapi["<b>backoffice-bff</b><br/>ön API, panelin BFF'i"]
api["<b>wallet-api</b><br/>hesap, cüzdan, transfer"]
orch["<b>withdrawal-orchestrator</b><br/>çekim saga'sı"]
onb["<b>onboarding</b><br/>kayıt, kimlik doğrulaması"]
sadm["<b>staff-admin</b><br/>personel, roller, atamalar"]
end
subgraph restricted["IP kısıtlı ingress"]
hook["<b>topup-webhook</b><br/>imza doğrular, inbox'a yazar"]
bhook["<b>bank-webhook</b><br/>imza doğrular, inbox'a yazar"]
end
subgraph noingress["ingress YOK"]
consumer["<b>wallet-consumer</b><br/>ledger'a yazan tek tüketici"]
adapter["<b>bank-adapter</b><br/>bankayı arar, sonucu yayınlar"]
end
subgraph outside["dış kurumlar — canlıda gerçekleri"]
bank["<b>bank-fake</b><br/>bankanın API'si:<br/>havale girişi ve transfer"]
stripe["<b>stripe-fake</b><br/>kart sağlayıcısı:<br/>yalnızca giriş"]
sms["<b>sms-fake</b><br/>SMS sağlayıcısı"]
nvi["<b>nvi-fake</b><br/>nüfus kaydı"]
mail["<b>mailpit</b><br/>e-posta sağlayıcısı"]
end
idp["<b>hiwallet-keycloak</b><br/>müşterilerin kimlik sağlayıcısı"]
sidp["<b>hiwallet-staff-keycloak</b><br/>çalışanların kimlik sağlayıcısı<br/>iç ağ"]
mq[["RabbitMQ"]]
wdb[("hiwallet_wallet")]
tdb[("hiwallet_topup")]
odb[("hiwallet_withdrawal")]
bdb[("hiwallet_bank")]
ndb[("hiwallet_onboarding<br/>kendi sunucusu")]
sdb[("hiwallet_staff_admin<br/>kendi sunucusu")]
client -->|HTTPS| papi
wclient -->|"HTTPS, cookie"| pwapi
bclient -->|HTTPS| bapi
bwclient -->|"HTTPS, cookie"| bwapi
staff -->|"HTTPS, cookie"| boapi
boapi -->|"giriş"| sidp
papi --> api
papi --> orch
pwapi --> api
pwapi --> orch
papi --> onb
pwapi --> onb
onb -->|"hesabı aç, seviyeyi yükselt"| api
onb -->|"kullanıcıyı aç"| idp
onb -->|HTTP| sms
onb -->|HTTP| nvi
onb -->|SMTP| mail
onb --> ndb
bapi --> api
bapi --> orch
bwapi --> api
bwapi --> orch
boapi --> api
boapi --> orch
boapi --> sadm
api -->|"çalışanın izni"| sadm
orch -->|"çalışanın izni"| sadm
sadm -->|"kullanıcı, davet"| sidp
sadm --> sdb
stripe -->|"webhook + HMAC"| hook
bank -->|"webhook + HMAC"| hook
bank -->|"callback + HMAC"| bhook
api --> wdb
consumer --> wdb
hook --> tdb
orch --> odb
adapter --> bdb
bhook --> bdb
adapter -->|"HTTP"| bank
hook -->|publish| mq
orch <-->|"publish + consume"| mq
mq -->|consume| consumer
consumer -->|publish| mq
mq -->|consume| adapter
adapter -->|publish| mq
İstemci yalnızca kendi ön API'sine bağlanıyor; wallet-api, orchestrator ve
onboarding iç servis. Webhook'lar ve ingress'siz uygulamalar erişim seviyesine göre ayrılıyor
(decisions.md madde 28): farklı erişim seviyesi ayrı process'lere dağılıyor, aynı
erişim seviyesi tek process'te toplanıyor. wallet-consumer hem top-up event'lerini hem
çekim komutlarını hem settlement'ı dinliyor; üçü de ingress'siz ve aynı ledger'a yazıyor.
Dikkat edilecek dört şey:
wallet-api'nin ve ön API'lerin broker'a hiç bağlantısı yok. wallet-api'nin tek
bağımlılığı Postgres; ön API'lerin veritabanı da yok.
ledger_entries'e yazan iki uygulama var — wallet-api ve wallet-consumer —
ama tek kod üzerinden: WalletService.Core. İkinci bir kopya açılmıyor (madde 25).
Orchestrator wallet'a yalnızca komut gönderiyor, ledger'a yazan taraf
wallet-consumer. Bedeli iki veritabanı arasında ayrışma ihtimali, karşılığı takılmış
saga taraması (madde 33).
Bankayla iletişim HTTP. Orchestrator StartBankTransfer komutunu RabbitMQ'ya
yazıyor (komutun ayrıntısı bölüm 3'te). bank-adapter komutu kuyruktan okuyor ve
bankayı HTTP ile arıyor. Banka sonucu bank-webhook'a HTTP callback ile bildiriyor.
bank-fake ile stripe-fake canlıda yok; adaptörler oradaki adres ayarıyla kurumların
kendi endpoint'lerine bakıyor ve kodda tek satır değişmiyor (madde 35).
bank-adapter ile bank-webhook ayrı uygulamalar çünkü erişim seviyeleri farklı:
birinin IP kısıtlı ingress'i var, öbürünün hiç ingress'i yok. Aralarındaki tek bağ
hiwallet_bank; doğrudan çağrı yok.
İstemcinin gördüğü tek adres kendi ön API'si:
| istemci | ön API | erişim | arkasında |
|---|---|---|---|
| bireysel mobil uygulama | personal-mobile-api |
public | wallet-api, withdrawal-orchestrator, onboarding |
| bireysel web uygulaması (tarayıcı) | personal-web-bff |
public | wallet-api, withdrawal-orchestrator, onboarding |
| işyerinin sistemi | business-api |
public | wallet-api, withdrawal-orchestrator |
| işyeri paneli (tarayıcı) | business-web-bff |
public | wallet-api, withdrawal-orchestrator |
| backoffice paneli (tarayıcı) | backoffice-bff |
iç ağ | wallet-api, withdrawal-orchestrator |
Ön API veritabanına ve broker'a bağlanmıyor; isteği iç servise iletiyor. İç servisin
reddi (400, 404, 409, 422) istemciye aynen dönüyor; iç servise ulaşılamazsa istemci
503 alıyor. Müşteri başına rate limit ön API'de, iç servislerde değil.
wallet-api'nin uçları (/v1/accounts, /v1/wallets, /v1/transfers, /v1/promos)
ve orchestrator'ın /v1/withdrawals ucu iç sözleşme. Yeni bir istemci grubu kendi ön
API'siyle geliyor; ihtiyaca göre public ya da yalnızca iç ağdan erişiliyor.
Tarayıcıdan kullanılan arayüzün ön API'si BFF: token'ı tarayıcıdaki koda vermiyor,
tarayıcı yalnızca HttpOnly oturum cookie'si taşıyor. Bu yüzden müşterinin ve işyerinin
ikişer ön API'si var: mobil uygulama personal-mobile-api'ye, tarayıcıdaki uygulama
personal-web-bff'ye; token taşıyan sistem entegrasyonu business-api'ye, tarayıcıdaki
panel business-web-bff'ye bağlanıyor.
personal-mobile-api ve personal-web-bff'nin uçları aynı: kayıt ve doğrulama, hesap,
cüzdan, hareketler, promo partileri, transfer ve çekim. Hesap ön API'den açılmıyor, kayıt
açıyor. Web uygulamasının sayfalarını da personal-web-bff
sunuyor (web/personal). business-api'nin uçları: hesap, cüzdan, hareketler, transfer
(B2P, B2B), müşteriye promo ve çekim. backoffice-bff'in uçları: müşteri kaydını
görüntüleme, çekim incelemesi, işyerinin promo kabulü, personel promo'su, kampanyalar ve
personel yönetimi; her biri kendi izniyle. Panelin sayfalarını da o sunuyor (web/backoffice).
business-web-bff sağlık uçlarıyla ayakta.
Token'ı Keycloak imzalıyor. Mobil uygulama kimlik sağlayıcıdan token alıp ön API'ye
getiriyor; ön API token'ı doğruluyor ve iç servise AYNEN iletiyor. wallet-api ve
orchestrator token'ı yeniden doğruluyor: ön API'nin beyanına değil token'a bakıyorlar,
yani ele geçirilmiş bir ön API başkası adına istek yazdıramıyor.
mobil uygulama ──token──▶ personal-mobile-api ──aynı token──▶ wallet-api / orchestrator
doğrular yeniden doğrular, sahipliği kontrol eder
Tarayıcıda token yok. Girişi BFF yapıyor: Keycloak'la kod akışı ve PKCE, kendi gizli anahtarlı istemcisiyle. Token'lar cookie'nin içinde BFF'in anahtarıyla şifreli; iç servise giden istek token'ı oradan okuyor. Access token dolmak üzereyken BFF onu yeniliyor ve cookie'yi yeniden yazıyor.
tarayıcı ──cookie──▶ personal-web-bff ──oturumdaki token──▶ wallet-api / orchestrator
çözer, gerekirse yeniler yeniden doğrular, sahipliği kontrol eder
Her ön API yalnızca kendisi için verilmiş token'ı kabul ediyor: token'ın hedef
kitlesinde ön API'nin adı ve iç servislerin ortak adı (hiwallet-api) var. Mobil
uygulama token'ı kullanıcının girişiyle alıyor; işyerinin sistemi kendi istemcisinin
gizli anahtarıyla (client credentials) ve token'daki kimlik o istemcinin servis hesabı.
Hangi kimliğin hangi hesabın kullanıcısı olduğu wallet'ta (account_members); hesabı
açan kimlik hesabın kullanıcısı oluyor. İşyeri hesabı ön API'den açılmıyor; işyerinin
entegrasyonu hesaba backoffice'ten bağlanacak. wallet-api her uçta çağıranın kaynağın
kullanıcısı olduğunu kontrol ediyor; değilse kaynak yokmuş gibi 404. Orchestrator
hesabın kullanıcılarını bilmiyor: çekimi isteyen kimliği saga'ya yazıyor, düşme
komutuyla wallet'a gönderiyor ve wallet ledger'a yazmadan önce üyeliği doğruluyor.
Çalışanların kimlik sağlayıcısı müşterilerinkinden ayrı bir Keycloak kurulumu
(hiwallet-staff-keycloak, realm'i hiwallet-staff): kendi veritabanı sunucusu, kendi
yöneticisi. Müşterilerin Keycloak'ının yöneticisi çalışan açamıyor, çalışanlarınkinin
yöneticisi müşteriye dokunamıyor. Çalışanın girişi ve paneli yalnızca iç ağdan
erişiliyor. Kayıt sayfası yok, çalışanı panelden bir yönetici davet ediyor ve girişte tek
kullanımlık kod (TOTP) zorunlu. Giriş ve kodun kurulumu Keycloak'ın sayfasında, müşterininkiyle aynı
HiWallet temasıyla. Çalışan backoffice panelinden giriyor; backoffice-bff oturumdaki
çalışan token'ını iç servise iletiyor.
tarayıcı ──cookie──▶ backoffice-bff ──çalışanın token'ı──▶ wallet-api / orchestrator
izni yoksa reddeder issuer'a göre doğrular, izni kontrol eder
İç servisler iki Keycloak'ın token'ını da kabul ediyor; token'ın hangisinden geldiğini
onu doğrulayan şema söylüyor, token'ın içeriği değil. Varsayılan politika çalışanı
dışarıda bırakıyor: yeni bir uç kendiliğinden çalışana kapalı. Çalışanın yetkisi izinle
kontrol ediliyor: customer.view ile her müşterinin kaydını görüntüleyebiliyor, üyelik
aranmıyor. Müşterinin para hareketi başlatan uçları çalışana kapalı; çalışanın yazma
işleri kendi uçlarında, kendi iznine bağlı ve ledger'a çalışanın aktörüyle düşüyor. Müşterinin ön API'leri çalışanların Keycloak'ını tanımıyor.
İzinler kodda ve sabit (StaffPermissions): müşteri kaydını görüntüleme, çekim incelemesi,
personel promo'su, kampanyaları görüntüleme ve yönetme, işyerinin promo kabulü, personel
yönetimi. Roller panelde tanımlanıyor; rol bir izin seti, çalışan bir ya da birden fazla
rol alıyor. Roller, çalışanlar ve atamalar staff-admin'in veritabanında. Keycloak'ta
yalnızca kullanıcı, parola, OTP ve oturum var; çalışanın token'ı kim olduğunu söylüyor,
izin taşımıyor.
panel ──cookie──▶ backoffice-bff ──çalışanın token'ı──▶ staff-admin ──servis hesabı──▶ hiwallet-staff-keycloak
│ kullanıcı, parola, OTP
└──▶ staff-admin-postgres
çalışanlar, roller, atamalar, kayıt
wallet-api, orchestrator ──GET /v1/me, çalışanın token'ıyla──▶ staff-admin
Çalışanın izni her istekte o anki haliyle okunuyor: wallet-api ve orchestrator çalışan
izni isteyen her uçta staff-admin'in GET /v1/me ucuna soruyor, çalışanın kendi
token'ıyla; staff-admin kendi uçlarında veritabanına bakıyor. Önbellek yok: rolü alınan
ya da kapatılan çalışanın aynı token'la gelen bir sonraki isteği reddediliyor. staff-admin'e
ulaşılamazsa çalışanın isteği 503 alıyor; müşterinin istekleri etkilenmiyor. Panel menüyü
ve düğmeleri aynı uçtan okuyor ve bir istek 403 alınca yeniden soruyor.
staff-admin yalnızca çalışanların Keycloak'ının token'ını tanıyor; GET /v1/me dışındaki
her ucu staff.manage istiyor. Çalışan kendine yetki veremiyor: kendi rollerini, sahip
olduğu rolün izinlerini değiştiremiyor ve kendini kapatamıyor. Yeni çalışan davetle
geliyor; parolasını ve OTP'sini davetteki bağlantıdan kendisi kuruyor, ilk girişiyle davet
tamamlanmış sayılıyor. Her değişiklik işi yapan çalışanla, değişiklikle aynı transaction'da
kayda yazılıyor. Açılışta servis yönetici rolünü kuruyor; kimsede personel yönetimi yoksa
ayardaki adrese ilk yönetici daveti gönderiyor. Keycloak'ın konsoluna personel işi için
girilmiyor; konsolda açılan kullanıcının hiçbir izni yok.
wallet-api ile orchestrator'ın ayrı durmasının sebebi madde 7: orchestrator'ın kendi
veritabanı ve kendi sınırı var.
Kaydı onboarding yürütüyor; ön API'ler kayıt uçlarını kimliksiz, doğrulama uçlarını
müşterinin token'ıyla ona iletiyor. Keycloak'ta kendi kendine kayıt kapalı: müşterinin
kullanıcısını onboarding açıyor, kendi istemcisinin servis hesabıyla yönetim API'sinden.
Giriş yine Keycloak'ın sayfasında, HiWallet temasıyla.
kayıt: e-posta ──▶ kod (e-postayla) ──▶ parola ──▶ Keycloak'ta kullanıcı ──▶ wallet'ta hesap (Unknown)
giriş: Keycloak'ın sayfası, e-posta dolu
temel: telefon (SMS kodu) ──▶ kimlik (nüfus kaydı) ──▶ sözleşme ve aydınlatma metni ──▶ Unverified
E-posta parola sorulmadan doğrulanıyor; parola yalnızca kullanıcıyı açan istekte geçiyor,
hiçbir yerde saklanmıyor. Kişisel veri (e-posta, telefon, TCKN, doğum tarihi, onaylar)
onboarding'in kendi Postgres sunucusunda; wallet yalnızca sonucu, hesabın seviyesini
biliyor. Bireysel hesabı ve seviyeyi yalnızca onboarding değiştirebiliyor: wallet-api'de
bu uçlar token'ın azp'sinde onboarding'in istemcisini arıyor.
| seviye | nasıl | ayda ne kadar |
|---|---|---|
Unknown |
kayıt tamamlandı | hiçbir hareket |
Unverified |
telefon, kimlik, onaylar | gelen transfer ve işyerine ödeme; giden transfer ve çekim yok |
Verified |
kendi banka hesabından ilk havale | hepsi, orta limit |
Contracted |
uzaktan kimlik tespiti ya da fiziksel sözleşme | hepsi, en yüksek limit |
Tutarlar wallet-api'nin (transfer, ödeme) ve wallet-consumer'ın (çekim) ayarında. Seviye
yalnızca yükseliyor. Verified ve Contracted'a geçiş henüz yok.
Akışı sağlayıcı başlatıyor. Müşteri kartıyla ödeme yapıyor ya da banka hesabımıza havale gönderiyor; parayı alan kurum bunu bize webhook ile bildiriyor. İlk temas o webhook.
Compose'da bu bildirimi sahte kurumlar üretiyor: POST :8096/v1/topups (stripe-fake)
ya da POST :8094/v1/topups (bank-fake). İkisi de arkadan topup-webhook'a imzalı
webhook gönderiyor — gerçek kurumun yapacağı çağrının aynısı.
sequenceDiagram
participant P as Sağlayıcı
participant H as topup-webhook
participant R as relay
participant MQ as hiwallet.topups
participant C as wallet-consumer
P->>H: POST /v1/webhooks/topup/stripe-fake
Note right of H: HAM gövde üzerinde HMAC,<br/>parse ETMEDEN önce.<br/>INSERT topup_inbox —<br/>provider + event_id UNIQUE
H-->>P: 202 Accepted
Note over P,H: Söz: kalıcı kaydettim
loop her tur
R->>R: SELECT FOR UPDATE SKIP LOCKED
R->>MQ: publish, routing key = cüzdan id
R->>R: işlendi olarak işaretle
end
Note right of R: ÖNCE publish, SONRA işaretle.<br/>Ters sıra kayıp üretir.<br/>Relay tek instance — advisory lock
MQ->>C: p0 .. p3, x-consistent-hash
Note right of C: prefetch=1, x-single-active-consumer.<br/>processed_events + ledger<br/>AYNI transaction'da
relay, topup-webhook'un içinde koşan bir BackgroundService. Diyagramda ayrı
çizilmesinin sebebi akışın orada ikiye ayrılması: HTTP
request'i inbox'a yazıldığında 202 ile bitiyor, yayın ise relay'in sonraki turunda
ve ayrı bir transaction'da oluyor.
Routing key cüzdan kimliği: aynı cüzdanın mesajları hep aynı partition'a düşüyor ve sıra orada korunuyor. Bu yüzden relay tek instance koşuyor — iki relay ayrı batch'leri farklı hızda yayınlarsa mesajlar exchange'e ters sırada varır ve kuyruk içi sıra garantisi bunu düzeltmez (madde 30).
İki kademe idempotency var: inbox'ta (provider, event_id) UNIQUE, tüketicide
processed_events. İkincisi ledger yazımıyla aynı transaction'da.
Akışı müşteri başlatıyor: POST /v1/withdrawals, withdrawal-orchestrator
üzerinde, Idempotency-Key başlığı zorunlu. Orchestrator saga satırını ve ilk komutu
aynı transaction'da yazıp 202 dönüyor; o an hiçbir para hareket etmiyor. Geri kalan
adımların hepsi kuyruk üzerinden, müşteri beklemeden ilerliyor.
sequenceDiagram
participant U as İstemci
participant O as withdrawal-orchestrator
participant W as wallet-consumer
participant A as bank-adapter
participant B as bank-fake
participant H as bank-webhook
U->>O: POST /v1/withdrawals<br/>(Idempotency-Key zorunlu)
Note over O: saga + outbox<br/>AYNI transaction'da
O-->>U: 202, saga initiated
O->>W: DebitForWithdrawal
Note over W: cüzdan −102<br/>clearing +100<br/>revenue +2
W->>O: WithdrawalDebited
O->>A: StartBankTransfer
A->>B: POST /v1/transfers<br/>(Idempotency-Key = CommandId)
B-->>A: 202 pending + bankReference
Note over A: bank_transfers = pending<br/>cevap sonucu öğrenince
Note over O: saga bank_transfer_pending'de<br/>callback'i bekliyor
B->>H: callback + HMAC
H-->>B: 202 (inbox'a yazıldı)
Note over H,A: tek bağ veritabanı,<br/>relay adaptörde
alt banka kabul etti
A->>O: BankTransferSucceeded
O->>W: SettleWithdrawal
Note over W: clearing −100<br/>nostro +100
W->>O: WithdrawalSettled
Note over O: completed
else banka reddetti
A->>O: BankTransferFailed
O->>W: RefundWithdrawal
Note over W: ters kayıt üç bacaklı:<br/>cüzdan, clearing, revenue
W->>O: WithdrawalRefunded
Note over O: failed
end
Durumlar: initiated → debited → bank_transfer_pending → settling → completed.
Telafi yolu: debited → compensating → failed. rejected terminal.
Tutarı inceleme eşiğinin üstündeki çekim (Withdrawals:Review:Above, TRY için 10.000)
düşüldükten sonra bankaya gitmiyor: debited → under_review. Operasyon rolünden bir
çalışan serbest bırakıyor (under_review → bank_transfer_pending, banka komutu o anda
üretiliyor) ya da iptal ediyor (under_review → cancelling → cancelled; ters kaydın
aktörü iptal eden çalışan). İncelemedeki saga takılmış saga taramasına girmiyor.
StartBankTransfer, kuyruktan geçen bir komut mesajı (Shared.Contracts;
alanları CommandId, SagaId, Amount, Currency, DestinationIban). Komutu
saga'nın durum değişimi üretiyor: wallet parayı düşüp WithdrawalDebited dönünce
saga debited'a geçiyor ve orchestrator komutu withdrawal_outbox'a bu geçişle
aynı transaction'da yazıyor. Outbox relay'i satırı hiwallet.withdrawals
exchange'ine StartBankTransfer routing key'iyle yayınlıyor;
hiwallet.withdrawals.bank kuyruğundan bank-adapter tüketiyor. Diğer komutlar da
(DebitForWithdrawal, SettleWithdrawal, RefundWithdrawal) aynı yoldan gidiyor.
Saga bank_transfer_pending durumunda bekliyor. Banka transferi kabul ettiğini
202 ile söylüyor, sonucu callback ile sonra bildiriyor. Bekleme süresi bankanın
işleme hızı kadar; takılmış saga taraması (madde 33) bu durumda kalan saga'ları
izliyor.
Kaçırılan callback'leri mutabakat taraması topluyor. bank-adapter,
StaleAfter süresinden uzundur cevapsız kalan transferleri bankaya soruyor. Sonuçların
tamamına yakını callback ile geliyor; taramanın bulduğu satır sayısı callback hattının
sağlık göstergesi (madde 35).
Ters kayıt orijinal işlemin bacakları okunup negatiflenerek yazılıyor. Üçü de
geri dönüyor: cüzdan, clearing ve revenue. revenue bacağı müşterinin ödediği
komisyonu iade ediyor ve bu iade koşulsuz (CLAUDE.md, "Withdrawal saga").
Response'ların hepsi saklanıyor (processed_messages, bank_transfers). Aynı
mesaj ikinci kez teslim edildiğinde saklanan response yeniden yayınlanıyor ve saga
ilerlemeye devam ediyor.
Her işlemde bacakların toplamı sıfır. Beş hesap rolü var: ikisi "iddia", üçü "gerçekleşmiş".
flowchart LR
subgraph claim["iddia — henüz banka hareketi yok"]
wallet["<b>user_wallet</b><br/>müşteriye borcumuz"]
clearing["<b>clearing</b><br/>sağlayıcıyla<br/>açık hesap"]
end
subgraph real["gerçekleşmiş"]
nostro["<b>nostro</b><br/>bankadaki paramız"]
revenue["<b>revenue</b><br/>gelirimiz"]
expense["<b>provider_expense</b><br/>giderimiz"]
end
claim -.->|settlement<br/>iddiayı gerçeğe çevirir| real
Her işlem tipinin yazdığı bacaklar — toplamı her satırda sıfır:
| işlem | bacaklar |
|---|---|
topup |
wallet +100, clearing −100 |
p2p |
gönderen −100, alan +100 |
payment |
gönderen −102, alan +100, revenue +2 |
payment (promo ile) |
gönderen promo −40, gönderen cash −62, alan cash +100, revenue +2 |
promo_grant (işyeri) |
işyeri cash −40, müşteri promo +40 |
promo_grant (kampanya) |
promo_expense −10, müşteri promo +10 |
promo_expiry (işyeri fonlu) |
müşteri promo −kalan, işyeri cash +kalan |
promo_expiry (platform fonlu) |
müşteri promo −kalan, promo_breakage +kalan |
withdrawal |
cüzdan −102, clearing +100, revenue +2 |
refund |
orijinalin bacakları negatiflenerek — üçü de |
settlement (top-up) |
clearing +gross, nostro −net; Net modelde ayrıca provider_expense −fee |
settlement (çekim) |
clearing −owed, nostro +owed |
provider_invoice |
provider_expense −tutar, nostro +tutar |
İşaret konvansiyonu: credit +, debit −, hiçbir yerde tersine çevrilmiyor.
nostro bir varlık hesabı ve bu ledger'da varlıklar negatif duruyor: −97.1,
bankada 97.1 olduğunu gösteriyor.
revenue ile provider_expense netleştirilmiyor: biri müşteriden aldığımız,
diğeri sağlayıcıya ödediğimiz. Ayrı hesaplar.
nostro para birimi başına tek: ödeme sağlayıcısının nostro'su yok, çünkü nostro
bir banka hesabı. Stripe parayı bizim banka hesabımıza yatırıyor.
| iş | nerede | varsayılan | ne yapıyor |
|---|---|---|---|
| takılmış saga taraması | orchestrator | 5 dk | iki veritabanı arasında asılı kalan çekimi yakalıyor |
| banka mutabakatı | bank-adapter | 4 saat | callback'i kaçırılmış transferi bankaya sorup kapatıyor |
| mutabakat | wallet-consumer | 6 saat | projeksiyon sapması, gelmeyen settlement, geciken fatura |
| işletme günlük özeti | wallet-consumer | 1 saat | hacim, işlem sayısı, kesilen komisyon |
Dördü de pg_try_advisory_lock ile tek instance'a kilitleniyor ve ilk turunu bir
aralık sonra koşuyor; dağıtımda ayağa kalkan instance'lar aynı anda tarama
başlatmıyor.
İki tarama da bir kararın zorunlu tamamlayıcısı: takılmış saga taraması ayrı orchestrator veritabanının (madde 33), banka mutabakatı asenkron banka sonucunun (madde 35). Banka mutabakatının aralığı ayarlanabiliyor, kendisi her kurulumda koşuyor. Kapsamları farklı: biri iki veritabanımız arasındaki ayrışmaya bakıyor, öbürü bizimle banka arasındakine.