← Journal
JOURNAL / 23.09.2026

Hazır Ödeme Linki Yerine Neden Kendi Payment Sistemimizi Geliştirdik? | zelrah.dev

ZELRAH PAYMENT akış diyagramı; müşteri, proje, ödeme ve promo bilgilerinin checkout, webhook ve transaction adımlarıyla güvenli biçimde işlendiğini gösterir.

ZELRAH PAYMENT ödeme akışı; müşteri, proje, ödeme ve promo bilgilerinin checkout, webhook ve transaction süreciyle güvenli şekilde işlendiği sistem diyagramı.
ZELRAH PAYMENT ödeme akışı; müşteri, proje, ödeme ve promo bilgilerinin checkout, webhook ve transaction süreciyle güvenli şekilde işlendiği sistem diyagramı.

Hazır Ödeme Linki Yerine Neden Kendi Payment Sistemimizi Geliştirdik?

Bir müşteriye ödeme almak için link göndermek yeni bir problem değil.

Birçok ödeme sağlayıcısı birkaç dakika içinde ödeme bağlantısı oluşturmanıza izin veriyor. Tutarı giriyorsunuz, açıklamayı yazıyorsunuz ve müşteriye linki gönderiyorsunuz.

Bizim problemimiz ise link oluşturmak değildi.

Problem şuydu:

Bir projenin ödeme sürecini, müşterisini, indirimini, gerçek tahsilat tutarını ve ödeme sonucunu tek bir sistem içerisinde nasıl güvenilir şekilde yönetebiliriz?

Bu yüzden ZELRAH PAYMENT ortaya çıktı.

Sistemin amacı yeni bir banka veya kart işleme altyapısı geliştirmek değil. Kart bilgilerini kendi sunucularımızda tutmak da değil.

Asıl amaç, ödeme sağlayıcısı ile Zelrah'ın proje operasyonları arasında kontrollü bir katman oluşturmak.

Bir ödeme linkinden daha fazlasına ihtiyacımız vardı

Standart bir ödeme linkinde çoğunlukla birkaç bilgi vardır:

  • açıklama
  • ödeme tutarı
  • ödeme butonu

Bizim çalışma biçimimizde ise ödeme bundan daha fazla bağlam taşıyor.

Bir ödeme;

  • bir müşteriye,
  • bir projeye,
  • bir referans numarasına,
  • belirli bir tutara,
  • belirli bir para birimine,
  • gerektiğinde belirli bir kampanyaya,
  • ve sonucunda oluşan bir transaction'a

bağlı.

Bu nedenle sistemde ödeme oluştururken yalnızca “10.000 TL tahsil et” demiyoruz.

Örneğin kayıt şu şekilde olabilir:

```text Payment Website Development

Customer Hüdai Kaya Tattoo

Reference HK-2026-004

Original amount ₺10,000

Payment URL payment.zelrah.dev/hudai-kaya-web-site ```

Payment link böylece tek kullanımlık rastgele bir URL olmaktan çıkıp projenin finansal kaydının bir parçasına dönüşüyor.

Neden hazır bir dashboard kullanmadık?

Hazır sistemler kötü olduğu için değil.

Aksine ödeme sağlayıcılarının çözmesi gereken çok zor problemler var ve özellikle kart işleme tarafında bunları yeniden geliştirmek istemiyoruz.

Fakat sağlayıcının dashboard'u ile bizim operasyonel ihtiyacımız aynı şey değil.

Bizim için önemli olan örneğin:

```text Original ₺10,000

Promo WELCOME15 · 15%

Discount -₺1,500

Final ₺8,500 ```

bilgisinin yalnızca ekranda görünmesi değil.

Ödeme sağlayıcısına gerçekten ₺8.500 gönderilmiş olması.

Bu küçük gibi görünen fark aslında sistemin tasarımındaki en önemli kararlardan biri.

Frontend fiyatı belirleyemez

Bir checkout ekranında yapılabilecek en tehlikeli hatalardan biri, tarayıcıda görünen değeri güvenilir kabul etmektir.

Kullanıcı tarayıcının geliştirici araçlarını açabilir.

Request'i değiştirebilir.

JavaScript state'ini manipüle edebilir.

Örneğin:

```text finalAmount = 100 ```

gönderebilir.

Eğer backend bu değere güveniyorsa, 10.000 TL'lik bir işlemin 1 TL üzerinden başlatılması gibi ciddi bir açık ortaya çıkabilir.

Bu nedenle ZELRAH PAYMENT'ta browser fiyatın kaynağı değildir.

