import "server-only";
import { createHmac, timingSafeEqual } from "node:crypto";
import Iyzipay from "iyzipay";
// Yalnızca nft trace için: Iyzipay.js resource sınıflarını readdir ile dinamik
// yüklediğinden IyzipayResource ve onun require ettiği postman-request zinciri
// statik analizde görünmez, standalone imaja kopyalanmaz ve initialize prod'da
// MODULE_NOT_FOUND ile düşer. Bu import zinciri trace'e sokar; resources/*.js
// dosyalarını ise next.config.ts'teki outputFileTracingIncludes taşır.
import "iyzipay/lib/IyzipayResource";
// iyzipay CJS + callback tabanlı; burada promisify'lı ince bir katman var.
// Anahtarlar boşsa (henüz sandbox hesabı yoksa) çağrı anlaşılır bir hatayla düşer.
//
// Doğrulama: iyzico her yanıtta HMAC-SHA256 bir `signature` döner ve webhook'ta
// X-IYZ-SIGNATURE-V3 başlığı gönderir. Alan listeleri ve sıraları iyzico
// dokümantasyonundan birebir alınmıştır (docs.iyzico.com → Response Signature
// Validation / Webhook); değiştirilirse imzalar tutmaz.
const globalForIyzi = globalThis as unknown as { __iyzipay?: Iyzipay };
const SANDBOX_URI = "https://sandbox-api.iyzipay.com";
function anahtarlar() {
const apiKey = process.env.IYZICO_API_KEY;
const secretKey = process.env.IYZICO_SECRET_KEY;
if (!apiKey || !secretKey) throw new Error("IYZICO_KEYS_MISSING");
const uri = process.env.IYZICO_BASE_URL ?? SANDBOX_URI;
// Sessizce sandbox'a düşmek prod'da "ödeme alındı ama para yok" demektir;
// env unutulursa en azından log'da bağırsın.
if (process.env.NODE_ENV === "production" && uri === SANDBOX_URI) {
console.warn(
"[odeme] UYARI: prod'da iyzico sandbox URI kullanılıyor — IYZICO_BASE_URL ayarlanmamış",
);
}
return { apiKey, secretKey, uri };
}
function getClient(): Iyzipay {
const { apiKey, secretKey, uri } = anahtarlar();
if (!globalForIyzi.__iyzipay) {
globalForIyzi.__iyzipay = new Iyzipay({ apiKey, secretKey, uri });
}
return globalForIyzi.__iyzipay;
}
// ---- imza doğrulama ----
/**
* iyzico imzası: alanlar ":" ile birleştirilir, secretKey ile HMAC-SHA256'lanır
* ve hex'e çevrilir. Boş/eksik alan boş string olarak katılır (iyzico da öyle
* hesaplar), sıra kritiktir.
*/
function imzaHesapla(
alanlar: readonly (string | number | null | undefined)[],
secretKey: string,
) {
const veri = alanlar.map((a) => (a == null ? "" : String(a))).join(":");
return createHmac("sha256", secretKey).update(veri).digest("hex");
}
/** Sabit zamanlı karşılaştırma — uzunluk farkında timingSafeEqual patlar. */
function esitMi(a: string, b: string) {
const ab = Buffer.from(a, "utf8");
const bb = Buffer.from(b, "utf8");
return ab.length === bb.length && timingSafeEqual(ab, bb);
}
/**
* İmzada fiyatlar sondaki sıfırlar atılmış haliyle geçer: "10.50" → "10.5",
* "299.00" → "299". (iyzico'nun kendi örneği parseFloat(x).toString().)
*/
function fiyatSadelestir(p: string | number | undefined) {
if (p == null) return "";
const n = typeof p === "number" ? p : Number.parseFloat(p);
return Number.isFinite(n) ? String(n) : String(p);
}
export type ImzaDurumu = "gecerli" | "gecersiz" | "yok";
function imzaKarsilastir(
alanlar: readonly (string | number | null | undefined)[],
imza: string | undefined,
): ImzaDurumu {
if (!imza) return "yok";
const { secretKey } = anahtarlar();
return esitMi(imzaHesapla(alanlar, secretKey), imza) ? "gecerli" : "gecersiz";
}
// ---- checkout form ----
export interface CheckoutFormInitSonuc {
status: string;
token?: string;
paymentPageUrl?: string;
/** Kendi sayfamıza gömülen form script'i —
ister */
checkoutFormContent?: string;
conversationId?: string;
signature?: string;
errorCode?: string;
errorMessage?: string;
}
export interface CheckoutFormSonuc {
status: string;
paymentStatus?: string; // SUCCESS | FAILURE | INIT_THREEDS | PENDING_CREDIT ...
fraudStatus?: number; // 1 onaylı, 0 incelemede, -1 reddedildi
conversationId?: string;
basketId?: string;
paymentId?: string;
currency?: string;
price?: string | number;
paidPrice?: string | number;
token?: string;
signature?: string;
errorCode?: string;
errorMessage?: string;
}
/** initialize yanıtı imzası: conversationId:token */
export function initImzaDurumu(r: CheckoutFormInitSonuc): ImzaDurumu {
return imzaKarsilastir([r.conversationId, r.token], r.signature);
}
/**
* retrieve yanıtı imzası:
* paymentStatus:paymentId:currency:basketId:conversationId:paidPrice:price:token
*/
export function retrieveImzaDurumu(r: CheckoutFormSonuc): ImzaDurumu {
return imzaKarsilastir(
[
r.paymentStatus,
r.paymentId,
r.currency,
r.basketId,
r.conversationId,
fiyatSadelestir(r.paidPrice),
fiyatSadelestir(r.price),
r.token,
],
r.signature,
);
}
/**
* Webhook X-IYZ-SIGNATURE-V3 (HPP formatı — checkout form bu formatta gelir).
* Anahtarın kendisi de veriye katılır:
* HMAC(secretKey, secretKey + iyziEventType + iyziPaymentId + token +
* paymentConversationId + status)
*/
export function webhookImzaDurumu(
govde: {
iyziEventType?: string;
iyziPaymentId?: string | number;
token?: string;
paymentConversationId?: string;
status?: string;
},
imza: string | undefined,
): ImzaDurumu {
if (!imza) return "yok";
const { secretKey } = anahtarlar();
const veri =
secretKey +
(govde.iyziEventType ?? "") +
(govde.iyziPaymentId ?? "") +
(govde.token ?? "") +
(govde.paymentConversationId ?? "") +
(govde.status ?? "");
const beklenen = createHmac("sha256", secretKey).update(veri).digest("hex");
return esitMi(beklenen, imza) ? "gecerli" : "gecersiz";
}
export function initializeCheckoutForm(opts: {
orderId: string;
fiyatKurus: number;
urunAdi: string;
email: string;
userId: string;
ad: string;
soyad: string;
callbackUrl: string;
buyerIp: string;
}): Promise {
const price = (opts.fiyatKurus / 100).toFixed(2);
// iyzico buyer bloğu zorunlu alanlar ister; dijital üründe adres sembolik
const adres = {
contactName: `${opts.ad} ${opts.soyad}`.trim(),
city: "Istanbul",
country: "Turkey",
address: "Dijital teslimat",
};
const request = {
locale: Iyzipay.LOCALE.TR,
conversationId: opts.orderId,
price,
paidPrice: price,
currency: Iyzipay.CURRENCY.TRY,
basketId: opts.orderId,
paymentGroup: Iyzipay.PAYMENT_GROUP.PRODUCT,
callbackUrl: opts.callbackUrl,
enabledInstallments: [1],
buyer: {
id: opts.userId,
name: opts.ad,
surname: opts.soyad,
gsmNumber: "+905000000000",
email: opts.email,
identityNumber: "11111111111",
registrationAddress: adres.address,
ip: opts.buyerIp,
city: adres.city,
country: adres.country,
},
// Sepet tamamen VIRTUAL; iyzico shippingAddress'i bu durumda zorunlu
// tutmuyor ama göndermek de sorun değil, fraud skorunda tutarlılık sağlar.
shippingAddress: adres,
billingAddress: adres,
basketItems: [
{
id: opts.orderId,
name: opts.urunAdi,
category1: "Dijital Hizmet",
itemType: Iyzipay.BASKET_ITEM_TYPE.VIRTUAL,
price,
},
],
};
return new Promise((resolve, reject) => {
getClient().checkoutFormInitialize.create(
request as never,
(err: unknown, result: CheckoutFormInitSonuc) => {
if (err) reject(err);
else resolve(result);
},
);
});
}
/**
* conversationId BİLEREK gönderilmez: gönderilirse iyzico onu aynen yankılar ve
* "yanıttaki conversationId siparişimizle aynı mı" kontrolü anlamsızlaşır.
* Göndermeyince ödemenin kendi conversationId'si döner ve doğrulanabilir.
*/
export function retrieveCheckoutForm(token: string): Promise {
return new Promise((resolve, reject) => {
getClient().checkoutForm.retrieve(
{ locale: Iyzipay.LOCALE.TR, token } as never,
(err: unknown, result: CheckoutFormSonuc) => {
if (err) reject(err);
else resolve(result);
},
);
});
}