Files
kolaytercih/src/lib/iyzico.ts
bilalgursen d2651eadb9
Some checks failed
Deploy / deploy (push) Has been cancelled
fix(odeme): postman-request zincirini standalone trace'ine sok
outputFileTracingIncludes yalnızca eşleşen dosyaları kopyalar, trace etmez:
resources/*.js imaja girdi ama IyzipayResource'un require ettiği
postman-request ve bağımlılıkları girmedi — initialize prod'da
"Cannot find module 'postman-request'" ile düşüyordu. IyzipayResource'u
statik import etmek nft trace'inin tüm zinciri doğru pnpm yerleşimiyle
kopyalamasını sağlar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-21 00:42:44 +03:00

255 lines
8.0 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.
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 — <div id="iyzipay-checkout-form"> 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<CheckoutFormInitSonuc> {
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<CheckoutFormSonuc> {
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);
},
);
});
}