Ödeme yazılımları dışarıdan basit görünse de, aslında oldukça karmaşık yapılar olabilir. Temel hesaplama, saatleri oranla çarpıp, vergileri çıkararak bir sonucu bankaya göndermeye kadar giden basit bir süreç. Ama when you start building it, everything changes.
Uzun bir süredir Laravel tabanlı bir İK ve ödeme platformu geliştiriyoruz — 14 entegre modül, çok kiracılı (multi-tenant), modüler bir yapı. Bu süreçte alınan bazı kararlar, beklenenden daha zorlu hale geldi ve farklı yapabileceğimiz noktalar oldu.
Çok Kiracılılık Kararı
Çok Kiracılılık Kararı
İlk büyük karar, çok kiracılılığı nasıl yöneteceğimizdi. Laravel’de üç yaygın yaklaşım mevcuttur:
- Kiracı başına ayrı veritabanları — en temiz izolasyon, yönetimi daha masraflı
- Ayrı şemalar (PostgreSQL) — iyi bir orta yol
- Kiracı ID sütunu olan paylaşılan veritabanı — en yaygın, yanlış yaparsanız en tehlikeli olanı
Biz 3. seçeneği tercih ettik. Her tablodaki tenant_id ile global sorgu kapsamlarının yönetimi mantıklı görünüyordu. Ancak hemen herkesin dikkate aldığı sorunla karşılaştık.
Bir yönetici işlemi sırasında eksik bir kapsam, kayıtların kiracılar arasında sızmasına neden oldu. Bir görev uygulamasında hayal kırıklığı olsa da, ödeme yazılımında verilerin maaş miktarları ve banka hesap numaraları olması sebepleriyle bu hata katı bir engel oluşturdu.
Bunun sonucunda her kiracı için ayrı veritabanlarına geçtik. Her kiracı, kendi veritabanını alıyor. Bağlantı, her istekte değil, kimlik doğrulama sırasında çözülüyor. Bir kullanıcı oturum açtığında, bu kiracının veritabanı adı oturuma kaydediliyor, sonraki her istekte bu kullanılmakta:
class AuthenticatedSessionController extends Controller
{
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$user = $request->user();
$tenant = $user->tenant;
// Oturum açıldığında saklayın — bir kez çözülmeli, istek başına değil
session(['tenant_db' => $tenant->database_name]);
config(['database.connections.tenant.database' => $tenant->database_name]);
DB::purge();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
}
Her istekte çözümleme masrafı yok. Bağlantı, oturum açılırken ayarlanıyor ve ömrü boyunca kalıyor.
Kiracılar arası sızma şimdi yapısal olarak imkansız — bu bir disiplin problemi değil, bir yapısal sorun. Gerçek bir takas var: veritabanı geçişleri her kiracı için çalıştırılıyor, bir kez değil. Bununla, tüm kiracı veritabanlarında Artisan::call() kullanarak kuyruklu bir geçiş çalıştırıcısı ile başa çıktık.
Bu ek yük, kesinlikle buna değerdi. Çünkü ödeme verilerini yönetirken, izolasyona güvenmek değil, garantilemek zorundasınız.
Modüler Mimari — Doğru Yaptığımız Kısım
Modüler Mimari — Doğru Yaptığımız Kısım
İK yazılımlarında kullanılmayan modüller sıkça görülmektedir. Eğer 30 kişilik bir ekip, işe alım sürecini, performans değerlendirme motorunu ve eğitim katalogunu ilk günden açar ve bunların hepsinden masraf ederse, gereksiz bir karmaşaya dabakat alırlar.
Her bir modülü, kendi rotalarını, politikalarını ve birleştirmelerini kaydeden bir Laravel hizmet sağlayıcısı yaptık:
class PayrollServiceProvider extends ServiceProvider
{
public function register(): void
{
if (!Module::isEnabled()) {
return; // Rotalar, politikalar ve kaynaklar yüklenmez
}
$this->app->bind(PayrollEngine::class, StandardPayrollEngine::class);
}
public function boot(): void
{
if (!Module::isEnabled()) {
return;
}
$this->loadRoutesFrom(__DIR__.);
$this->loadPoliciesFrom(__DIR__.);
}
}
Bir modül devre dışı bırakıldığında, rotaları var olmaz. Politika kayıtları gerçekleştirilmez. Sorgular çalıştırılmaz. Kontrolörlerde parçalı mantık yoktur — modül basitçe yoktur.
Bu durum, testleri de önemli ölçüde temizlemiştir. Her modülün test seti, yalnızca bağımlılıkları yüklenerek izole bir biçimde çalışır.
Ödeme Hesaplama Problemi
Ödeme Hesaplama Problemi
Ödeme yazılımının zorluğu burada başlar.
500 çalışan için bir ödeme dönemi tek bir hesaplama değil — aslında 500 bağımsız hesaplama yapmaktır, her biri şu bilgileri içerir:
- Dönem için temel ücret (ay içinde işe başlayan ve ayrılanlar dahil)
- Devam verileri ve onaylı fazla mesai
- Kullanılan izinler (ücretli, ücretsiz, kısmi ücretli)
- Aktif kesintiler (kredi taksitleri, avanslar)
- Yasal rakamlar (Ulusal Sigorta, Sağlık Sigortası, gelir vergisi kesintisi)
- Maaş yapısına göre yapılandırılan özel kazanç ve kesinti bileşenleri
Bunu tek seferde bir web isteği içinde çalıştırmak pek mümkün değil. Yavaş bir bağlantıda 500 çalışan için bir çalışma süresi, iki dakikadan kısa sürede zaman aşımına uğrayacaktır.
Bu nedenle, tüm ödeme hesaplamalarını Laravel kuyruklarına taşıdık ve Horizon, çalışanların yönetimini sağladı:
class ProcessPayrollRun implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $timeout = 3600; // Büyük kiracılar için maksimum 1 saat
public function handle(PayrollEngine $engine): void
{
$this->run->employees->each(function (Employee $employee) use ($engine) {
ProcessEmployeePayslip::dispatch($employee, $this->run);
});
}
}
Her çalışanın maaş bordrosu, ayrı bir iş olarak hesaplanır. Eğer biri başarısız olursa (kötü veri, fazla mesai hesaplamasındaki bir uç durum), geri kalan işler devam eder. Başarısız olan işler otomatik olarak tekrar denenir ve bordro memuru için bir inceleme kuyruğuna çıkar.
Arayüz, canlı bir ilerleme çubuğu gösterir. Bordro memuru anormallikleri inceleyebilir — fazla mesai dalgalanmaları, kısmi ay işe girişler, ortada kalan istifalar — ödemeyi onaylamadan önce gözden geçirebilir.
Yasal Uyum Rapor Değildir
Yasal Uyum Rapor Değildir
Yaptığımız neredeyse bir hata: yasal uyumu sonradan eklenen bir raporlama özelliği olarak düşünmek.
Ulusal sigorta katkıları, sağlık sigortası üzerinde çalışan ve işveren payları, gelir vergisi kesintileri — bunlar yıl sonu hesaplayıp umduğunuz rakamların üzerinden geçmekle olmuyor. Her hesaplama döngüsünde, o dönemde geçerli olan oranlar ve ücret tavanlarına karşı göz önünde bulundurulmalıdır.
Yasal oranları, versiyonlanmış yapılandırma olarak modelledik:
// config/statutory/national_insurance.php
return [
=> [
=> 0.12,
=> 0.138,
=> 967, // haftalık
=> 123,
],
];
Oranlar değiştiğinde, yeni bir giriş ekliyoruz. Geçmişteki bordro işlemleri, o dönemde geçerli olan oranları kullanıyor — hesaplama, yıllar sonra tekrarlanabilir ve veritabanı bugünün oranlarını bilmek zorunda değil.
Denetim İzleme Problemi
Denetim İzleme Problemi
İK verisi değişiklikleri kalıcı ve değiştirilemez olmalıdır. Bir çalışanın maaş değişikliği, bir izin onayı, bir ödeme yapıldığında — bunlardan herhangi biri daha sonra sorgulanırsa, düzenlenemez bir kaydınız olmalıdır.
Standart Laravel model olayları bir günlük tabloya yazar, ancak bu tablo, veritabanına erişimi olan herkes tarafından düzenlenebilir.
Bunu yalnızca ekleme ile çözmek için ve bir hash zinciri kullanarak:
class AuditEntry extends Model
{
public $timestamps = false;
// update() veya delete() yöntemleri yok
protected static function booted(): void
{
static::updating(fn() => throw new \Exception());
static::deleting(fn() => throw new \Exception());
}
}
Her giriş, bir öncekinin hash’ini içerir. Herhangi bir kaydı bozmak, zinciri kırar — ayrı bir bütünlük hizmetine ihtiyaç duymadan tespit edilebilir olur.
Nasıl Farklı Yapardık
Nasıl Farklı Yapardık
İzin modeliyle başlamak. Özellikleri önce tasarladık ve erişim denetimini sonradan ekledik. Alan düzeyinde izinleri mevcut bir kod tabanına geriye dönük olarak eklemek zor bir süreçtir. Erişim kontrol matrisinizi ilk geçişinizden önce tanımlayın.
Bildirim sistemini sıfırdan inşa etmeyin. In-app bir bildirim sistemi geliştirmek için haftalar harcadık, bu da aslında Laravel Bildirimları’nın basit bir versiyonuydu ancak özel bir arayüz ile. Laravel Bildirimları ile bir veritabanı kanalını ve ince bir frontend’i çoğu şey için yeterlidir.
API’nizi başından itibaren versiyonlayın. REST API’ye versiyonlama eklemesi, iki dış entegrasyon zaten üretimdeyken gerçekleştirdik. Geçiş yönetilebilir fakat gereksizdi.
Ürün canlı. Benzer bir şey üzerinde çalışıyorsanız veya bu kararlarla ilgili sorularınız varsa, yorumlar açık.
Laravel tabanlı ürünler geliştiriyoruz Syftnex. İK & Ödeme platformumuz, mimarinin işleyişini görmek isterseniz, demo için hazır.
Kaynak: Orijinal Makale


