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

131 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)