Her endpoint için request ve beklenen response. Elle denemek ve bir şeyin bozulduğunu anlamak için; sözleşmenin kaynağı kod, bu dosya ona uyar.
Response'lar compose'da koşan sistemden alındı (localhost:8091-8096). Kimlikler her
koşuda değişir.
Adresler ve docker compose komutları stack'in koştuğu makineye ait; hepsi orada
çalışır.
set -a; . ./.env; set +a # webhook secret'ları kabuğa gelsinDurum kodları neden bu şekilde: 201 yaratıldı, 202 kalıcı olarak alındı ama
henüz işlenmedi, 400 girdi bozuk, 404 kayıt yok, 409 eşzamanlılık çakışması,
422 request geçerli ama iş kuralı reddetti. 409 ile 422 karıştırılmaz — birincisi
"tekrar dene", ikincisi "tekrar denemenin faydası yok".
curl -i -X POST localhost:8091/v1/accounts \
-H 'Content-Type: application/json' \
-d '{"type":"Person"}'HTTP/1.1 201 Created
Location: http://localhost:8091/v1/accounts/d5e9df14-cd62-4ad5-b628-08df56e06daa
{
"accountId": "d5e9df14-cd62-4ad5-b628-08df56e06daa",
"type": "Person",
"createdAt": "2026-09-06T12:41:03.117421+00:00"
}type: Person | Business. Sayı değil isim gönderilir — sayı olsaydı enum'a yeni
değer eklemek mevcut istemcilerin anlamını kaydırırdı.
Hesap para tutmaz. Bir hesabın aynı para biriminde birden fazla cüzdanı olabilir
(decisions.md madde 20); günlük limit bu yüzden cüzdan değil hesap bazında
uygulanır.
Geçersiz tip → 400
curl -i -X POST localhost:8091/v1/accounts \
-H 'Content-Type: application/json' -d '{"type":"Robot"}'HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
curl -i -X POST localhost:8091/v1/accounts/$ACCOUNT/wallets \
-H 'Content-Type: application/json' \
-d '{"name":"Birikim","currency":"TRY"}'HTTP/1.1 201 Created
Location: http://localhost:8091/v1/wallets/23948ca4-7c69-4b0a-b8a1-eccdc87876a4
{
"walletId": "23948ca4-7c69-4b0a-b8a1-eccdc87876a4",
"accountId": "d5e9df14-cd62-4ad5-b628-08df56e06daa",
"name": "Birikim",
"currency": "TRY",
"balance": 0.0000,
"withdrawable": 0.0000,
"balances": [
{ "fundType": "cash", "balance": 0.0000 },
{ "fundType": "card", "balance": 0.0000 },
{ "fundType": "promo", "balance": 0.0000 }
]
}Açılışta üç kova da sıfır. Response'un şekli sorguyla aynı; kovalar açılışta gizlenip sonra ortaya çıkmıyor.
name zorunlu: aynı hesabın aynı para birimindeki cüzdanları başka türlü ayırt
edilemiyor. Bakiye her zaman 0 başlar — para yalnızca ledger üzerinden girer.
Olmayan hesap → 404
{
"type": "https://hiwallet.dev/problems/account-not-found",
"title": "Hesap bulunamadı",
"status": 404,
"detail": "Hesap bulunamadı: 00000000-...",
"traceId": "..."
}Sistem hesabı olmayan para birimi → 422
curl -i -X POST localhost:8091/v1/accounts/$ACCOUNT/wallets \
-H 'Content-Type: application/json' -d '{"name":"Dolar","currency":"USD"}'{
"type": "https://hiwallet.dev/problems/business-rule",
"title": "İşlem iş kuralı gereği reddedildi",
"status": 422,
"detail": "USD için sistem hesapları açılmamış; ... Desteklenen: TRY.",
"rule": "unsupported_currency"
}400 değil 422: USD geçerli bir ISO 4217 kodu, request kusursuz. Reddin sebebi
o para biriminde clearing ve revenue hesaplarının seed edilmemiş olması — bu
ledger'ın bilgisi, sınırdaki doğrulayıcı bilemez.
Boş ad → 400
{
"status": 400,
"errors": { "Name": ["Cüzdan adı zorunlu."] }
}curl -s localhost:8091/v1/wallets/$WALLET{
"walletId": "23948ca4-7c69-4b0a-b8a1-eccdc87876a4",
"accountId": "d5e9df14-cd62-4ad5-b628-08df56e06daa",
"name": "Birikim",
"currency": "TRY",
"balance": 500.0000,
"withdrawable": 300.0000,
"balances": [
{ "fundType": "cash", "balance": 300.0000 },
{ "fundType": "card", "balance": 150.0000 },
{ "fundType": "promo", "balance": 50.0000 }
]
}Bakiye ledger_balances projeksiyonundan okunur, ledger_entries toplanarak değil.
balance kovaların toplamı, withdrawable IBAN'a çıkabilen kısım
(decisions.md madde 36). Yukarıdaki cüzdanda 500 TRY var ama çekime açık olan
300; kalan 150 kart ile yüklendiği, 50 de hediye bakiye olduğu için nakde
çevrilemiyor. İkisi ayrı dönüyor, yoksa müşteri çekimin neden reddedildiğini
göremezdi.
Kovalar sıfır olsalar da listede duruyor — yeni açılmış bir cüzdanda üçü de
0 döner.
Sistem hesabı bu endpoint'ten görünmez. revenue ya da clearing kimliğiyle sorarsan
404 dönerler — aynı tabloda duruyorlar ama iç muhasebe, public API'nin
cevaplayacağı soru değil.
curl -s "localhost:8091/v1/wallets/$WALLET/movements?size=2"{
"items": [
{
"movementId": 918274,
"transactionId": "6b1f0c2e-...",
"type": "withdrawal",
"amount": -206.0000,
"currency": "TRY",
"fundType": "cash",
"createdAt": "2026-09-22T14:10:55.201Z"
},
{
"movementId": 918270,
"transactionId": "a4c81d55-...",
"type": "p2p",
"amount": -102.0000,
"currency": "TRY",
"fundType": "cash",
"createdAt": "2026-09-22T13:02:11.884Z"
}
],
"size": 2,
"nextCursor": 918270
}Kaynak ledger_entries — bakiye projeksiyonu değil, hareketin kendisi. amount
yön taşır: cüzdana giren +, çıkan -. Transferde gönderen tek satır görür ve o
satır komisyon dahil toplamı gösterir (yukarıda 100 + 2).
Sayfalama cursor ile. Bir sonraki sayfa nextCursor'ı after olarak
göndererek alınır:
curl -s "localhost:8091/v1/wallets/$WALLET/movements?size=2&after=918270"Son sayfada nextCursor null döner; istemci listenin bittiğini buradan anlar ve
ayrıca bir toplam sayı sorgusu koşulmaz.
Offset yerine cursor seçildi çünkü ledger append-only ve yeni satırlar listenin
başına giriyor. OFFSET ile iki sayfa arasında gelen bir top-up sayfayı
kaydırır, müşteri aynı kaydı iki kez görürdü. Ayrıca derin sayfada OFFSET
Postgres'e okunup atılacak satır saydırıyor; cursor ix_ledger_entries_movements
üzerinde tek arama yapıyor.
size tavanı 100. Daha büyüğü isteyen request reddedilmiyor, tavana çekiliyor ve
response'taki size gerçekte uygulanan değeri söylüyor.
Sistem hesapları bu endpoint'ten de görünmez: revenue kimliğiyle sorarsan 404.
curl -s localhost:8091/v1/accounts/$ACCOUNT{
"accountId": "d5e9df14-cd62-4ad5-b628-08df56e06daa",
"type": "Person",
"createdAt": "2026-09-06T12:41:03.117421+00:00",
"wallets": [
{
"walletId": "23948ca4-...",
"name": "Birikim",
"currency": "TRY",
"balance": 398.0000,
"withdrawable": 198.0000,
"balances": [
{ "fundType": "cash", "balance": 198.0000 },
{ "fundType": "card", "balance": 150.0000 },
{ "fundType": "promo", "balance": 50.0000 }
]
},
{
"walletId": "7c1e0b22-...",
"name": "Harcama",
"currency": "TRY",
"balance": 0.0000,
"withdrawable": 0.0000,
"balances": [
{ "fundType": "cash", "balance": 0.0000 },
{ "fundType": "card", "balance": 0.0000 },
{ "fundType": "promo", "balance": 0.0000 }
]
}
]
}Kırılım liste görünümünde de var: "neden çekemiyorum" sorusunun cevabı tek cüzdana girmeden görünüyor.
curl -i -X POST localhost:8091/v1/transfers \
-H 'Idempotency-Key: transfer-1' -H 'Content-Type: application/json' \
-d "{\"fromWalletId\":\"$FROM\",\"toWalletId\":\"$TO\",\"amount\":200,\"currency\":\"TRY\",\"type\":\"Payment\"}"HTTP/1.1 201 Created
{ "transactionId": "...", "replayed": false }type: P2P | Payment | P2B | B2P | B2B. Komisyon istenen tutara ek
olarak gönderenden düşülür: Payment %2 ise gönderen -204, alan +200,
revenue +4. Ledger'a üç satır düşer, toplamı sıfır.
Idempotency-Key ZORUNLU; başlık yoksa 400 ve ledger'a hiçbir şey yazılmaz
(decisions.md madde 4). Anahtarsız bir tekrar hiçbir constraint'e takılmaz ve çift
harcama sessizce ledger'a düşerdi; append-only olduğu için de geri alınamaz, yalnızca
ters kayıtla düzeltilir.
Aynı anahtarla ikinci request yeni transfer yapmaz:
{ "transactionId": "aynı-kimlik", "replayed": true }Yetersiz bakiye → 422
{ "status": 422, "rule": "insufficient_funds", "traceId": "..." }Limit aşımında rule limitin adını taşır (per_transaction, daily).
Eşzamanlılık çakışmasında ise 409 döner ve rule yoktur — o bir iş kuralı reddi
değil, "tekrar dene" demek.
Dışarıdan para girişi. Sağlayıcı rolünü sen oynuyorsun: gövdeyi imzalayıp gönderiyorsun.
BODY="{\"eventId\":\"evt_1\",\"walletId\":\"$WALLET\",\"amount\":500.00,\"currency\":\"TRY\",\"reference\":\"pi_1\",\"occurredAt\":\"2026-09-06T10:00:00+00:00\"}"
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$STRIPE_FAKE_WEBHOOK_SECRET" -hex | awk '{print $2}')
curl -i -X POST localhost:8092/v1/webhooks/topup/stripe-fake \
-H 'Content-Type: application/json' \
-H "X-Hive-Signature: sha256=$SIG" \
--data "$BODY"HTTP/1.1 202 Accepted
{ "accepted": true, "duplicate": false }200 değil 202, bilerek. Verilen söz "işledim" değil "kalıcı kaydettim".
Response döndüğünde para henüz cüzdanda yok; hat webhook → inbox → relay → RabbitMQ →
wallet-consumer → ledger. Birkaç saniye sonra bakiyeye bak.
İmza ham gövde baytları üzerinde HMAC-SHA256. Gövdeyi yeniden serialize edersen (boşluk, alan sırası) imza tutmaz.
Sağlayıcılar: stripe-fake, bank-fake — her birinin kendi secret'ı var.
Tekrar eden event → yine 202
Aynı eventId ile ikinci request:
{ "accepted": true, "duplicate": true }Sağlayıcı için yeniden gönderim başarılı bir sonuçtur; hata dönmek onu sonsuza
kadar tekrar ettirirdi. Ayrım gövdedeki duplicate alanında.
Geçersiz imza / eksik başlık / tanınmayan sağlayıcı → 401
Üçü de 401. Tanınmayan sağlayıcıya 404 DÖNÜLMEZ — hangi sağlayıcıların tanımlı
olduğunu dışarıya söylemek istemiyoruz.
curl -i -X POST localhost:8093/v1/withdrawals \
-H 'Idempotency-Key: cekim-1' -H 'Content-Type: application/json' \
-d "{\"accountId\":\"$ACCOUNT\",\"walletId\":\"$WALLET\",\"amount\":100,\"currency\":\"TRY\",\"destinationIban\":\"TR330006100519786457841326\"}"HTTP/1.1 202 Accepted
Location: http://localhost:8093/v1/withdrawals/cf13827f-470c-43af-a3b7-e3606e48c31d
{
"withdrawalId": "cf13827f-470c-43af-a3b7-e3606e48c31d",
"state": "initiated",
"replayed": false
}Idempotency-Key ZORUNLU — transfer'de olduğu gibi. Burada bahis daha da yüksek:
çekim çok adımlı ve dışarıya para çıkarıyor, anahtarsız bir tekrar ikinci bir banka
transferi başlatırdı. Başlık yoksa 400.
202 dönüldüğünde hiçbir para hareket etmedi. IBAN boşluklu yazılabilir,
normalize edilir; mod-97 geçmezse 400.
curl -s localhost:8093/v1/withdrawals/$WD{
"withdrawalId": "cf13827f-470c-43af-a3b7-e3606e48c31d",
"state": "completed",
"amount": 100.0000,
"currency": "TRY",
"destinationIban": "TR33******************1326",
"totalDebited": 102.0000,
"failureReason": null,
"createdAt": "2026-09-06T12:54:21.993862+00:00",
"updatedAt": "2026-09-06T12:54:23.2473+00:00"
}state sırası: initiated → debited → bank_transfer_pending → settling →
completed. settling, para bankadan çıktıktan sonra iç muhasebenin kapanmasını
bekliyor (clearing boşalıp nostro'ya yazılıyor) ve saniyeler sürüyor.
Telafi yolunda: debited → compensating → failed. Reddedilmişse rejected.
totalDebited cüzdandan gerçekte çıkan toplam (tutar + komisyon). Wallet düşmeyi
yapana kadar null — 0 yazılmıyor, "komisyonsuz çekildi" ile karışırdı.
IBAN maskeli döner: müşteri zaten kendi girdi, tam hali response'ta dolaşınca log'a, hata izlemeye ve tarayıcı geçmişine de düşer.
Aynı anahtarla tekrar → 202, replayed: true
{ "withdrawalId": "aynı-kimlik", "state": "completed", "replayed": true }Yeni çekim AÇILMADI.
Yetersiz bakiye / limit aşımı → saga rejected
POST yine 202 döner — request geçerliydi ve kalıcı olarak alındı. Ret sonradan
ortaya çıkıyor:
{
"state": "rejected",
"failureReason": "Cüzdan 2394... 50,00 TRY tutuyor, 102,00 TRY çekilemez.",
"totalDebited": null
}failureReason domain'in mesajı, makine tarafından ayrıştırılacak bir kod değil.
Kod isteyen bir istemci çıkarsa event'e ayrı bir alan eklenir.
Bu durum dead-letter'a GİTMEZ: cevapsız kalan saga müşteriyi sonsuza kadar "işleniyor"da bırakırdı.
Bankanın API'sinin yerinde duran servis; canlıda yok (decisions.md madde 35).
Senaryo endpoint'i gerçek bir bankada bulunmaz — varlık sebebi "banka reddetti" durumunun
denenebilmesi.
Transfer sonucu artık senkron dönmüyor. POST /v1/transfers 202 pending
veriyor, kesin sonuç callback ile ya da GET /v1/transfers/{ref} ile sonra
öğreniliyor. Bu yüzden çekim saga'sı gerçekten bank_transfer_pending'de bekliyor.
curl -i -X POST localhost:8094/v1/scenarios \
-H 'Content-Type: application/json' \
-d "{\"clientReference\":\"$WD\",\"outcome\":\"Failure\"}"HTTP/1.1 204 No Content
outcome: Success | Failure | TransientFailure | DelayedSuccess.
TransientFailure için transientFailures (1-10, varsayılan 1) kaç kez geçici hata
üretileceğini, DelayedSuccess için delayMilliseconds (≤30000) gecikmeyi belirler.
TransientFailure ile Failure arasındaki fark kritik: birincisinde banka 503
dönüyor ve transfer hiç açılmıyor (adaptör yeniden deniyor, saga bekliyor),
ikincisinde transfer açılıyor ama sonucu başarısız (saga telafiye giriyor).
Senaryo çekim başına kuruluyor ve anahtarı clientReference — bizim saga
kimliğimiz. Yani çekimi başlattıktan sonra kurman gerekiyor ve bu bir yarış:
zincir seni beklemiyor, banka senaryoyu transfer request'i geldiği anda okuyor.
Çekim request'inin hemen ardından aynı betikte kurarsan genelde yetişirsin; elle
kopyalayıp yapıştırırken geç kalırsın. Garantili yol varsayılanı değiştirmek:
BANK_DEFAULT_OUTCOME=Failure docker compose up -d --force-recreate --no-deps bank-fake
docker compose exec bank-fake printenv BankFake__DefaultOutcomeGeri almak için aynı komutu değişkensiz çalıştır.
Senaryolar ve transferler bellekte — sahte bankanın veritabanı yok. Yukarıdaki
gibi container'ı yeniden yaratmak hepsini siler. O anda bank_transfer_pending'de
bekleyen bir çekim varsa kapanmaz: mutabakat taraması sorduğunda banka onu artık
tanımıyor (404). Önce bekleyen çekimlerin bitmesini bekle.
curl -s localhost:8094/v1/scenarios/$WD{
"clientReference": "...",
"outcome": "TransientFailure",
"remainingTransientFailures": 0,
"attempts": 2
}attempts retry'ın gerçekten çalıştığının kanıtı. Kurulmamış çekim için 404.
Bunları elle çağırman gerekmiyor; bank-adapter çağırıyor. Burada duruyorlar çünkü
gerçek entegrasyonda bankanın dokümanından yazılacak kısım tam olarak bu ikisi.
curl -i -X POST localhost:8094/v1/transfers \
-H 'Content-Type: application/json' -H "Idempotency-Key: $(uuidgen)" \
-d '{"clientReference":"<sagaId>","amount":100,"currency":"TRY","destinationIban":"TR330006100519786457841326"}'HTTP/1.1 202 Accepted
{ "bankReference": "BNK4F2A9C1E8B7D6A3", "status": "pending", "replayed": false }status her zaman pending. Banka "aldım" diyor, "gönderdim" demiyor. Sonuç
callback ile ya da durum sorgusuyla sonra geliyor (decisions.md madde 35).
curl -s localhost:8094/v1/transfers/BNK4F2A9C1E8B7D6A3{
"bankReference": "BNK4F2A9C1E8B7D6A3",
"clientReference": "...",
"status": "succeeded",
"amount": 100.0000,
"fee": 1.5000,
"currency": "TRY",
"failureReason": null,
"acceptedAt": "..."
}Mutabakat taramasının okuduğu endpoint bu. Bankanın böyle bir endpoint'i olmasaydı, callback'i kaçırılan transferin sonucunu hiçbir şey öğrenemezdi.
Idempotency-Key başlıksız request 400. Aynı anahtarla ikinci request yeni transfer
AÇMAZ: aynı bankReference ve "replayed": true döner.
TransientFailure senaryosunda endpoint 503 veriyor ve transfer hiç açılmıyor —
kalıcı hatadan farkı bu. Adaptör bunu yeniden deniyor, saga'ya hiçbir şey
bildirilmiyor.
Kart sağlayıcısının yerinde duran servis; canlıda yok. Tek endpoint'i var — Stripe'tan para çıkmadığı için ne transfer endpoint'i var ne callback alıcısı.
Gerçek Stripe'ta bu endpoint YOKTUR: webhook müşteri ödeme yaptığında gelir, sen istediğinde değil.
curl -i -X POST localhost:8096/v1/topups \
-H 'Content-Type: application/json' \
-d "{\"walletId\":\"$WALLET\",\"amount\":100,\"currency\":\"TRY\",\"mode\":\"Normal\"}"HTTP/1.1 202 Accepted
{ "mode": "Normal", "eventCount": 1 }202 çünkü gönderim ARKA PLANDA: dönüldüğünde webhook henüz gitmedi. eventCount
kaç webhook gideceğini söylüyor.
mode |
ne yapar | beklenen |
|---|---|---|
Normal |
tek event | bakiye bir kez artar |
Duplicate |
aynı event iki kez (eventId de aynı) |
bakiye bir kez artar |
Delayed |
tek event, delayMilliseconds sonra |
eventual davranış görünür olur |
OutOfOrder |
aynı cüzdana count event, en yenisi önce |
hepsi iner, bakiye toplama eşit |
Duplicate'in eventId'si bilerek aynı: farklı olsaydı bu iki ayrı para girişi
olurdu, tekrar değil.
OutOfOrder "sıra korunuyor" demiyor — top-up'ta toplama değişmeli. Dediği şey ters
sırada gelen bir dizinin tamamının kabul edildiği; değeri consistent-hash routing'in
hepsini aynı partition'a düşürmesinde.
Aynı endpoint bank-fake'te de var (:8094) ve clearing/bank-fake'e yazıyor — aynı
banka hem gelen havaleyi bildiriyor hem giden transferi kabul ediyor.
Bankanın transfer sonucunu bildirdiği endpoint. Bizim kodumuz, canlıda da koşuyor;
bank-adapter'dan ayrı bir deployable çünkü ingress'i var (decisions.md madde 28).
Elle çağırman gerekmiyor — bank-fake çağırıyor. İmza topup-webhook'unkiyle aynı
algoritma ama ayrı bir sözleşme: başlık adı X-Bank-Signature ve secret
BANK_CALLBACK_SECRET. Orada şemayı biz dayatıyoruz, burada bankanınkini uyguluyoruz.
BODY='{"eventId":"evt-BNK4F2A9C1E8B7D6A3","bankReference":"BNK4F2A9C1E8B7D6A3","clientReference":"...","status":"succeeded","fee":1.50,"currency":"TRY","failureReason":null,"occurredAt":"2026-03-01T10:00:00+00:00"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$BANK_CALLBACK_SECRET" -hex | awk '{print $2}')
curl -i -X POST localhost:8095/v1/webhooks/bank/bank-fake \
-H 'Content-Type: application/json' -H "X-Bank-Signature: sha256=$SIG" --data "$BODY"HTTP/1.1 202 Accepted
{ "accepted": true, "duplicate": false }202, 200 değil: verilen söz "işledim" değil "kalıcı kaydettim". Bu servis
işlemiyor — inbox'a yazıp bırakıyor, transferi kapatmak bank-adapter'daki
relay'in işi.
eventId tekrar denemelerde aynı kalmak zorunda; ikinci kez gelirse yine 202 ama
"duplicate": true ve satır ikinci kez yazılmıyor.
İmza tutmazsa 401 ve inbox'a hiçbir şey yazılmaz. Tanınmayan kurum da 401,
404 değil — hangi bankalarla çalıştığımız dışarıya sızmamalı. eventId yoksa
400: kimliksiz bir bildirim deduplike edilemez.
Banka kalıcı olarak reddettiğinde para üç bacaklı ters kayıtla geri döner:
BEFORE=$(curl -s localhost:8091/v1/wallets/$WALLET | jq -r .balance)
WD=$(curl -s -X POST localhost:8093/v1/withdrawals \
-H 'Idempotency-Key: cekim-red' -H 'Content-Type: application/json' \
-d "{\"accountId\":\"$ACCOUNT\",\"walletId\":\"$WALLET\",\"amount\":100,\"currency\":\"TRY\",\"destinationIban\":\"TR330006100519786457841326\"}" | jq -r .withdrawalId)
# Hemen ardından: zincir bankaya varmadan senaryo kurulmuş olmalı. Yetişmezse
# çekim başarılı biter — o durumda BANK_DEFAULT_OUTCOME=Failure yolunu kullan
# (yukarıda, "Senaryo kur").
curl -s -X POST localhost:8094/v1/scenarios -H 'Content-Type: application/json' \
-d "{\"clientReference\":\"$WD\",\"outcome\":\"Failure\"}"
# Banka sonucu ANINDA vermiyor: BANK_SETTLEMENT_DELAY kadar bekliyor, sonra
# callback gönderiyor, sonra adaptörün relay'i cevabı yayınlıyor.
sleep 10
curl -s localhost:8093/v1/withdrawals/$WD; echo
echo "önce=$BEFORE sonra=$(curl -s localhost:8091/v1/wallets/$WALLET | jq -r .balance)"Beklenen: saga failed, bakiye başladığı yerde.
Ledger'da altı satır — withdrawal üçlüsü ve tam aynası refund üçlüsü:
docker compose exec postgres psql -U postgres -d hiwallet_wallet -c \
"SELECT t.type, la.type AS hesap, e.amount FROM ledger_entries e
JOIN ledger_accounts la ON la.id = e.ledger_account_id
JOIN ledger_transactions t ON t.id = e.transaction_id
WHERE t.correlation_id = '$WD' ORDER BY t.created_at, e.amount;" type | hesap | amount
------------+-------------+-----------
withdrawal | user_wallet | -102.0000
withdrawal | revenue | 2.0000
withdrawal | clearing | 100.0000
refund | clearing | -100.0000
refund | revenue | -2.0000
refund | user_wallet | 102.0000
refund satırındaki revenue -2 kritik. O bacak atlansaydı kayıt yine dengeli
olurdu, trigger susardı, testler geçerdi — ama müşteri gerçekleşmemiş bir işlemin
komisyonunu ödemiş kalırdı. Ters kayıt bu yüzden politikadan yeniden ÜRETİLMİYOR:
orijinal işlemin bacakları okunup negatifleniyor.
curl -s localhost:8091/health/ready{
"status": "Healthy",
"durationMs": 0.4953,
"checks": [
{ "name": "postgres", "status": "Healthy", "durationMs": 0.4189, "error": null }
]
}checks listesi uygulamanın bağımlılıklarını sayıyor. wallet-api yalnızca
Postgres'e bağlanıyor. Broker bağlantısı wallet-consumer'da; mesajları o çekiyor
ve ledger'a o yazıyor (decisions.md madde 28).
Orchestrator ikisini birden sayıyor:
curl -s localhost:8093/health/ready{
"status": "Healthy",
"durationMs": 2.0093,
"checks": [
{ "name": "postgres", "status": "Healthy", "durationMs": 0.4396, "error": null },
{ "name": "rabbitmq", "status": "Healthy", "durationMs": 1.9646, "error": null }
]
}Broker durdurulduğunda bu endpoint 200 dönmeye devam eder, yalnızca status alanı
Degraded olur. Çekim request'i kabul edilmeye devam ediyor çünkü komut outbox'a
yazılıyor ve broker döndüğünde yayınlanıyor (decisions.md madde 26 ve 32).
Kalan endpoint'ler aynı gövdeyi döndürüyor, yalnızca checks içerikleri farklı:
curl -s localhost:8092/health/ready # topup-webhook — postgres + rabbitmq
curl -s localhost:8094/health/ready # bank-fake — checks BOŞ (canlıda yok)
curl -s localhost:8095/health/ready # bank-webhook — postgres
curl -s localhost:8096/health/ready # stripe-fake — checks BOŞ (canlıda yok)Sahte kurumların checks listesi boş: ikisinin de veritabanı yok, sağlıklı olmaları
process'in ayakta olduğunu söylüyor.
wallet-consumer'ın host'a açılmış portu yok; onun health check'i container'ın
içinden koşuyor ve sonucu docker compose ps çıktısında healthy olarak görünüyor.