All checks were successful
Deploy / deploy (push) Successful in 8m46s
- 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>
112 lines
5.8 KiB
Markdown
112 lines
5.8 KiB
Markdown
# 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
|
||
|
||
- [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)
|