Türk Bayrağı
Software Architecture

Payment Gateway Geliştirmek: Kod Değil, Güven İnşa Etmek

Sıfırdan Ödeme Sistemi Geliştirmek: 9 Banka, 5 Servis, 1 Mimari

Bir ödeme sistemi geliştirmek, dışarıdan bakıldığında “bankaya bir HTTP isteği at, cevabı al” kadar basit görünür. İçine girdiğinizde ise karşınıza sertifika doğrulama, tutar manipülasyonu, çift çekim riski, 3D Secure callback yarış koşulları ve bankaların birbirinden farklı XML/JSON/SOAP yapılarıyla boğuşmak çıkar.

Bu yazıda, 9 farklı Türk bankasıyla entegre çalışan, .NET 9 tabanlı bir ödeme geçidi (Payment Gateway) projesini sıfırdan nasıl kurguladığımı anlatıyorum. Sadece “ne yaptım” değil, “neden böyle yaptım” ve “hangi hataları yaptım” soruları üzerinden ilerleyeceğim.

1. Mimari Kurgu: Tek Proje Değil, Beş Servis

İlk refleks her zaman tek bir ASP.NET projesi açıp her şeyi içine koymak olur. Ancak bir ödeme sisteminde sorumluluk ayrımı hayati önem taşır. Projeyi beş bağımsız servise böldüm:

B2B.PaymentGateway/
├── WebAPI          → Müşteriden gelen ödeme isteklerini karşılar
├── AdminAPI        → Finansal raporlama, iade, iptal, kullanıcı yönetimi
├── WebUI           → Ödeme sayfası (kart bilgisi girilen arayüz)
├── AdminUI         → Yönetim paneli (banka ayarları, dashboard, loglar)
└── Worker          → Arka plan işleri (ERP senkronizasyonu, e-posta bildirimleri)

Neden Bu Ayrım?

Saldırı yüzeyini küçültmek. WebAPI internete açıkken, AdminAPI sadece belirli IP blokları veya VPN üzerinden erişilebilir. Yönetim paneli hacklense bile ödeme trafiği etkilenmez. Ödeme sayfası DDoS yese bile finans ekibi raporlarına erişebilir.

Bağımsız ölçekleme. Bayram dönemlerinde ödeme trafiği 10 katına çıkarken, admin panelinin tek bir instance’ı yeterli kalır. Kubernetes veya IIS üzerinde her servisi ayrı ölçeklendirebilmek ciddi kaynak tasarrufu sağlar.

Deploy güvenliği. Admin paneline yeni bir özellik eklerken, canlıdaki ödeme akışına dokunmam gerekmiyor. Her servis kendi solution’ında, kendi deploy döngüsünde yaşıyor.

2. Adapter Pattern: 9 Banka, 9 Farklı Dünya

Türkiye’deki bankalarla çalışmanın en büyük zorluğu standart eksikliğidir. Akbank JSON tabanlı bir REST API sunarken, Yapı Kredi (Posnet) XML ile konuşur. Kuveyt Türk SOAP bazlı bir yapı kullanır. Garanti BBVA’nın hash hesaplama yöntemi diğerlerinden tamamen farklıdır.

Bu kaos ortamında mimariyi Adapter Pattern ile kurguladım:

IPOSService (Unified Interface)
├── AkbankPOSService
├── QnbFinansPOSService
├── YapiKrediPOSService
├── IsbankPOSService
├── ZiraatPOSService
├── HalkbankPOSService
├── GarantiPOSService
├── TurkiyeFinansPOSService
└── KuveytTurkPOSService

Her adapter aynı interface’i (IPOSService) implemente eder. Üst katman hangi bankayla konuştuğunu bilmez, bilmek zorunda da değildir. Yeni bir banka eklemek; mevcut koda dokunmadan yeni bir adapter sınıfı yazmak demektir.

Banka Tespiti: BIN Numarasıyla Routing

Kullanıcı kart numarasını girdiği anda, kartın ilk 6-8 hanesi (BIN numarası) ile hangi bankaya yönlendirileceği tespit edilir. BankDetectionService bu işi üstlenir:

var bankInfo = await _bankDetectionService.DetectBankByCardNumberAsync(request.CardNumber);
if (bankInfo == null)
{
    return new PaymentResponse
    {
        IsSuccess = false,
        Message = "Banka tespit edilemedi ve default banka tanımlı değil",
        ErrorCode = "BANK_NOT_DETECTED"
    };
}

BIN veritabanı düzenli güncellenmeli. 8 haneli BIN desteği eklememiz de bu yüzden oldu — bazı yeni kartlar 6 haneli BIN tablosunda eşleşmiyordu.

3. Orchestration: Ödeme Akışının Kalbi

