# 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=` 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:///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ı: . 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 - [CF-Initialize](https://docs.iyzico.com/en/payment-methods/checkoutform/cf-implementation/cf-initialize) - [CF-Retrieve](https://docs.iyzico.com/en/payment-methods/checkoutform/cf-implementation/cf-retrieve) - [Response Signature Validation](https://docs.iyzico.com/en/advanced/response-signature-validation) - [Webhook](https://docs.iyzico.com/en/advanced/webhook)