# Ödeme akışı

> iyzico veya PayTR ile güvenli ödeme; sepet, onaylar, callback ve iade.

## Kurulum

```bash
npx trkit add odeme odeme-onaylari
```

```bash title=".env.local"
TRKIT_PAY_PROVIDER=iyzico      # iyzico | paytr | mock
IYZICO_API_KEY=sandbox-...
IYZICO_SECRET_KEY=sandbox-...
IYZICO_SANDBOX=1
```

Geliştirmede `TRKIT_PAY_PROVIDER=mock` ile gerçek sağlayıcı olmadan tüm akışı deneyebilirsiniz.

## Akış

```text
Sepet → Ödeme sayfası (onaylar) → createCheckout() → Sağlayıcı formu
     → Sağlayıcı /api/odeme/callback'e bildirir → verifyCallback() → sipariş güncellenir
     → Kullanıcı sonuç sayfasına yönlendirilir
```

## 1. Ödeme sayfası ve onaylar

Mesafeli Sözleşmeler Yönetmeliği'ne göre ön bilgilendirme formu, sipariş onayından **önce** ve sipariş bilgileriyle gösterilmeli:

```tsx title="app/odeme/page.tsx"
import { generate, toHtml } from "@trkit-app-pack/legal";
import config from "@/trkit.config";
import { OdemeOnaylari } from "@/components/trkit/odeme-onaylari";

export default async function OdemeSayfasi() {
  const sepet = await sepetiGetir();
  const order = {
    no: sepet.id, tarih: new Date().toISOString(), odemeYontemi: "Kredi kartı",
    alici: { adSoyad: sepet.adSoyad, adres: sepet.adres, telefon: sepet.telefon, eposta: sepet.eposta },
    urunler: sepet.kalemler.map((k) => ({ ad: k.ad, adet: k.adet, birimFiyat: k.fiyatKurus })),
    kargoUcreti: sepet.kargoKurus,
  };
  return (
    <form action={odemeyiBaslat}>
      <OdemeOnaylari
        onBilgilendirmeHtml={toHtml(generate("on-bilgilendirme", config, { order }).document)}
        mesafeliSatisHtml={toHtml(generate("mesafeli-satis", config, { order }).document)}
      />
      <button>Ödemeye geç</button>
    </form>
  );
}
```

## 2. Ödemeyi başlatma (sunucu)

```ts title="app/odeme/actions.ts"
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { pay } from "@/lib/pay";

export async function odemeyiBaslat(formData: FormData) {
  if (formData.get("onay[sozlesme]") !== "on") throw new Error("Sözleşme onayı gerekli");
  const sepet = await sepetiGetir();
  const ip = (await headers()).get("x-forwarded-for")?.split(",")[0] ?? "127.0.0.1";

  const session = await pay.createCheckout({
    orderId: sepet.id,                     // PayTR: yalnızca harf ve rakam
    amount: sepet.toplamKurus,             // kalemlerin toplamına eşit olmalı
    buyer: {
      id: sepet.uyeId, name: sepet.ad, surname: sepet.soyad,
      email: sepet.eposta, phone: sepet.telefon, ip,
      address: { city: sepet.il, district: sepet.ilce, line: sepet.adres },
    },
    items: [
      ...sepet.kalemler.map((k) => ({ id: k.id, name: k.ad, category: k.kategori, unitPrice: k.fiyatKurus, quantity: k.adet })),
      { id: "kargo", name: "Kargo", category: "Kargo", unitPrice: sepet.kargoKurus, type: "virtual" as const },
    ],
    callbackUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/api/odeme/callback`,
    successUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/odeme/basarili`,
    failUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/odeme/basarisiz`,
    installments: [1, 2, 3, 6],
  });

  if (session.paymentPageUrl) redirect(session.paymentPageUrl); // iyzico
  redirect(`/odeme/form?token=${session.token}`);               // PayTR: iframeUrl'i <iframe> ile göster
}
```

## 3. Callback

`npx trkit add odeme` ile gelen `app/api/odeme/callback/route.ts` bildirimi doğrular. Yapmanız gereken `siparisiGuncelle()` fonksiyonunu doldurmak:

```ts
async function siparisiGuncelle(result: PaymentResult) {
  const siparis = await db.siparis.find(result.orderId);
  if (!siparis || siparis.durum === "odendi") return;          // idempotent
  if (result.status !== "success") return db.siparis.update(result.orderId, { durum: "basarisiz" });
  if (result.paidAmount! < siparis.toplamKurus) throw new Error("Tutar uyuşmuyor");
  await db.siparis.update(result.orderId, { durum: "odendi", odemeId: result.paymentId });
}
```

## Güvenlik kuralları

- **Başarı sayfasına güvenmeyin.** `/odeme/basarili` adresine herkes gidebilir. Siparişin ödendiğine yalnızca callback karar verir.
- **Sırlar sunucuda.** `lib/pay.ts` `server-only` içerir; client component'ten içe aktarılırsa derleme hata verir.
- **Tutarı sunucuda hesaplayın.** Formdan gelen fiyatı kullanmayın.
- **Kuruş kullanın.** `0.1 + 0.2 !== 0.3`. `toKurus("129,50")` → `12950`.
- **PayTR "OK" bekler.** `callbackResponse()` bunu döndürür; aynı bildirim tekrar gelebilir.
- **iyzico fraud incelemesi.** `fraudStatus` incelemedeyse sonuç `pending` döner; siparişi kargoya vermeyin.

## Sağlayıcı değiştirmek

`.env.local` içinde `TRKIT_PAY_PROVIDER=paytr` ve PayTR anahtarları. PayTR panelinde Bildirim URL'sini `https://alanadiniz/api/odeme/callback` olarak ayarlayın. Kodunuz değişmez.

## İade

```ts
await pay.refund({ paymentId: siparis.odemeId, amount: 5900, ip });
```
