Tüm yazılara dön

8 Farklı Ödeme Sağlayıcısını Tek Muhasebe Sisteminde Birleştirmek: Mutabakat Mimarisi

11 Eylül 20262 dk okuma
ASP.NET CoreBackground WorkersMuhasebe Otomasyonu

Sanal (uygulama içi) coin bayileri için geliştirdiğim muhasebe sisteminde en zor problem SignalR ya da UI değildi — sekiz farklı dış servisten (banka makbuz tarayıcıları, birden fazla ödeme sağlayıcısı, e-fatura) gelen veriyi tek, tutarlı bir mutabakat kaydına dönüştürmekti. Her servisin kendi formatı, kendi gecikme profili ve kendi hata modları vardı. Bu yazıda o mimariyi nasıl kurduğumu anlatıyorum.

Neden tek bir "senkronizasyon job'ı" yeterli olmadı

İlk yaklaşımım tek bir zamanlanmış job'ın sırayla sekiz servisi dolaşmasıydı. İki sorun çıktı:

  • Domino etkisi: Servislerden biri (örneğin bir ödeme sağlayıcısının API'si) yavaşladığında, sıradaki yedi servisin işlenmesi de gecikiyordu.
  • Kısmi başarısızlık takibi zor: Job ortasında hata verirse, hangi servislerin işlendiği hangi servislerin işlenmediği belirsizleşiyordu.

Çözüm, her dış servis için bağımsız bir BackgroundService worker'ı yazmaktı — her biri kendi polling aralığında, kendi hata yönetimiyle, birbirinden habersiz çalışıyor:

public class BankReceiptWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var receipts = await _bankClient.FetchNewReceiptsAsync(stoppingToken);
                foreach (var receipt in receipts)
                {
                    await _reconciliationService.ProcessReceiptAsync(receipt, stoppingToken);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Banka makbuz senkronizasyonu başarısız");
                // Bu worker'ın hatası diğer worker'ları etkilemiyor.
            }
 
            await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
        }
    }
}

Mutabakat: aynı ödemeyi iki kez işlememek

Sekiz servisten gelen veriler bazen aynı gerçek ödemeyi farklı zamanlarda, farklı kimliklerle bildiriyordu (örneğin banka makbuzu ödemeyi saat 14:03'te, ödeme sağlayıcısı webhook'u 14:05'te görüyor). Aynı ödemeyi iki kez işlemek, bayinin bakiyesine çift ekleme yapmak demekti — muhasebe sisteminde bu affedilemez bir hata sınıfı.

Çözüm, her gelen kaydı tutar + zaman penceresi + gönderen bilgisi üçlüsünden türetilen bir idempotency anahtarıyla işlemek oldu:

var idempotencyKey = $"{amount:F2}_{senderIdentifier}_{timestamp:yyyyMMddHH}";
 
var exists = await _db.ReconciliationRecords
    .AnyAsync(r => r.IdempotencyKey == idempotencyKey, stoppingToken);
 
if (exists)
{
    continue; // Zaten işlenmiş, aynı ödemenin farklı kaynaktan tekrarı.
}

Saat bazlı pencere (yyyyMMddHH) kullanmamın sebebi, aynı kişiden aynı tutarda ama gerçekten farklı iki ödemeyi (örneğin bir saat arayla) yanlışlıkla aynı ödeme sanmamaktı — dakika hassasiyeti çok katı olurdu (banka/sağlayıcı arasındaki saat senkron sapması yüzünden gerçek eşleşmeleri kaçırabilirdi), saat hassasiyeti pratikte yeterli ayrım sağladı.

Kısmi eşleşme: insan onayına düşen vakalar

Her ödeme otomatik eşleşmiyor — tutar birebir uymadığında (örneğin banka komisyonu düşülmüş bir tutar geldiğinde) sistem bunu otomatik işlemek yerine bir "beklemede" kuyruğuna atıyor ve muhasebe ekibine SignalR üzerinden gerçek zamanlı bildirim gönderiyordu. Otomasyonun amacı %100 insan müdahalesini sıfırlamak değil, %90'ı otomatikleştirip kalan %10'u insanın hızlıca karar verebileceği net bir kuyruğa indirgemekti.

E-fatura entegrasyonu: senkron değil, kuyruklu

E-fatura sağlayıcısının API'si zaman zaman 30 saniyeyi bulan yanıt süreleri veriyordu. Bunu diğer worker'ları bloklamadan işlemek için ayrı bir kuyruğa (basit bir veritabanı tablosu + FOR UPDATE SKIP LOCKED benzeri bir claim deseni) aldım; e-fatura worker'ı kendi hızında kuyruğu tüketiyor, diğer mutabakat akışı onu beklemiyor.

Sonuç

Sekiz bağımsız worker, saat penceresi bazlı idempotency anahtarı ve insan onayına düşen kısmi eşleşme kuyruğu — bu üç karar, 67'den fazla migration'la uzun süre üretimde kalan bir sistemin temelini oluşturdu. Sistemin BCrypt/rol tabanlı yetkilendirme ve raporlama tarafını proje sayfasında bulabilirsin.

İlgili Proje

Bu yazının anlattığı projeyi detaylıca incele.

Projeyi İncele