Checkout başladığında server yeniden:

  • payment kaydını bulur
  • gerçek tutarı database'den alır
  • promo kodunu kontrol eder
  • uygunluk şartlarını tekrar doğrular
  • indirimi tekrar hesaplar
  • final tutarı belirler
  • ancak bundan sonra ödeme sağlayıcısında checkout oluşturur

Başka bir ifadeyle:

Arayüz fiyatı gösterir. Server fiyatı belirler.

Promo code neden yalnızca görsel bir özellik değil?

Promo code ilk bakışta basit görünebilir.

Bir kod girilir:

```text WELCOME15 ```

ve fiyat %15 düşer.

Ama gerçek bir sistemde bundan sonra birçok soru ortaya çıkar.

Kod aktif mi?

Süresi dolmuş mu?

Henüz başlamamış olabilir mi?

Kaç kez kullanılabilir?

Bir müşteri kaç kez kullanabilir?

Her payment'ta mı geçerli?

Sadece belirli müşteriler için mi?

Minimum ödeme tutarı var mı?

Maksimum indirim sınırı var mı?

ZELRAH PAYMENT içerisindeki promo engine bütün bu kontrolleri server tarafında yapacak şekilde tasarlandı.

Örneğin %15 indirimli bir promo düşünelim.

Normalde:

```text ₺50,000 × %15 = ₺7,500 ```

indirim oluşturur.

Ancak kampanyanın maksimum indirimi ₺2.000 ise sistem:

```text Original ₺50,000

Discount -₺2,000

Total ₺48,000 ```

hesaplar.

Ve bu hesap frontend'de değil backend'de yapılır.

Para hesabında neden float kullanmıyoruz?

Ödeme sisteminde küçük matematik hataları kabul edilebilir değildir.

Bu nedenle tutarları JavaScript floating-point değerleriyle taşımak yerine minor unit kullanıyoruz.

Örneğin:

```text ₺8,500.00 ```

database tarafında:

```text 850000 ```

olarak temsil edilebilir.

Böylece tutarlar integer üzerinden işlenir ve yüzde hesaplamalarında belirlenmiş rounding kuralları kullanılır.

Bu karar arayüzde görünmez.

Ama ödeme sistemlerinde iyi mimarinin önemli bir kısmı zaten kullanıcının hiçbir zaman fark etmeyeceği kararların doğru alınmasıdır.

Müşteri “Pay” butonuna bastığında ne oluyor?

Kullanıcı açısından işlem oldukça basit görünmeli.

Payment sayfasını açar.

Bilgilerini girer.

Varsa promo kodunu uygular.

Toplamı görür.

Ödemeye devam eder.

Arka tarafta ise daha kontrollü bir akış gerçekleşir.

```text Payment link oluşturulur ↓ Müşteri sayfayı açar ↓ Server payment durumunu doğrular ↓ Müşteri bilgilerini girer ↓ Promo kodu server tarafından doğrulanır ↓ Final tutar hesaplanır ↓ Checkout isteği gönderilir ↓ Server fiyatı yeniden hesaplar ↓ Payment provider checkout'u oluşturulur ↓ Müşteri ödemeyi gerçekleştirir ↓ Provider callback/webhook gönderir ↓ Webhook doğrulanır ↓ Transaction PAID durumuna geçirilir ↓ Promo kullanımı kesinleştirilir ```

Başarı sayfasına neden güvenmiyoruz?

Bu sistemde önemli kararlardan biri de şu:

Kullanıcının success sayfasına ulaşması, ödemenin başarılı olduğu anlamına gelmez.

Redirect edilebilir.

URL manuel açılabilir.

Browser kapanabilir.

Network bağlantısı kesilebilir.

Bu nedenle transaction'ın gerçek durumu ödeme sağlayıcısından gelen doğrulanmış callback veya webhook üzerinden güncellenir.

Ödeme başarılıysa transaction `PAID` olur.

Başarısızsa promo kullanım hakkı tüketilmez.

Aynı webhook tekrar gelirse idempotency mekanizması aynı işlemin iki kez kaydedilmesini engeller.

Payment status neden state machine?

Bir ödeme linkinin durumu yalnızca “ödendi / ödenmedi” değildir.

Sistemde kontrollü durumlar bulunuyor:

```text DRAFT ACTIVE PROCESSING PAID FAILED EXPIRED CANCELLED ```

Ve durumlar rastgele değişemez.

Örneğin:

```text ACTIVE → PROCESSING PROCESSING → PAID PROCESSING → FAILED ACTIVE → EXPIRED ```

mümkündür.

Ama:

```text PAID → ACTIVE ```

olmamalıdır.

