Akışlar
İstek Yaşam Döngüsü
Bir istek nerede başlar, hangi durakları geçer, nerede biter? Aşağıda sistemin ana akışlarını baştan sona, adım adım anlatıyoruz. Teknik isimler parantezde duruyor ama anlatım günlük Türkçe — her adımı bilmeniz gerekmez, merak ederseniz derinleşebilirsiniz.
Tekrarlayan desen: Kullanıcının konuştuğu API (api-yos), bir yazma banka işi gerektiğinde (rıza kurma / ödeme gönderme) bunu doğrudan yapmaz. Bunun yerine "şunu yap" diye bir niyet (SyncIntent) olarak veritabanına yazar; arka plandaki batch işçi bu niyeti alıp bankayla konuşur ve sonucu veritabanına yazar. Böylece API kullanıcıya hızlı yanıt verir, asıl iş güvenle arka planda yürür. İstisna: kullanıcı ekranda canlı okuma(fresh=true) istediğinde, niyet kutusu atlanır; API batch'i senkron çağırıp taze veriyi anında döner (bkz. bölüm 4). Okuma başarısız olursa kullanıcı yine dener, o yüzden dayanıklılık (outbox) gerekmez.
1) Genel istek boru hattı (middleware)
Her HTTP isteği, asıl işi yapan controller'a ulaşmadan önce sırayla şu duraklardan geçer. Her durak (middleware) tek bir işten sorumludur ve isteği bir sonrakine devreder:
Correlation ID üret
CorrelationIdMiddleware
Gelen her isteğe, o isteğe özel benzersiz bir kimlik (correlation_id, yani 'takip numarası') verilir. Bu numara isteğin bıraktığı tüm log satırlarında görünür. Bir istekte sorun olduğunda, bu numarayı arayarak isteğin geçtiği her durağı baştan sona takip edebilirsiniz. Kargodaki takip numarası gibi düşünün.
Hata kalkanı
ExceptionHandlingMiddleware
Bu noktadan sonrası bir güvenlik ağıyla sarılır. İlerideki herhangi bir adımda beklenmedik bir hata çıkarsa, hata dışarı taşmadan burada yakalanır. Yakalanan hata, standart bir hata yanıtına (RFC 7807 ProblemDetails formatı — hata kodu, başlık, açıklama ve takip numarası içeren düzenli bir yapı) çevrilir. Böylece kullanıcı çirkin bir çökme yerine anlaşılır bir mesaj alır.
İstek/yanıt logla
RequestLoggingMiddleware
İsteğin yöntemi (GET/POST), yolu, sonuç durumu (200/404 gibi) ve ne kadar sürdüğü kaydedilir. İstek ve yanıt gövdeleri de loglanır, ancak şifre ve token gibi hassas alanlar önce redakte edilir (yıldızlanır) — gizli veri loga düşmez. Loglar düz metin değil yapılandırılmış (JSON) tutulur, böylece sonradan aranıp filtrelenebilir.
Dil çöz
LocaleResolutionMiddleware
İsteğin Accept-Language başlığına bakılarak kullanıcının dili (tr veya en) belirlenir. Başlık yoksa kullanıcının kayıtlı tercih dili, o da yoksa varsayılan Türkçe kullanılır. Bu sayede bir hata olduğunda mesaj kullanıcının anladığı dilde döner.
Kimlik doğrula (JWT)
Authentication
İstekte bir oturum bileti (JWT token — giriş yapınca verilen, kim olduğunuzu kanıtlayan imzalı bir belge) varsa geçerliliği kontrol edilir: imzası doğru mu, süresi dolmuş mu, bu API için mi üretilmiş. Korumalı uçlar (örneğin hesap listesi) geçerli bir token olmadan cevap vermez. Sadece giriş/kayıt gibi açık uçlar token istemez.
Yetki kontrolü
Authorization
Token içinden kullanıcı kimliği (userId) okunur. Bu kimlik olmadan korumalı bir uca ulaşılamaz. Ayrıca her kullanıcı yalnızca kendi verisine erişebilir — başka bir kullanıcının hesabını istemek 404 (bulunamadı) döner, 'bu sizin değil' demek yerine kaydın varlığını bile ele vermeyiz (BOLA koruması).
Girdi doğrula
ValidationFilter
İsteğin gövdesi iş kurallarına göre denetlenir (FluentValidation kütüphanesiyle): IBAN geçerli mi, tutar sıfırdan büyük mü, zorunlu alanlar dolu mu gibi. Bir şey eksik veya hatalıysa istek daha işlem görmeden 400 (hatalı istek) ile geri çevrilir. Yanıtta hangi alanın neden hatalı olduğu tek tek belirtilir.
Tekrar koruması
IdempotencyFilter
Yazma isteklerinde (POST/PUT/DELETE) Idempotency-Key başlığı zorunludur. Bu başlık, aynı isteğin yanlışlıkla iki kez gönderilmesini (örneğin kullanıcı 'Öde' düğmesine iki kez basarsa) yakalar. İkinci çağrı işi tekrar yapmaz; ilk çağrının kaydedilmiş yanıtını aynen döndürür. Böylece bir ödeme iki kez gerçekleşmez.
Controller çalışır
İş mantığı
Tüm bu kontrollerden geçen istek nihayet ilgili servise ulaşır ve asıl iş burada yapılır (hesap okunur, ödeme oluşturulur vb.). İş bitince yanıt hazırlanır ve istek, geldiği duraklardan ters sırada geçerek geri döner (loglama, hata kalkanı yanıtı sarar). Kullanıcıya nihai HTTP yanıtı ulaşır.
2) Banka bağlama / rıza oluşturma
En temel akış: kullanıcıyı bir bankaya bağlamak, yani onun izniyle banka verisine erişim kurmak. İki dünya — hızlı yanıt veren API ile bankayla konuşan batch — burada birlikte çalışır:
Kullanıcı bağlantı başlatır
POST /hhs-connections
Kullanıcı hangi bankaya ve hangi izinlerle (hesap bilgisi / bakiye / işlem geçmişi / kart) bağlanmak istediğini gönderir. Ayrıca onay yönteminin yönlendirmeli mi (redirect — kullanıcı bankanın sayfasına gidip döner) yoksa ayrık mı (decoupled — kullanıcı banka mobil uygulamasından SMS/bildirimle onaylar) olacağı seçilir. Bu istek henüz sadece bir talep; banka bu anda haberdar değildir.
Rıza + niyet tek işlemde yazılır
HhsConnectionService.CreateAsync
Bağlantı kaydı, seçilen izinler ve bankaya gidilmesi gerektiğini belirten bir 'ConsentCreate' niyeti (SyncIntent — 'şunu yap' notu) aynı veritabanı işleminde birlikte yazılır. Tek işlem olması önemli: ya hepsi kaydolur ya hiçbiri, yarım kalmış kayıt oluşmaz. Bu anda bankaya HİÇBİR çağrı yapılmaz — çünkü kullanıcının konuştuğu API (api-yos) bankaya doğrudan hiç bağlanmaz. Rızaya ömür boyu sabit kalacak bir X-Group-ID (rıza grup numarası) atanır.
Batch işçi tetiklenir
Hangfire → SyncIntentConsumerJob
Yazılan niyet için arka plandaki işçiye (batch — bankalarla konuşan ayrı program) anında bir tetikleme sinyali gönderilir. Beklemeden hemen ele alınsın diye tetiklenir; ama sinyal kaybolsa bile işçi kutuyu düzenli tarar, yani niyet asla kaybolmaz. İşçi niyeti alır ve 'işleniyor' olarak işaretler ki başka bir işçi aynı işi ikinci kez yapmasın.
Bankaya rıza çağrısı
ConsentCreateHandler → HHS
İşçi bankaya (HHS — bankanın veri paylaşım sistemi) güvenli, imzalı bir istek atar: mTLS (iki tarafın da sertifikayla kimliğini kanıtladığı şifreli kanal) + JWS (isteğin dijital imzası). Banka bir rıza numarası (rizaNo) ve kullanıcının onaylayacağı bir onay adresi (authorizationUrl) döndürür. Banka isteği reddederse bağlantı 'başarısız' işaretlenir ve akış burada durur.
Onay adresi kullanıcıya döner
Bounded-wait
Yönlendirmeli akışta API, işçinin onay adresini veritabanına yazmasını kısa bir süre bekler (bounded-wait — sınırlı bekleme, sonsuza kadar değil) ve adresi kullanıcıya döndürür. Kullanıcı bu adrese giderek bankanın onay ekranını görür. Ayrık akışta ise adres yerine kullanıcı doğrudan banka uygulamasından işlemi onaylar.
Kullanıcı GKD ekranında onaylar → callback
GET /hhs-connections/callback (redirect) · /{id}/callback (poll)
Kullanıcı bankanın GKD ekranında (Güçlü Kimlik Doğrulama — bankanın kullanıcıyı şifre/SMS/biyometri ile onaylattığı ekran) kimliğini doğrular ve izni verir. İki tamamlama yolu vardır: (1) Yönlendirme modunda banka, API'ye bir geri dönüş (callback) yapar — GET /hhs-connections/callback — ve içinde onay durumu + yetki kodu (yetKod) gelir. (2) Ayrık modda banka geri yönlendirmez; uygulama GET /hhs-connections/{id}/callback ucunu çağırarak bağlantının tamamlanıp tamamlanmadığını yoklar (yetKod bankadan sorulur). Her iki yol da aynı aktivasyon adımına bağlanır.
Aktivasyon niyeti yazılır
HandleCallbackAsync
Gelen callback, yetki kodunu da içeren bir 'AuthorizationComplete' niyeti oluşturur. Bu niyet yine niyet kutusuna yazılır ve işi batch'e devreder. API burada da bankayla konuşmaz; sadece 'onay geldi, şimdi token'a çevir' notunu bırakır.
Token alınır, bağlantı 'bağlı' olur
ConsentActivationService
İşçi, elindeki yetki kodunu bankada gerçek bir erişim token'ına (token — bankadan alınan, kullanıcı adına veri çekme yetkisi veren dijital anahtar) çevirir. Token şifrelenerek saklanır (düz metin olarak asla tutulmaz). Bağlantının durumu artık 'Connected' (bağlı) olur — yani kullanıcı adına bankadan veri çekmeye hazırdır.
İlk veri ANINDA çekilir
AuthorizationComplete → RunInitialFetchAsync (inline)
Bağlantı 'bağlı' olduğu ANDA, daha aktivasyon işlemi bitmeden, ilk veri çekimi aynı işlem içinde SENKRON (inline) yapılır: kullanıcının hesapları, bakiyeleri, son işlemleri ve kartları bankadan alınıp veritabanına yazılır. Böylece kullanıcı onay ekranından döndüğünde hesap/kart verisi zaten hazırdır — kısa bir 'boş liste' gecikmesi yaşanmaz. Eğer bu inline çekim bir hata alırsa (ör. banka geçici hatası / kota), güvenli bir yedek olarak eski asenkron 'InitialFetch' niyeti kutuya bırakılır ve işçi veriyi biraz sonra çeker. Sonraki güncellemeler periyodik yenileme veya canlı okuma (fresh=true) ile gelir.
Sonuç: Bağlantı "Connected" (bağlı) olur ve hesap/kart/işlem verisi hazırdır. Kullanıcı GET /hhs-connections/{id} ile bağlantı durumunu, GET /accountsile de hesaplarını ve bakiyelerini görebilir.
3) Ödeme emri
Ödeme akışı, rıza akışına çok benzer; ama önemli bir farkı vardır: her ödeme kendi başına bir rızadır. Yani para göndermek için önceden kurulmuş bir banka bağlantısına ihtiyaç yoktur, her ödeme kendi onayını alır:
Ödeme oluşturulur
POST /payment-orders
Kullanıcı alıcı IBAN'ı, tutarı ve açıklamayı gönderir. Önemli fark: ödeme için önceden kurulmuş bir banka bağlantısı GEREKMEZ — her ödeme kendi başına ayrı bir rızadır (para göndermek için ayrı onay alınır). Yanlışlıkla çift ödemeyi önlemek için Idempotency-Key başlığı zorunludur.
Ödeme + rıza + niyet yazılır
PaymentOrderService.CreateAsync
Ödeme için yeni bir 'ödeme tipi' rıza (bağlantı) kaydı, ödeme emrinin kendisi ve bankaya gönderim niyeti tek bir veritabanı işleminde birlikte yazılır. Bu işlem serileştirilebilir (serializable) seviyede yapılır — yani aynı anda gelen iki isteğin birbirine karışması engellenir. Yine bankaya bu anda çağrı yapılmaz.
Bankada ödeme rızası
PaymentOrderSubmitHandler → HHS
İşçi bankaya ödeme rızası (ödeme-emri-rizasi) oluşturur. Banka, tıpkı hesap bağlamada olduğu gibi bir onay adresi döndürür. Kullanıcı bu adreste ödemeyi onaylayacaktır.
Kullanıcı onaylar → callback
GET /payment-orders/callback
Kullanıcı ödemeyi banka ekranında (GKD) onayladığında banka bir callback yapar. Bu geri dönüş, ödemeyi tamamlamak için bir aktivasyon niyeti oluşturur — hesap bağlamadaki 'AuthorizationComplete' adımının ödeme karşılığıdır. İş yine batch'e devredilir.
Ödeme talimatı gönderilir
PaymentInstructionCompletionFlow
İşçi token'ı alır ve asıl ödeme emrini bankaya iletir. Banka bir ödeme referansı, güncel durum ve hangi ödeme sistemiyle (odmStm → H=Havale, F=FAST, E=EFT) yürütüleceğini döndürür. Ödeme sistemini banka kendisi seçer; YÖS (bizim taraf) bunu dayatmaz.
Durum takip edilir
GET /payment-orders/{id}?fresh=true
Kullanıcı ödemenin akıbetini merak edip sorgular. ?fresh=true eklerse (bkz. bölüm 4) sistem bankadan o anki canlı durumu çeker — gönderildi / başarılı / reddedildi gibi. fresh olmadan sorarsa en son kaydedilmiş durum döner, ki bu da genelde yeterlidir.
4) Canlı veri çekme (?fresh=true) — kullanıcı-present senkron yol
Gerçek kullanıcı ekranın başındayken "şu anki" veriyi istediğinde (PSU=E), sistem niyet kutusunu kullanmaz. API, batch'in iç ucunu doğrudan senkron çağırır; batch anında bankaya sorar ve taze veri aynı yanıtta geri döner. Okuma işlemi kalıcılık/dayanıklılık gerektirmediği için niyet kutusu burada gereksiz bir yük olurdu; yazma işleri (rıza/ödeme) ise güvence için niyet kutusunda kalır:
Canlı istek gelir
GET .../{id}?fresh=true
Gerçek kullanıcı, ekranın başındayken en güncel veriyi ister ve isteğe ?fresh=true ekler. Bu bir 'kullanıcı-present' sorgudur, yani kod tarafında PSU=E olarak işaretlenir (PSU = ödeme hizmeti kullanıcısı; E = kullanıcı şu an burada). Örneğin kullanıcı ekranda 'bakiyeyi tazele' düğmesine basarsa bu yol devreye girer.
Tazelik penceresi kontrolü
LiveRefreshCoordinator
Önce verinin ne kadar taze olduğuna bakılır. Eğer veri çok yeni ise (kısa bir 'tazelik penceresi' içinde alınmışsa) doğrudan yerel veritabanından döner — bankayı gereksiz yere yormamak için yeni çağrı yapılmaz. Aynı anda aynı kaynağa gelen birden fazla tazeleme isteği tek bir çağrıda birleştirilir (tekrarları emer), böylece banka üst üste sorulmaz.
Batch'e SENKRON RPC
ILiveQueryClient → POST /internal/live-query
Veri eskiyse API, batch'in iç ucunu SENKRON çağırır (RPC — bir programın diğerini doğrudan çağırıp yanıtını beklemesi). Bu iç uç dış dünyaya kapalıdır, yalnız iç ağdan ve gizli bir başlıkla (X-Internal-Secret) erişilir. Burada niyet kutusu veya kuyruk KULLANILMAZ; okuma dayanıklılık gerektirmediği için anlık gidip anlık dönülür.
Batch anlık BKM sorgusu
LiveQueryService → ConnectionSyncService
Batch, bankaya PSU=E ile hemen sorar (hesap / bakiye / işlem / kart — hangisi istendiyse). Kullanıcı-present olduğu için bu sorguya istek-adedi KOTASI uygulanmaz; kullanıcı beklerken 'saatlik sınıra takıldın' denmez. Batch, mevcut yenileme mantığını yeniden kullanır — özel bir kod yolu yoktur. Gelen sonuç ayrıca veritabanına yazılır, böylece önbellek de sıcak kalır.
Taze veri döner (Live)
DetachAll + yeniden oku
Batch işini bitirip dönünce API, veritabanından yeni yazılan taze veriyi okur ve yanıtı Freshness=Live (canlı) etiketiyle döndürür — hepsi aynı HTTP isteği içinde. Eğer batch'e ulaşılamaz veya banka hata verirse istek çökmez; API elindeki son önbellek verisini Freshness=Stale (bayat) etiketiyle döner ve arka planda yenilemeyi tetikler. Kullanıcı hiçbir zaman hata ekranı görmez, en kötü ihtimalle biraz eski veri görür.
Neden E için kota yok? Kota, bankanın "bana saatte şu kadar soru sorabilirsin" sınırıdır ve bankayı otonom botların yormasından korur. BKM bu istek-adedi limitini yalnız otonom/periyodik (H) sorgulara uygular; kullanıcı-present (E) ve olay-tetikli (O) sorgular sınırsızdır. Bu yüzden gerçek bir kullanıcı ekranda tazeleme istediğinde bankaya serbestçe gidilebilir. Güvenlik yine korunur: API sertifika taşımaz, bankaya kendisi bağlanmaz — sadece batch'i çağırır.
5) psuInitiated — banka çağrısı kimin adına?
Bankaya giden her sorgu, o sorgunun kaynağını belirten bir psuInitiated kodu taşır. Yani banka, isteğin ardında canlı bir kullanıcı mı yoksa arka plan botu mu olduğunu bu koddan anlar. Kod, bankanın oran-sınırı (kota) uygulayıp uygulamayacağını belirler:
| Kod | Ne zaman | Oran sınırı |
|---|
| E | Kullanıcı ekranın başında, veriyi kendisi tetikledi (fresh=true) | Yok — serbest gidilir |
| H | Batch, kullanıcı yokken otomatik/periyodik arka plan yenilemesi yapıyor | Var — kotalı (bankayı korur) |
| O | Banka bir olay bildirdi (KaynakGuncellendi), sistem ona tepki veriyor | Yok — serbest gidilir |
Kota sayacı (HhsQueryQuota) yalnız H sorgularını sayar; E (kullanıcı) ve O (banka olayı) sorguları muaftır — çünkü ikisinde de gecikme doğrudan bir kullanıcıyı bekletir.
6) Niyet kutusu (SyncIntent Outbox) neden güvenli?
Tek işlemde yazılır
Asıl kayıt değişikliği (rıza/ödeme) ile niyet, aynı veritabanı işleminde birlikte yazılır. Ya ikisi de olur ya hiçbiri — "kayıt oldu ama niyet yazılmadı" gibi yarım bir durum asla oluşmaz.
En az bir kez işlenir
İşçi tam iş sırasında çökse bile niyet kutuda durur ve tekrar denenir — hiçbir iş sessizce kaybolmaz. "OutboxConsumed" işaretiyle de aynı niyetin iki kez işlenmesi (çift ödeme gibi) engellenir.
⏳
Akıllı yeniden deneme
Banka geçici olarak yanıt vermezse, giderek artan aralıklarla tekrar denenir (üst üste yüklenmez). Kota dolmuşsa iş ertelenir. Düzelmeyen kalıcı bir hataysa "dead" (ölü) işaretlenir ve bir insan bakana kadar bekletilir.
7) İzlenebilirlik başlıkları
Bir isteği ve ilgili banka çağrılarını sonradan izleyebilmek için üç ayrı kimlik kullanılır. Her biri farklı bir kapsamı işaretler:
| Başlık | Ne işe yarar | Kapsam |
|---|
correlation_id | Tek bir HTTP isteğinin tüm log satırlarını birbirine bağlar. Her istekte yeniden üretilir; sorun ararken bu numarayı takip edersiniz. | İstek başına |
X-Group-ID | Bir rızaya ait tüm banka çağrılarını tek grupta toplar; bankalar bu numaraya bakar. Rıza oluşturulurken üretilir ve o rıza yaşadığı sürece hiç değişmez (connection.GroupId). | Rıza ömrü boyunca |
X-Request-ID | Bankaya giden her tekil işlem için ayrıdır. Aynı işlem tekrar denenirse aynı kalır (banka çift işlemez), farklı işlemde farklı olur. | İşlem başına |