Ödeme işlemi tek bir HTTP çağrısı değil; birden fazla adımdan oluşan bir pipeline’dır. Bunu yöneten PaymentOrchestrationService, sistemin en kritik sınıfıdır:

  1. İstek doğrulama (validasyon)
  2. Transaction kaydı oluştur (PENDING)
  3. BIN ile banka tespiti
  4. Komisyon hesaplama (NET/BRÜT)
  5. İlgili banka adapter’ını seç
  6. 3D Secure sayfasına yönlendir VEYA doğrudan çekim yap
  7. Callback’i al, tutar doğrulaması yap
  8. Transaction statüsünü güncelle (SUCCESS / FAILED)
  9. Worker kuyruğuna at (ERP sync, e-posta)

Her adım PaymentTransactionDetails tablosuna loglanır. Bir işlem başarısız olduğunda, hangi adımda ve neden kırıldığını saniyeler içinde tespit edebiliriz.

public async Task<PaymentResponseDto> ProcessPaymentAsync(PaymentRequestDto dto)
{
    string transactionId = Guid.NewGuid().ToString();
    try
    {
        var paymentRequest = MapToPaymentRequest(dto, transactionId);
        var (flowType, stepName) = GetFlowType(paymentRequest.Use3D);

        await _paymentTransactionService.CreateTransactionAsync(paymentRequest, flowType);
        await LogInitialRequestAsync(paymentRequest, stepName);

        var paymentResponse = await _posService.ProcessPaymentAsync(paymentRequest);
        return await HandlePaymentResponseAsync(paymentRequest, paymentResponse);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Ödeme işlemi sırasında hata oluştu");
        await LogExceptionAsync(transactionId, ex);
        return new PaymentResponseDto
        {
            IsSuccess = false,
            Message = "Ödeme işlemi sırasında bir hata oluştu",
            TransactionId = transactionId
        };
    }
}

4. 3D Secure ve En Büyük Risk: Tutar Manipülasyonu

3D Secure akışında kullanıcı bankasının sayfasına yönlendirilir. SMS onayından sonra sistem bir callback alır. İşte tam burada Man-in-the-Middle (MitM) riski devreye girer: Callback’teki tutar, yolda değiştirilebilir.

Bunun önüne geçmek için PaymentTransactionValidationService yazdım. Her callback geldiğinde üç kritik kontrol yapılır:

Kontrol 1: Transaction Statüsü

Aynı callback’in iki kere işlenmesini engeller. Zaten SUCCESS veya FAILED olan bir transaction’a yeni callback gelmemelidir:

if (transaction.Status == (byte)PaymentTransactionStatus.SUCCESS)
{
    return new ValidationResult
    {
        IsValid = false,
        ErrorMessage = "Bu işlem zaten başarıyla tamamlanmış."
    };
}

Kontrol 2: Tutar Doğrulama

Veritabanındaki CalculationAmount ile callback’ten gelen tutar karşılaştırılır. 0.01 TL toleransla (kuruş yuvarlama farkları için) kontrol edilir:

var amountDifference = Math.Abs(transaction.CalculationAmount - callbackAmount.Value);
if (amountDifference > 0.01m)
{
    return new ValidationResult
    {
        IsValid = false,
        ErrorMessage = $"İşlem tutarı eşleşmiyor. Beklenen: {transaction.CalculationAmount:N2} ₺, Gelen: {callbackAmount.Value:N2} ₺"
    };
}

1000 TL’lik bir işlem 1 TL olarak dönüyorsa, bu kesinlikle bir manipülasyon girişimidir ve sistem bunu reddeder.

Kontrol 3: Hash Doğrulama

Bankalar callback verisini bir hash imzasıyla gönderir. Bu hash’i kendi MerchantKey’imizle yeniden hesaplayıp eşleşmeyi doğrularız. Eşleşmezse veri yolda değiştirilmiştir.

5. Transaction Lifecycle ve Idempotency

Ödeme sistemlerinin kabusu çift çekimdir. Kullanıcı butona iki kere basar, bağlantı kopar ve yeniden dener, veya tarayıcı timeout alıp isteği tekrarlar.

Transaction Statüleri

Her ödemenin net bir yaşam döngüsü var:

public enum PaymentTransactionStatus : byte
{
    PENDING   = 1,  // İşlem başlatıldı, bankadan cevap bekleniyor
    FAILED    = 2,  // İşlem başarısız
    SUCCESS   = 3,  // İşlem başarılı
    CANCELLED = 4,  // İptal edildi
    REFUNDED  = 5   // İade edildi
}

İşlem PENDING olarak başlar. Callback geldiğinde validation service önce statüyü kontrol eder. Zaten SUCCESS olan bir transaction’a yeni bir çekim yapılmaz — bu, idempotency’nin en temel garantisidir.

