All checks were successful
Deploy / deploy (push) Successful in 8m46s
- 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>
120 lines
4.6 KiB
TypeScript
120 lines
4.6 KiB
TypeScript
"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}` };
|
||
}
|