Bu küçük state machine, ödeme kaydının zaman içerisinde anlamsız durumlara gelmesini engeller.

Kart bilgilerini neden işlemiyoruz?

Kendi payment sistemimizi geliştirmek, kart işleme sistemimizi geliştirdiğimiz anlamına gelmiyor.

Tam tersine mimarinin en önemli sınırlarından biri bu.

Kart numarası, CVV veya tam kart bilgisi Zelrah backend'i veya database'i üzerinde tutulmamalı.

Payment provider bunun için var.

ZELRAH PAYMENT'ın görevi:

```text ₺10,000 ↓ WELCOME15 ↓ -₺1,500 ↓ ₺8,500 final amount ↓ Payment Provider ```

akışının ilk bölümünü güvenilir şekilde yönetmek.

Gerçek kart işlemini ise ödeme sağlayıcısı gerçekleştirir.

Kontrol katmanını geliştiriyoruz; kart altyapısını yeniden icat etmiyoruz.

Neden provider-independent tasarlıyoruz?

Sistemi doğrudan tek sağlayıcıya bağımlı yazmak daha hızlı olabilirdi.

Ancak ödeme kodunun her tarafına sağlayıcıya özgü API çağrıları koymak ileride ciddi teknik borç oluşturur.

Bu nedenle arada bir provider abstraction bulunuyor.

Temel fikir yaklaşık olarak şöyle:

```ts interface PaymentProvider { createCheckoutSession(...) getPayment(...) refundPayment(...) verifyWebhook(...) } ```

Bugün bunun arkasında PayTR bulunabilir.

Başka bir kurulumda iyzico veya farklı bir provider kullanılabilir.

Payment sisteminin geri kalanının bunu bilmesi gerekmez.

Neden admin paneli ayrı bir masaüstü uygulaması?

Bir başka tercih ise yönetim tarafını public payment uygulamasından ayırmak.

ZELRAH PAYMENT iki ana parçadan oluşuyor:

```text Zelrah Payment Admin Desktop application

Zelrah Payment Server API + public payment pages ```

Admin tarafı payment oluşturmak, müşteri yönetmek, promo tanımlamak ve transaction durumlarını görmek için kullanılıyor.

Public server ise müşterinin eriştiği:

```text payment.zelrah.dev/<slug> ```

sayfalarını ve ödeme API'sini çalıştırıyor.

Böylece müşterinin gördüğü checkout ile operasyon tarafındaki yönetim arayüzü birbirinden ayrılmış oluyor.

Checkout neden özellikle sade?

Ödeme ekranının amacı kullanıcıyı etkilemek değil.

Kullanıcının birkaç saniye içerisinde şu soruların cevabını alması gerekiyor:

Kime ödeme yapıyorum?

Ne için ödeme yapıyorum?

Ne kadar ödeyeceğim?

Bir indirim uygulanmış mı?

Ödemeyi nasıl tamamlayacağım?

Bu yüzden checkout'a normal zelrah.dev navigation'ını taşımıyoruz.

Services yok.

Journal yok.

Work yok.

Marketing navigation yok.

Yalnızca işlem var.

Tasarım açısından hedef daha fazla element eklemek değil, belirsizliği azaltmak.

Hazır sistem yerine neden bunu yaptık?

Çünkü bizim için ödeme yalnızca paranın A noktasından B noktasına taşınması değil.

Bir ödeme aynı zamanda:

```text Project + Customer + Payment + Promo + Transaction + Activity ```

ilişkisinin bir parçası.

Hazır bir payment link servisi tahsilat problemini çözebilir.

Biz ise tahsilatın Zelrah içerisindeki yaşam döngüsünü kontrol etmek istedik.

Bu yüzden ödeme sağlayıcısını ortadan kaldırmadık.

Onun etrafındaki sistemi kendimiz tasarladık.

Aradaki fark önemli.

Sonuç

ZELRAH PAYMENT'ın amacı Stripe'ın, PayTR'nin veya başka bir ödeme sağlayıcısının yerine geçmek değil.

Amacı Zelrah'ın ödeme süreçlerinde sahip olmak istediği kontrolü tek yerde toplamak.

Payment oluşturulurken başlayan kayıt;

müşteri,

promo,

checkout,

provider,

webhook

ve transaction sonucuna kadar aynı akış içerisinde ilerliyor.

En temel prensip ise değişmiyor:

Bir indirim yalnızca ekrandaki rakamı değiştirmemeli. Gerçek transaction tutarını değiştirmeli.

Çünkü ödeme sistemlerinde güzel bir arayüz güven verir.

Ama güvenilir bir mimari, o güvenin gerçekten karşılığının olmasını sağlar.