Atomicity

Eğer sipariş kaydı oluşturulamazsa veya herhangi bir adımda hata alınırsa, tüm transaction rollback edilir. Yarım kalmış bir ödeme kaydı sistemde asla SUCCESS statüsünde bırakılmaz.

6. Güvenlik Katmanları

Bir ödeme sistemi güvenliği tek bir noktada değil, her katmanda sağlanmalıdır.

AES-256-CBC Şifreleme

Hassas veriler (kullanıcı session bilgileri, ERP entegrasyon parametreleri) AES-256-CBC ile şifrelenir. Her şifreleme için cryptographically secure random IV üretilir — aynı veriyi iki kez şifreleseniz bile farklı çıktı alırsınız:

// Her zaman random IV oluştur (güvenlik için kritik)
byte[] ivBytes;
using var aesTemp = Aes.Create();
aesTemp.GenerateIV();
ivBytes = aesTemp.IV;

Ek olarak, şifre çözme sonrası hassas byte dizileri bellekten temizlenir (Array.Clear). Bu, memory dump saldırılarına karşı ek bir koruma katmanıdır.

Security Headers (WebUI)

Ödeme sayfasında tarayıcı seviyesinde koruma:

  • X-Frame-Options: DENY — Clickjacking önlenir, sayfa iframe içine alınamaz
  • X-Content-Type-Options: nosniff — MIME type sniffing engellenir
  • Content Security Policy (CSP) — XSS saldırı yüzeyi daraltılır
  • Referrer-Policy: strict-origin-when-cross-origin — Hassas URL parametreleri sızdırılmaz
  • CSRF Token — Her form submit’te X-CSRF-TOKEN header’ı doğrulanır

PCI-DSS Yaklaşımı

İlk kural: Kart verisini asla veritabanına kaydetme. Kart numaraları sadece işlem anında bellekte tutulur, loglanmaz, veritabanına yazılmaz. PCI-DSS sertifikanız yoksa kart verisini sisteminizde kalıcı olarak saklamamanız gerekir.

7. Audit Log: Her İşlemin İzi

Finans dünyasında “kim, ne zaman, ne yaptı” sorusunun cevabı her an verilebilir olmalıdır. AdminAPI’ye gelen her istek otomatik olarak bir Audit Log middleware’i tarafından yakalanır:

public async Task InvokeAsync(HttpContext context, IAuditLogService auditLogService)
{
    var stopwatch = Stopwatch.StartNew();
    var requestBody = await ReadRequestBodyAsync(context.Request);

    try
    {
        await _next(context);
    }
    finally
    {
        stopwatch.Stop();
        var auditLog = CreateAuditLog(context, requestBody, stopwatch.ElapsedMilliseconds);
        await auditLogService.LogAsync(auditLog);
    }
}

Her log kaydında şunlar saklanır:

Alan Açıklama
UserId / UserEmail JWT claim’lerinden çözülen kimlik
IpAddress X-Forwarded-For, X-Real-IP zincirleri dahil
HttpMethod / RequestPath Hangi endpoint’e ne tür istek atıldı
RequestBody POST/PUT/PATCH için gövde (10MB limit)
ResponseStatusCode Dönen HTTP status
DurationMs İşlem süresi (milisaniye)
OperationType Read / Create / Update / Delete

Bu kayıtlar immutable olarak saklanır. Bir kullanıcı 50.000 TL’lik bir iadeyi kimin onayladığını sorduğunda, saniyeler içinde cevap verebiliriz.

8. Worker Service: Arka Plan İşleri

Ödeme başarılı olduktan sonra yapılması gereken işler ana thread’i bloke etmemelidir. İki ayrı worker servis bu işi üstlenir:

ErpSyncWorker

Başarılı ödemeleri ERP sistemine senkronize eder. Periyodik olarak PENDING durumundaki ERP kayıtlarını sorgular ve işler:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        try
        {
            await ProcessErpSyncAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "ErpSyncWorker işlemi sırasında hata oluştu");
        }
        await Task.Delay(TimeSpan.FromMinutes(_intervalMinutes), stoppingToken);
    }
}

PaymentNotificationWorker

Ödeme makbuzu e-postalarını gönderir. E-posta servisi çökse bile ödeme işlemi etkilenmez — worker bir sonraki döngüde tekrar dener.

Neden Queue Değil de Polling?

Mevcut ölçekte (dakikada yüzlerce işlem) polling yeterli. Ancak ölçek büyüdüğünde RabbitMQ veya Redis Streams’e geçiş için altyapı hazır — worker’lar zaten veritabanından bağımsız çalışacak şekilde tasarlandı.

