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:
- İstek doğrulama (validasyon)
- Transaction kaydı oluştur (PENDING)
- BIN ile banka tespiti
- Komisyon hesaplama (NET/BRÜT)
- İlgili banka adapter’ını seç
- 3D Secure sayfasına yönlendir VEYA doğrudan çekim yap
- Callback’i al, tutar doğrulaması yap
- Transaction statüsünü güncelle (SUCCESS / FAILED)
- 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-TOKENheader’ı 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ş