trkit
Menü

Rehberler

Ödeme akışı

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

.mdChatGPT'de açClaude'da aç

Kurulum

npx trkit add odeme odeme-onaylari
.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ış

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:

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)

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:

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

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