9. Teknoloji Seçimleri ve Gerekçeleri

Teknoloji Neden?
.NET 9 Performans, tip güvenliği, ekosistem olgunluğu. Finansal sistemlerde runtime hataları yerine compile-time hataları tercih ederiz.
PostgreSQL ACID garantisi, güçlü ilişkisel yapı, table partitioning desteği. Milyonlarca transaction kaydında performans kritik.
Dapper Entity Framework’ün overhead’i yerine, SQL üzerinde tam kontrol. Finansal sorgularda ne döndüğünü birebir bilmek istiyoruz.
Serilog + Grafana Loki Yapılandırılmış (structured) loglama. TransactionId ile filtreleme yaparak bir ödemenin tüm yaşam döngüsünü tek sorguda görebilmek.
JWT (AdminAPI) Stateless authentication. Her isteğin kendi kimlik bilgisini taşıması, yatay ölçeklemeyi kolaylaştırır.

10. Komisyon Hesaplama: NET vs BRÜT

Farklı müşteri tipleri (Genel, Özel, Toptan) için farklı komisyon oranları uygulanır. İki hesaplama yöntemi var:

  • NET: Müşterinin ödediği tutara komisyon eklenir. 1000 TL ürün + %3 komisyon = 1030 TL çekim.
  • BRÜT: Komisyon tutarın içinden düşülür. 1000 TL çekim, satıcıya 970 TL aktarılır.

Bu hesaplama taksit sayısına göre de değişir. 6 taksit ile 12 taksitin komisyon oranı farklıdır. Tüm bu oranlar veritabanında yönetilir ve AdminUI üzerinden güncellenir.

11. Monitoring ve Observability

Bir ödeme sisteminde “çalışıyor mu?” sorusu yeterli değildir. “Son 5 dakikada başarı oranı nedir?”, “Hangi bankanın hata oranı yükseldi?”, “Ortalama işlem süresi kaç ms?” sorularına anlık cevap verebilmeniz gerekir.

  • Serilog → Grafana Loki: Tüm loglar merkezi bir yerde toplanır. TransactionId bazlı end-to-end trace yapılabilir.
  • Dashboard (AdminUI): Günlük işlem hacmi, başarı/başarısızlık oranları, banka bazlı dağılım. Finans ekibi koddan bağımsız olarak sistemi izleyebilir.
  • Transaction Details: Her ödeme adımı (3D yönlendirme, callback, banka cevabı) ayrı ayrı loglanır. Bir sorun olduğunda “hangi adımda kırıldı?” sorusu saniyeler içinde cevaplanır.

12. Öğrenilen Dersler

Bankaların Dokümantasyonu Yetersizdir

Test ortamı ile canlı ortam arasında davranış farkları olabiliyor. Bir bankanın test ortamında düzgün çalışan hash algoritması, canlıda farklı bir encoding kullanıyor olabiliyor. Entegrasyon testlerine zaman ayırın.

Her Şeyi Logla, Ama Akıllıca

Kart numarası, CVV, şifre gibi hassas verileri asla loglamayın. Ancak transaction ID, banka cevap kodları, HTTP status’lar ve süre bilgileri mutlaka loglanmalı. Bir sorun olduğunda “logda yok” demek, ödeme sisteminde kabul edilemez.

Timeout Yönetimi Hayat Kurtarır

Banka API’leri bazen 30 saniye boyunca cevap vermeyebilir. Uygun timeout değerleri ve retry mekanizmaları olmadan kullanıcı deneyimi çöker, bağlantı havuzu tükenir.

İade İşlemi Ödeme Kadar Kritiktir

Herkes ödeme alma akışına odaklanır ama iade (refund) akışı en az onun kadar önemlidir. Kısmi iade, tam iade, iptal — her birinin kendi iş kuralları ve banka API farklılıkları vardır.

Sonuç

Ödeme sistemi geliştirmek, “en kötü senaryoyu” düşünerek inşa etmek demektir. İnternet kopabilir, banka cevap vermeyebilir, kullanıcı butona beş kere basabilir, callback verisi manipüle edilebilir. Kodun şık olmasından çok, sistemin tutarlı olması ve her bir kuruşun izlenebilir olması önceliklidir.

9 banka entegrasyonu, 5 bağımsız servis, binlerce satır adapter kodu ve sayısız edge case... Sonuçta ortaya çıkan sadece bir yazılım projesi değil, güven inşa eden bir platform.

Bu yazıda bahsedilen mimari ve kod örnekleri, .NET 9, PostgreSQL ve Dapper kullanılarak geliştirilmiş gerçek bir B2B ödeme geçidi projesine aittir.

Yazar: Ürfet Demirtaş