- odemeyiSonuclandir krediyi yazıp sonra paid damgalar; araya kesinti girerse
sipariş pending kalır ve yeniden denenir (eskiden: paid görünür, kredi yok).
paid + deftersiz eski siparişler ilk ziyarette onarılır
- paket tek seferlik: grantCredits({ paketTekil }) zaten paketliye yazmaz
(kontrol UPDATE'in WHERE'inde). Sipariş paid damgalanır, senkron log +
destek@ e-postası, /odeme/sonuc 'Bu ödeme iade edilecek' kartı, /kosullar#iade
- drizzle DrizzleQueryError üst mesajında 'UNIQUE' yok → idempotent no-op yerine
hata fırlıyordu (grant + iki spend yolu); cause zincirine bakılıyor
- callback geçici DB hatasında 500 yerine sonuç sayfasına yönlendirir
- krediKaydiVarMi (reason, ref_id) indeksini kullanır; haftalık mutabakat sorgusu
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7.3 KiB
iyzico ödeme entegrasyonu
Ödeme akışı iyzico Checkout Form (CF) üzerinden yürür: kart bilgisi hiçbir
zaman bize ulaşmaz; kart formu iyzico iframe'i olarak kendi /odeme sayfamıza
gömülür.
Ortam değişkenleri
| Değişken | Açıklama |
|---|---|
IYZICO_API_KEY |
Merchant Portal → Ayarlar → API anahtarları |
IYZICO_SECRET_KEY |
Aynı ekran. İmza doğrulamasında da kullanılır |
IYZICO_BASE_URL |
Sandbox: https://sandbox-api.iyzipay.com · Canlı: https://api.iyzipay.com |
APP_URL |
callback ve yönlendirme URL'lerinin kökü; https ve geçerli SSL şart. Verilmezse BETTER_AUTH_URL'e düşer |
IYZICO_BASE_URL verilmezse sandbox'a düşülür. Prod'da bu durum log'a uyarı
basar — canlıya çıkarken bu satır mutlaka ayarlanmalı.
Kök adres NEXT_PUBLIC_APP_URL ile verilemez: NEXT_PUBLIC_* değişkenleri
build sırasında koda gömülür, Docker build'i ise .env.production'ı görmez
(.dockerignore .env*'i dışlar). Sonuç, env doğru girilmiş olsa bile imaja
http://localhost:3000 çakılması ve iyzico'nun initialize'ı reddetmesiydi.
Ayrıntı: src/lib/app-url.ts.
Lokal
http://localhost:3000ile uçtan uca test edilemez: iyzico callback adresinden geçerli SSL ister. Sandbox testinde tünel (cloudflared/ngrok) açıpAPP_URL'i o https adrese ayarla.
Akış
baslatOdeme(src/features/odeme/odeme-actions.ts) —orderssatırınıpendingolarak yazar,checkoutFormInitializeçağırır, dönentoken'ı siparişe iliştirir ve/odeme?siparis=<id>adresini döndürür (redirect()fırlatmaz); yönlendirmeyiSatinAlFormclient'tarouter.pushile yapar.conversationId=basketId= sipariş id'miz. Oturum yoksa/giris?callback=/paketdöner. Sunucu koruması: paketi olan yenidenpaket, paketi olmayantopupbaşlatamaz;/odemesayfası da paket alınmışken eski bekleyen paket siparişinin formunu göstermez (bayat sekme / ikinci cihazdan çift çekim önlemi).- Kullanıcı ödemeyi bitirince iyzico
/api/odeme/callbackadresine cross-site POST atar; gövdede yalnızcatokenvardır. SameSite=Lax nedeniyle session çerezi gelmez, bu yüzden kullanıcı token'dan çözülür. odemeyiSonuclandir(src/lib/odeme.ts) iyzico'yacheckoutForm.retrieveile sorar ve krediyi idempotent tanımlar. Kullanıcı 303 ile/odeme/sonuc?siparis=…sayfasına düşer./odeme/sonucself-healing'dir: sipariş hâlâpendingise aynı fonksiyonu tekrar çağırır (callback kaybolduysa kurtarır)./api/odeme/webhookiyzico bildirimini karşılar — sekmesini kapatan ya da fraud incelemesinde bekleyen ödemeler için yedek yol.
Doğruluk kuralları (bunlara dokunurken dikkat)
- Kredi yalnızca
retrieveyanıtına göre tanımlanır. Ne callback gövdesine ne webhook gövdesine güvenilir; ikisi de sadece "iyzico'ya tekrar sor" tetikleyicisidir. paidgeçişi koşulludur (WHERE status != 'paid'): eşzamanlı callback + sayfa render'ı ikinci kez kredi yazamaz.grantCredits'tekiUNIQUE(reason, ref_id)ikinci katman güvencedir.- Ara durumlar
faileddamgalanmaz.INIT_THREEDS,CALLBACK_THREEDS,PENDING_CREDIT,INIT_BANK_TRANSFER… ödemenin sonuçlanmadığı anlamına gelir; damgalarsak dakikalar sonra SUCCESS'e dönen ödemede kredi kaybolur. fraudStatus:1onaylı → kredi verilir.0incelemede →pendingbırakılır (çekim kesinleşmemiştir).-1reddedildi →failed.- Erken
failedkurtarılabilir: geçiş koşulustatus != 'paid'olduğu için yanlışlıklafaileddamgalanmış bir sipariş, iyzico SUCCESS derse yinepaid'e döner. Para çekildiyse kredi mutlaka tanımlanır. - Tutar kontrolü:
paidPricesipariş tutarıyla eşleşmiyorsa kredi otomatik tanımlanmaz, log'a düşer. - Önce kredi, sonra
paiddamgası.odemeyiSonuclandirkrediyi yazıp ardından siparişipaidyapar. Ters sırada araya kesinti girerse siparişpaidgörünür ama kredi hiç yazılmaz.paidgörülen ama defterde kaydı olmayan siparişte kredi yeniden denenir (eski kayıpların onarımı). - Çift paket = iade (Bilal kararı, 21.09.2026). Paket tek seferliktir:
grantCredits({ paketTekil })kullanıcı zaten paketliyse yazmaz (neden: "zaten-paketli", kontrol bakiye UPDATE'inin WHERE'inde → eşzamanlı iki ödemede de tek paket). Sipariş yinepaiddamgalanır,odeme_tamamlandiatılmaz,destek@kolaytercih.com'a "İade gerekli" e-postası gider ve/odeme/sonuc"Bu ödeme iade edilecek" kartını gösterir (iadeEdilecekMi: paid + paket + defterde kayıt yok). İade otomatik DEĞİL: iyzico panelinden elle yapılır. İade sonrası defter satırı silinmez; gerekirse ters kayıt atılır. - Haftalık mutabakat (salt okunur). Ödenmiş ama kredisi yazılmamış siparişler:
SELECT o.id, o.user_id, u.has_paket FROM orders o JOIN user u ON u.id=o.user_id LEFT JOIN credit_ledger l ON l.ref_id=o.id AND l.reason IN ('purchase','topup') WHERE o.status='paid' AND l.id IS NULL;—has_paket=0satırlar kullanıcının ilk/odeme/sonucziyaretinde kendiliğinden onarılır;has_paket=1satırlar iade bekleyen çift paket ödemesidir (e-posta kaçmış olabilir → elle iade et).
İmza doğrulaması
iyzico yanıtlarında HMAC-SHA256 signature döner; alanlar : ile birleşir ve
fiyatlarda sondaki sıfırlar atılır (299.00 → 299). Tümü
src/lib/iyzico.ts içinde:
| Yer | Alan sırası |
|---|---|
initImzaDurumu |
conversationId:token |
retrieveImzaDurumu |
paymentStatus:paymentId:currency:basketId:conversationId:paidPrice:price:token |
webhookImzaDurumu (V3, HPP) |
HMAC(secret, secret + iyziEventType + iyziPaymentId + token + paymentConversationId + status) |
Karar kuralı: imza tutmuyorsa işlem reddedilir; imza alanı hiç yoksa (hesapta kapalıysa) akış sürer ve log'a uyarı düşer — retrieve zaten kimliği doğrulanmış sunucu-sunucu çağrısıdır, bu yüzden imza yokluğu ödemeyi bloklamaz.
Webhook kurulumu
Merchant Portal → Ayarlar → İşyeri Ayarları → İşyeri Bildirimleri →
https://<alan-adı>/api/odeme/webhook (HTTPS zorunlu).
X-IYZ-SIGNATURE-V3 başlığının gönderilmesi ayrıca aktifleştirilmelidir
(entegrasyon@iyzico.com). Aktif değilse başlık gelmez; route yine güvenlidir
çünkü durumu gövdeden değil retrieve'den okur.
iyzico 2xx alana kadar 15 dakika arayla 3 kez dener — bu yüzden işleyemediğimiz durumlarda bile 200 döneriz, yalnızca geçersiz imzada 401.
Test
Sandbox test kartları: https://docs.iyzico.com/en/add-ons/test-cards. Son kullanma tarihi gelecekte olmak kaydıyla SKT ve CVV serbesttir.
Dev panelinden (src/components/dev/dev-panel.tsx) iyzico'ya hiç gitmeden
"ödenmiş sahte sipariş" üretilebilir — sonuç ekranını denemek için.