Skip to content

Latest commit

 

History

History
507 lines (409 loc) · 24.5 KB

File metadata and controls

507 lines (409 loc) · 24.5 KB

Mimari — kim ne yapıyor, veri nereye gidiyor

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.


1. Topoloji: on üç uygulama, altı veritabanı, bir broker

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
Loading

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

Ön API'ler

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

Kimlik

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.

Personel yönetimi

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

Kayıt ve doğrulama

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.


2. Top-up: para dışarıdan giriyor

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
Loading

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.


3. Çekim: para dışarı çıkıyor

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
Loading

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.


4. Para nerede duruyor

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
Loading

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.


5. Zamanlanmış işler

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.