All checks were successful
Deploy / deploy (push) Successful in 8m50s
- 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>
131 lines
7.3 KiB
Markdown
131 lines
7.3 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.
|
||
- **Ö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
|
||
|
||
- [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)
|