Files
kolaytercih/src/features/odeme/odeme-actions.ts
bilalgursen 8f4669e31d
All checks were successful
Deploy / deploy (push) Successful in 8m46s
fix(odeme): action redirect fırlatmaz, hedef döndürür + çift paket çekimine sunucu koruması
- 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>
2026-09-21 16:13:30 +03:00

120 lines
4.6 KiB
TypeScript
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.
"use server";
import { eq } from "drizzle-orm";
import { headers } from "next/headers";
import { getCurrentUser, getSession } from "@/lib/session";
import { appUrl as siteKok } from "@/lib/app-url";
import { appDb, schema } from "@/lib/appdb";
import { URUNLER } from "@/lib/credits";
import { initializeCheckoutForm, initImzaDurumu } from "@/lib/iyzico";
// Action bilerek `redirect()` FIRLATMAZ, hedefi döndürür; yönlendirmeyi client
// yapar (bkz. satin-al-form.tsx). Fırlatılan redirect, client'ta action
// promise'ini reddedip formu RedirectBoundary üzerinden yeniden mount ediyor;
// cacheComponents altında /paket gizli <Activity> içinde canlı tutulduğu için
// bu yarım kalmış durum geri dönüşte butonu tıklamalara sağır bırakıyordu.
export type OdemeBaslatDurum = { error: string } | { yonlendir: string };
const GENEL_HATA =
"Ödeme şu anda başlatılamıyor; kartından çekim yapılmadı. Birazdan tekrar dener misin?";
/** "Bilal Gürsen" → ["Bilal", "Gürsen"]; iyzico ad ve soyadı ayrı ve dolu ister. */
function adSoyadAyir(tamAd: string): [string, string] {
const parcalar = tamAd.trim().split(/\s+/).filter(Boolean);
if (parcalar.length === 0) return ["KolayTercih", "Kullanıcısı"];
if (parcalar.length === 1) return [parcalar[0], parcalar[0]];
return [parcalar.slice(0, -1).join(" "), parcalar[parcalar.length - 1]];
}
export async function baslatOdeme(
product: "paket" | "topup",
): Promise<OdemeBaslatDurum> {
// Ucuz senkron doğrulama pahalı oturum turundan önce (argüman client'tan
// gelir; tipine güvenilmez)
if (product !== "paket" && product !== "topup") {
return { error: "Geçersiz ürün." };
}
const session = await getSession();
if (!session) {
return { yonlendir: `/giris?callback=${encodeURIComponent("/paket")}` };
}
// Arayüz bu kuralları zaten uygular ama bayat sekmeden (ör. paket başka
// sekmede alındı) gelen istek ikinci kez ücret çekmesin.
const kullanici = await getCurrentUser();
if (product === "paket" && kullanici?.hasPaket) {
return { error: "Paketin zaten aktif; yeniden ödeme alınmadı." };
}
if (product === "topup" && !kullanici?.hasPaket) {
return { error: "Ek kredi için önce paketi alman gerekiyor." };
}
const urun = URUNLER[product];
const orderId = crypto.randomUUID();
await appDb.insert(schema.orders).values({
id: orderId,
userId: session.user.id,
product,
amountKurus: urun.amountKurus,
credits: urun.credits,
status: "pending",
createdAt: new Date(),
});
const h = await headers();
const buyerIp =
h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "85.34.78.112";
const appUrl = siteKok();
const [ad, soyad] = adSoyadAyir(session.user.name ?? "");
let init;
try {
init = await initializeCheckoutForm({
orderId,
fiyatKurus: urun.amountKurus,
urunAdi: `KolayTercih — ${urun.label}`,
email: session.user.email,
userId: session.user.id,
ad,
soyad,
// iyzico bu adrese hem başarıyı hem başarısızlığı POST'lar; geçerli SSL şart
callbackUrl: `${appUrl}/api/odeme/callback`,
buyerIp,
});
} catch (err) {
if (err instanceof Error && err.message === "IYZICO_KEYS_MISSING") {
// Yapılandırma detayı log'a; kullanıcıya altyapı sızdırmayan mesaj
console.error("[odeme] iyzico anahtarları eksik — ödeme başlatılamadı");
return { error: GENEL_HATA };
}
console.error("[odeme] iyzico initialize hatası", err);
return { error: GENEL_HATA };
}
if (init.status !== "success" || !init.checkoutFormContent || !init.token) {
// initialize aşamasında karta hiç dokunulmaz: buradaki hatalar kart/kullanıcı
// kaynaklı değil, bizim yapılandırmamızdandır (anahtar, üye işyeri ayarı,
// sepet kuralı). iyzico'nun mesajı log'a gider, kullanıcıya genel mesaj.
console.error("[odeme] initialize başarısız", {
orderId,
errorCode: init.errorCode,
errorMessage: init.errorMessage,
});
return { error: GENEL_HATA };
}
// Yanıtın bütünlüğü: imza tutmuyorsa kullanıcıyı o ödeme sayfasına yollama.
if (initImzaDurumu(init) === "gecersiz") {
console.error("[odeme] initialize imzası geçersiz", { orderId });
return { error: GENEL_HATA };
}
// Form içeriği de saklanır: /odeme/[siparis] sayfası yenilense bile aynı
// token'lı form yeniden gösterilir, ikinci bir initialize gerekmez.
await appDb
.update(schema.orders)
.set({ iyzicoToken: init.token, iyzicoFormContent: init.checkoutFormContent })
.where(eq(schema.orders.id, orderId));
return { yonlendir: `/odeme?siparis=${orderId}` };
}