Kod havuzu veri modeli neden kritik? Çünkü dijitalde “geri alma” yok
E-pin/dijital kod satışında en pahalı iki hata şunlardır: (1) aynı kodun iki kez satılması ve (2) stok var görünüp teslim edilememesi. Bu hatalar sadece para kaybettirmez; destek yükünü artırır, chargeback riskini büyütür ve markaya “güvenilmez” etiketi yapıştırır.
Bu yazıda tek bir hedef var: veri modeli seviyesinde çift satış riskini sistematik olarak bitirmek. Yani “kod seçimini dikkatli yapalım” değil; hata yapmayı imkânsızlaştıran bir model kurmak.
1) Kod stoğu “var/yok” değildir: 3 durum + net geçişler
Sağlam bir kod havuzunda kodlar en az şu 3 durumdan birindedir:
- Available (Müsait): Satışa hazır, kimseye ayrılmamış.
- Reserved (Rezerve): Bir sipariş için ayrılmış, ödeme kesinleşmemiş.
- Delivered (Teslim edildi): Müşteriye verilmiş, artık geri dönülmez.
Bu durumlar arasındaki geçişler de net olmalı:
- Available → Reserved (rezervasyon)
- Reserved → Delivered (ödeme doğrulandı)
- Reserved → Available (timeout / ödeme başarısız)
“Delivered → Available” gibi bir geri dönüşü genelde istemezsin; dijitalde bu riskli bir kapıdır.
2) Minimum tablolar: Kod, Sipariş, Kod Teslimatı
Tek tabloyla her şeyi tutmaya çalışmak büyüdükçe karmaşa çıkarır. Basit ama sağlam yapı şöyle ilerler:
A) CodePool (kodların kendisi)
- CodePoolId (PK)
- ProductId (hangi dijital ürün)
- CodeValue (şifreli/korumalı saklama önerilir)
- Status (Available/Reserved/Delivered)
- ReservedAt (ne zaman rezerve edildi)
- DeliveredAt (ne zaman teslim edildi)
- BatchId (tedarikçi yükleme partisi)
B) Order / OrderItem (sipariş tarafı)
- OrderId (PK)
- CustomerId
- PaymentStatus (Pending/Paid/Failed/Cancelled)
- CreatedAt
C) CodeDelivery (sipariş-kod eşleşmesi)
Bu tablo, “hangi sipariş hangi kodu aldı?” sorusunun tek kaynağı olur.
- CodeDeliveryId (PK)
- OrderId
- OrderItemId (adetli siparişlerde önemli)
- CodePoolId
- Status (Reserved/Delivered/Released gibi)
- CreatedAt, UpdatedAt
3) Altın Kural #1: Tekillik (uniqueness) veri tabanında zorunlu olmalı
“Uygulama kodunda kontrol ediyoruz” yeterli değildir. Eşzamanlılıkta (concurrency) iki istek aynı anda geçebilir. O yüzden tekillik DB seviyesinde garanti edilmelidir.
Uygulanabilir tekillik kısıtları
- Bir kod sadece bir kez Delivered olabilir: CodePool.Status zaten bunu ifade eder ama ek güvenlik gerekir.
- Bir CodePoolId sadece bir aktif teslimata bağlanabilsin: CodeDelivery üzerinde “aktif kayıt” tekil olmalı.
- Bir ödeme transaction’ı bir kez işlenebilsin: ayrı bir PaymentTransactions tablosunda Unique(transactionId) gibi.
Özellikle şu mantık çok işe yarar: CodeDelivery tablosunda “Delivered kayıt” için CodePoolId tekil olsun. Böylece aynı kodun iki farklı siparişe Delivered yazılması DB tarafından engellenir.
4) Altın Kural #2: Rezervasyon atomik olmalı (SELECT + UPDATE birlikte)
Çift satışın en sık nedeni, “kodu seçtim” ile “kodu rezerve yaptım” işlemlerinin ayrı ayrı yapılmasıdır. İki istek aynı anda aynı kodu seçebilir.
Doğru yaklaşım: DB’de tek hamlede “müsait bir kodu bul ve rezerve et”.
- Available kodlardan birini seç
- Aynı işlem içinde Status=Reserved yap
- Bu işlem başarısızsa başka kod dene (veya stok yok de)
Bu kural uygulanırsa “eşzamanlı iki satış” senaryosu büyük ölçüde çözülür.
5) Altın Kural #3: Reservation timeout olmadan stok yönetemezsin
Ödeme her zaman başarıyla bitmez. Kullanıcı sayfayı kapatır, banka reddeder, 3DS yarıda kalır. Eğer Reserved kodları serbest bırakmazsan, stok “kilitlenir”.
- Rezervasyona bir TTL ver (örn. 15–30 dk)
- TTL aşınca: CodeDelivery = Released, CodePool.Status = Available
- Order = TimedOut/Cancelled (kuralına göre)
Bu işlem bir job/worker ile periyodik çalışabilir.
6) Altın Kural #4: Ödeme callback’i idempotent olmalı
Ödeme sağlayıcı aynı “success” callback’ini iki kez gönderebilir. Eğer sistem her callback’te teslimat yapıyorsa, çift satış/çift teslimat kaçınılmaz olur.
- Callback geldiğinde önce “bu transaction işlendi mi?” kontrol et
- İşlendiyse hiçbir şey yapma
- İşlenmediyse: siparişi Paid yap, Reserved kodu Delivered yap
Burada DB’de Unique(transactionId) gibi bir kısıt, idempotency’nin sigortasıdır.
7) Altın Kural #5: “Tek kaynak” prensibi (source of truth) belirle
Çoğu sistemde sorun şu yüzden çıkar: kodun durumu bir yerde, siparişin durumu başka yerde, teslimat durumu başka yerde ve birbirini tutmaz.
- “Sipariş hangi kodu aldı?” sorusunun cevabı CodeDelivery tablosunda olmalı.
- CodePool.Status gerçek stoğu göstermeli.
- Order.PaymentStatus ödeme gerçeğini temsil etmeli.
Bu üçlü tutarlı olursa, destek ve muhasebe tarafı da rahatlar.
8) Audit log: Krizde 2 dakikada teşhis için şart
Dijital ürün işinde kriz anı hızlı gelir: “Kod gelmedi”, “Kod çalışmıyor”, “Aynı kod satılmış”. Audit log olmadan her şey “tahmin” olur.
- Kod rezerve edildi (kim, ne zaman, hangi sipariş)
- Ödeme başarı/başarısız callback’i geldi
- Kod teslim edildi (gösterildi/gönderildi)
- Rezerv timeout ile serbest bırakıldı
Bu loglar; hem destek süreçlerini hızlandırır hem de chargeback itirazlarında delil üretir.
9) Dijital ürünlerde ekstra dikkat: Kodun saklanması ve gösterimi
Veri modeli kadar önemli bir konu da kodun “açığa çıkma” yüzeyidir.
- Kodu düz metin tutmak yerine şifreli saklama düşün
- Admin panelde kodu rol bazlı maskele (son 4 karakter gibi)
- Kodun kaç kez görüntülendiğini (gerekliyse) logla
Burada hedef “paranoya” değil; sızıntı riskini makul seviyeye çekmek.
10) Hızlı kontrol listesi: Modelin çift satışa dayanıklı mı?
- 3 durum modeli (Available/Reserved/Delivered) var mı?
- Rezervasyon atomik mi?
- Reserved timeout + otomatik serbest bırakma var mı?
- CodeDelivery üzerinden “Delivered tekilliği” DB ile garanti mi?
- Payment transaction idempotency DB ile garanti mi?
- Audit loglar kriz teşhisine yeterli mi?
Kapanış
Aynı kodu iki kez satmamak, “dikkatli olmakla” değil; veri modeli ve kısıtlarla hatayı imkânsız kılmakla çözülür. Durum yönetimi, atomik rezervasyon, timeout, idempotent callback ve sağlam loglama birleştiğinde, dijital ürün işinde en kritik riskleri daha baştan kapatmış olursun.