Files
kolaytercih/docs/odeme/iyzico.md
bilalgursen 024a2a49c4
All checks were successful
Deploy / deploy (push) Successful in 8m50s
fix(odeme): önce kredi sonra paid damgası; çift pakette iade akışı; UNIQUE tespiti
- 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>
2026-09-21 16:36:24 +03:00

7.3 KiB
Raw Blame History

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:3000 ile uçtan uca test edilemez: iyzico callback adresinden geçerli SSL ister. Sandbox testinde tünel (cloudflared/ngrok) açıp APP_URL'i o https adrese ayarla.

Akış

  1. baslatOdeme (src/features/odeme/odeme-actions.ts) — orders satırını pending olarak yazar, checkoutFormInitialize çağırır, dönen token'ı siparişe iliştirir ve /odeme?siparis=<id> adresini döndürür (redirect() fırlatmaz); yönlendirmeyi SatinAlForm client'ta router.push ile yapar. conversationId = basketId = sipariş id'miz. Oturum yoksa /giris?callback=/paket döner. Sunucu koruması: paketi olan yeniden paket, paketi olmayan topup başlatamaz; /odeme sayfası da paket alınmışken eski bekleyen paket siparişinin formunu göstermez (bayat sekme / ikinci cihazdan çift çekim önlemi).
  2. Kullanıcı ödemeyi bitirince iyzico /api/odeme/callback adresine cross-site POST atar; gövdede yalnızca token vardır. SameSite=Lax nedeniyle session çerezi gelmez, bu yüzden kullanıcı token'dan çözülür.
  3. odemeyiSonuclandir (src/lib/odeme.ts) iyzico'ya checkoutForm.retrieve ile sorar ve krediyi idempotent tanımlar. Kullanıcı 303 ile /odeme/sonuc?siparis=… sayfasına düşer.
  4. /odeme/sonuc self-healing'dir: sipariş hâlâ pending ise aynı fonksiyonu tekrar çağırır (callback kaybolduysa kurtarır).
  5. /api/odeme/webhook iyzico bildirimini karşılar — sekmesini kapatan ya da fraud incelemesinde bekleyen ödemeler için yedek yol.

Doğruluk kuralları (bunlara dokunurken dikkat)

  • Kredi yalnızca retrieve yanı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.
  • paid geçişi koşulludur (WHERE status != 'paid'): eşzamanlı callback + sayfa render'ı ikinci kez kredi yazamaz. grantCredits'teki UNIQUE(reason, ref_id) ikinci katman güvencedir.
  • Ara durumlar failed damgalanmaz. 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: 1 onaylı → kredi verilir. 0 incelemede → pending bırakılır (çekim kesinleşmemiştir). -1 reddedildi → failed.
  • Erken failed kurtarılabilir: geçiş koşulu status != 'paid' olduğu için yanlışlıkla failed damgalanmış bir sipariş, iyzico SUCCESS derse yine paid'e döner. Para çekildiyse kredi mutlaka tanımlanır.
  • Tutar kontrolü: paidPrice sipariş tutarıyla eşleşmiyorsa kredi otomatik tanımlanmaz, log'a düşer.
  • Önce kredi, sonra paid damgası. odemeyiSonuclandir krediyi yazıp ardından siparişi paid yapar. Ters sırada araya kesinti girerse sipariş paid görünür ama kredi hiç yazılmaz. paid gö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ş yine paid damgalanır, odeme_tamamlandi atı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=0 satırlar kullanıcının ilk /odeme/sonuc ziyaretinde kendiliğinden onarılır; has_paket=1 satı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.

Kaynaklar