Files
kolaytercih/docs/odeme/iyzico.md
bilalgursen 8f4669e31d
All checks were successful
Deploy / deploy (push) Successful in 8m46s
fix(odeme): action redirect fırlatmaz, hedef döndürür + çift paket çekimine sunucu koruması
- baslatOdeme { error } | { yonlendir } döndürür; SatinAlForm client'ta
  router.push yapar (cacheComponents altında geri dönüşte buton tıklamaya
  sağır kalıyordu); tıklamada ok → dönen halka
- paketi olan yeniden 'paket', paketi olmayan 'topup' başlatamaz
- /odeme, paket alınmışken eski bekleyen paket siparişinin formunu göstermez

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 16:13:30 +03:00

5.8 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.

İ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