better-payment
Version:
Unified payment gateway library for Turkish payment providers
1,571 lines (1,564 loc) • 61 kB
JavaScript
import axios from 'axios';
import * as crypto2 from 'crypto';
import crypto2__default from 'crypto';
// src/core/BetterPayConfig.ts
var ProviderType = /* @__PURE__ */ ((ProviderType2) => {
ProviderType2["IYZICO"] = "iyzico";
ProviderType2["PAYTR"] = "paytr";
ProviderType2["AKBANK"] = "akbank";
return ProviderType2;
})(ProviderType || {});
// src/core/PaymentProvider.ts
var PaymentProvider = class {
constructor(config) {
this.config = {
locale: "tr",
...config
};
this.validateConfig();
}
/**
* Yapılandırmayı doğrula
*/
validateConfig() {
if (!this.config.apiKey) {
throw new Error("API Key is required");
}
if (!this.config.secretKey) {
throw new Error("Secret Key is required");
}
if (!this.config.baseUrl) {
throw new Error("Base URL is required");
}
}
/**
* BIN sorgulama
*/
async binCheck(binNumber) {
throw new Error(`BIN check for ${binNumber} not supported by this provider`);
}
};
// src/types/index.ts
var PaymentStatus = /* @__PURE__ */ ((PaymentStatus2) => {
PaymentStatus2["SUCCESS"] = "success";
PaymentStatus2["FAILURE"] = "failure";
PaymentStatus2["PENDING"] = "pending";
PaymentStatus2["CANCELLED"] = "cancelled";
return PaymentStatus2;
})(PaymentStatus || {});
var Currency = /* @__PURE__ */ ((Currency2) => {
Currency2["TRY"] = "TRY";
Currency2["USD"] = "USD";
Currency2["EUR"] = "EUR";
Currency2["GBP"] = "GBP";
return Currency2;
})(Currency || {});
var BasketItemType = /* @__PURE__ */ ((BasketItemType2) => {
BasketItemType2["PHYSICAL"] = "PHYSICAL";
BasketItemType2["VIRTUAL"] = "VIRTUAL";
return BasketItemType2;
})(BasketItemType || {});
function generateIyzicoAuthStringV2(apiKey, secretKey, randomString, uri, requestBody) {
const signature = crypto2__default.createHmac("sha256", secretKey).update(randomString + uri + requestBody).digest("hex");
const authorizationParams = [
`apiKey:${apiKey}`,
`randomKey:${randomString}`,
`signature:${signature}`
];
const base64Auth = Buffer.from(authorizationParams.join("&")).toString("base64");
return `IYZWSv2 ${base64Auth}`;
}
function generateRandomString() {
return Date.now() + Math.random().toString(36).slice(2);
}
function createIyzicoHeaders(apiKey, secretKey, uri, requestBody, pkiString) {
const randomString = generateRandomString();
const authStringV2 = generateIyzicoAuthStringV2(
apiKey,
secretKey,
randomString,
uri,
requestBody
);
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: authStringV2,
"x-iyzi-rnd": randomString,
"x-iyzi-client-version": "better-pay-1.0.0"
};
return headers;
}
// src/providers/iyzico/index.ts
var Iyzico = class extends PaymentProvider {
constructor(config) {
super(config);
this.client = axios.create({
baseURL: this.config.baseUrl,
timeout: 3e4
});
}
/**
* İyzico status'ünü PaymentStatus'e çevir
*/
mapStatus(iyzicoStatus) {
switch (iyzicoStatus) {
case "success":
return "success" /* SUCCESS */;
case "failure":
return "failure" /* FAILURE */;
default:
return "pending" /* PENDING */;
}
}
/**
* Genel request gönderme metodu
*/
async sendRequest(endpoint, data) {
const requestBody = JSON.stringify(data);
const headers = createIyzicoHeaders(
this.config.apiKey,
this.config.secretKey,
endpoint,
requestBody
);
const response = await this.client.post(endpoint, requestBody, {
headers: {
...headers,
"Content-Type": "application/json"
}
});
return response.data;
}
/**
* Uygulama request'ini İyzico formatına çevir
*/
mapToIyzicoRequest(request) {
return {
locale: this.config.locale || "tr",
conversationId: request.conversationId,
price: request.price,
paidPrice: request.paidPrice,
currency: request.currency,
installment: 1,
basketId: request.basketId,
paymentChannel: "WEB",
paymentGroup: "PRODUCT",
paymentCard: {
cardHolderName: request.paymentCard.cardHolderName,
cardNumber: request.paymentCard.cardNumber,
expireMonth: request.paymentCard.expireMonth,
expireYear: request.paymentCard.expireYear,
cvc: request.paymentCard.cvc,
registerCard: request.paymentCard.registerCard ? 1 : 0
},
buyer: {
id: request.buyer.id,
name: request.buyer.name,
surname: request.buyer.surname,
gsmNumber: request.buyer.gsmNumber,
email: request.buyer.email,
identityNumber: request.buyer.identityNumber,
registrationAddress: request.buyer.registrationAddress,
ip: request.buyer.ip,
city: request.buyer.city,
country: request.buyer.country,
zipCode: request.buyer.zipCode
},
shippingAddress: {
contactName: request.shippingAddress.contactName,
city: request.shippingAddress.city,
country: request.shippingAddress.country,
address: request.shippingAddress.address,
zipCode: request.shippingAddress.zipCode
},
billingAddress: {
contactName: request.billingAddress.contactName,
city: request.billingAddress.city,
country: request.billingAddress.country,
address: request.billingAddress.address,
zipCode: request.billingAddress.zipCode
},
basketItems: request.basketItems.map((item) => ({
id: item.id,
name: item.name,
category1: item.category1,
category2: item.category2,
itemType: item.itemType,
price: item.price
}))
};
}
/**
* Checkout Form request'ini İyzico formatına çevir
*/
mapToIyzicoCheckoutFormRequest(request) {
return {
locale: this.config.locale || "tr",
conversationId: request.conversationId,
price: request.price,
paidPrice: request.paidPrice,
currency: request.currency,
basketId: request.basketId,
paymentGroup: "PRODUCT",
paymentChannel: "WEB",
callbackUrl: request.callbackUrl,
enabledInstallments: request.enabledInstallments,
buyer: {
id: request.buyer.id,
name: request.buyer.name,
surname: request.buyer.surname,
gsmNumber: request.buyer.gsmNumber,
email: request.buyer.email,
identityNumber: request.buyer.identityNumber,
registrationAddress: request.buyer.registrationAddress,
ip: request.buyer.ip,
city: request.buyer.city,
country: request.buyer.country,
zipCode: request.buyer.zipCode
},
shippingAddress: {
contactName: request.shippingAddress.contactName,
city: request.shippingAddress.city,
country: request.shippingAddress.country,
address: request.shippingAddress.address,
zipCode: request.shippingAddress.zipCode
},
billingAddress: {
contactName: request.billingAddress.contactName,
city: request.billingAddress.city,
country: request.billingAddress.country,
address: request.billingAddress.address,
zipCode: request.billingAddress.zipCode
},
basketItems: request.basketItems.map((item) => ({
id: item.id,
name: item.name,
category1: item.category1,
category2: item.category2,
itemType: item.itemType,
price: item.price
}))
};
}
/**
* Direkt ödeme (3D Secure olmadan)
*/
async createPayment(request) {
try {
const iyzicoRequest = this.mapToIyzicoRequest(request);
const response = await this.sendRequest(
"/payment/auth",
iyzicoRequest
);
return {
status: this.mapStatus(response.status),
paymentId: response.paymentId,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
errorGroup: response.errorGroup,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Payment failed",
rawResponse: error.response?.data
};
}
}
/**
* 3D Secure ödeme başlat
*/
async initThreeDSPayment(request) {
try {
const iyzicoRequest = {
...this.mapToIyzicoRequest(request),
callbackUrl: request.callbackUrl
};
const response = await this.sendRequest(
"/payment/3dsecure/initialize",
iyzicoRequest
);
let decodedHtmlContent;
if (response.threeDSHtmlContent) {
try {
decodedHtmlContent = Buffer.from(response.threeDSHtmlContent, "base64").toString("utf-8");
} catch (decodeError) {
decodedHtmlContent = response.threeDSHtmlContent;
}
}
return {
status: this.mapStatus(response.status),
threeDSHtmlContent: decodedHtmlContent,
paymentId: response.paymentId,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "3DS initialization failed",
rawResponse: error.response?.data
};
}
}
/**
* 3D Secure ödeme tamamla
*/
async completeThreeDSPayment(callbackData) {
try {
const response = await this.sendRequest("/payment/3dsecure/auth", {
locale: this.config.locale || "tr",
conversationId: callbackData.conversationId,
paymentId: callbackData.paymentId,
conversationData: callbackData.conversationData
});
return {
status: this.mapStatus(response.status),
paymentId: response.paymentId,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
errorGroup: response.errorGroup,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "3DS completion failed",
rawResponse: error.response?.data
};
}
}
/**
* İade işlemi
*/
async refund(request) {
try {
const response = await this.sendRequest("/payment/refund", {
locale: this.config.locale || "tr",
conversationId: request.conversationId,
paymentTransactionId: request.paymentId,
price: request.price,
currency: request.currency,
ip: request.ip
});
return {
status: this.mapStatus(response.status),
refundId: response.paymentTransactionId,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Refund failed",
rawResponse: error.response?.data
};
}
}
/**
* İptal işlemi
*/
async cancel(request) {
try {
const response = await this.sendRequest("/payment/cancel", {
locale: this.config.locale || "tr",
conversationId: request.conversationId,
paymentId: request.paymentId,
ip: request.ip
});
return {
status: this.mapStatus(response.status),
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Cancel failed",
rawResponse: error.response?.data
};
}
}
/**
* Ödeme sorgulama
*/
async getPayment(paymentId) {
try {
const response = await this.sendRequest("/payment/detail", {
locale: this.config.locale || "tr",
paymentId
});
return {
status: this.mapStatus(response.status),
paymentId: response.paymentId,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
errorGroup: response.errorGroup,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Get payment failed",
rawResponse: error.response?.data
};
}
}
/**
* Checkout Form başlat
*/
async initCheckoutForm(request) {
try {
const iyzicoRequest = this.mapToIyzicoCheckoutFormRequest(request);
const response = await this.sendRequest(
"/payment/iyzipos/checkoutform/initialize/auth/ecom",
iyzicoRequest
);
return {
status: this.mapStatus(response.status),
checkoutFormContent: response.checkoutFormContent,
paymentPageUrl: response.paymentPageUrl,
token: response.token,
tokenExpireTime: response.tokenExpireTime,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Checkout form initialization failed",
rawResponse: error.response?.data
};
}
}
/**
* Checkout Form sonucunu sorgula
*/
async retrieveCheckoutForm(token, conversationId) {
try {
const response = await this.sendRequest(
"/payment/iyzipos/checkoutform/auth/ecom/detail",
{
locale: this.config.locale || "tr",
conversationId,
token
}
);
return {
status: this.mapStatus(response.status),
paymentId: response.paymentId,
paymentStatus: response.paymentStatus,
price: response.price,
paidPrice: response.paidPrice,
currency: response.currency,
basketId: response.basketId,
installment: response.installment,
binNumber: response.binNumber,
lastFourDigits: response.lastFourDigits,
cardType: response.cardType,
cardAssociation: response.cardAssociation,
cardFamily: response.cardFamily,
cardToken: response.cardToken,
cardUserKey: response.cardUserKey,
fraudStatus: response.fraudStatus,
merchantCommissionRate: response.merchantCommissionRate,
merchantCommissionRateAmount: response.merchantCommissionRateAmount,
iyziCommissionRateAmount: response.iyziCommissionRateAmount,
iyziCommissionFee: response.iyziCommissionFee,
paymentTransactionId: response.paymentTransactionId,
conversationId: response.conversationId,
errorCode: response.errorCode,
errorMessage: response.errorMessage,
rawResponse: response
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Retrieve checkout form failed",
rawResponse: error.response?.data
};
}
}
/**
* ===================
* SUBSCRIPTION METHODS
* ===================
*/
/**
* Abonelik başlat (NON3D)
*/
async initializeSubscription(request) {
try {
const iyzicoRequest = {
locale: request.locale || this.config.locale || "tr",
conversationId: request.conversationId,
pricingPlanReferenceCode: request.pricingPlanReferenceCode,
subscriptionInitialStatus: request.subscriptionInitialStatus,
customer: {
name: request.customer.name,
surname: request.customer.surname,
email: request.customer.email,
gsmNumber: request.customer.gsmNumber,
identityNumber: request.customer.identityNumber,
billingAddress: {
contactName: request.customer.billingAddress.contactName,
city: request.customer.billingAddress.city,
country: request.customer.billingAddress.country,
address: request.customer.billingAddress.address,
zipCode: request.customer.billingAddress.zipCode
},
shippingAddress: request.customer.shippingAddress ? {
contactName: request.customer.shippingAddress.contactName,
city: request.customer.shippingAddress.city,
country: request.customer.shippingAddress.country,
address: request.customer.shippingAddress.address,
zipCode: request.customer.shippingAddress.zipCode
} : void 0
},
paymentCard: {
cardHolderName: request.paymentCard.cardHolderName,
cardNumber: request.paymentCard.cardNumber,
expireMonth: request.paymentCard.expireMonth,
expireYear: request.paymentCard.expireYear,
cvc: request.paymentCard.cvc
}
};
const response = await this.sendRequest("/v2/subscription/initialize", iyzicoRequest);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Subscription initialization failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* Aboneliği iptal et
*/
async cancelSubscription(request) {
try {
const response = await this.sendRequest(
`/v2/subscription/subscriptions/${request.subscriptionReferenceCode}/cancel`,
{}
);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Subscription cancellation failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* Aboneliği yükselt/güncelle
*/
async upgradeSubscription(request) {
try {
const iyzicoRequest = {
newPricingPlanReferenceCode: request.newPricingPlanReferenceCode,
useTrial: request.useTrial,
resetRecurrenceCount: request.resetRecurrenceCount
};
const response = await this.sendRequest(
`/v2/subscription/subscriptions/${request.subscriptionReferenceCode}/upgrade`,
iyzicoRequest
);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Subscription upgrade failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* Abonelik detaylarını getir
*/
async retrieveSubscription(request) {
try {
const response = await this.sendRequest(
`/v2/subscription/subscriptions/${request.subscriptionReferenceCode}`,
{}
);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Subscription retrieve failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* Abonelik kartı güncelle (Checkout Form ile)
*/
async updateSubscriptionCard(request) {
try {
const iyzicoRequest = {
locale: request.locale || this.config.locale || "tr",
conversationId: request.conversationId,
subscriptionReferenceCode: request.subscriptionReferenceCode,
callbackUrl: request.callbackUrl
};
const response = await this.sendRequest(
"/v2/subscription/card-update/checkoutform/initialize",
iyzicoRequest
);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Card update initialization failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* Abonelik ürünü oluştur
*/
async createSubscriptionProduct(request) {
try {
const iyzicoRequest = {
locale: request.locale || this.config.locale || "tr",
conversationId: request.conversationId,
name: request.name,
description: request.description
};
const response = await this.sendRequest("/v2/subscription/products", iyzicoRequest);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Product creation failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* Fiyatlandırma planı oluştur
*/
async createPricingPlan(request) {
try {
const iyzicoRequest = {
locale: request.locale || this.config.locale || "tr",
conversationId: request.conversationId,
productReferenceCode: request.productReferenceCode,
name: request.name,
price: request.price,
currency: request.currency || "TRY",
paymentInterval: request.paymentInterval,
paymentIntervalCount: request.paymentIntervalCount,
trialPeriodDays: request.trialPeriodDays,
recurrenceCount: request.recurrenceCount
};
const response = await this.sendRequest(
`/v2/subscription/products/${request.productReferenceCode}/pricing-plans`,
iyzicoRequest
);
return response;
} catch (error) {
return {
status: "failure",
errorMessage: error.message || "Pricing plan creation failed",
errorCode: error.response?.data?.errorCode
};
}
}
/**
* BIN sorgulama
*/
async binCheck(binNumber) {
const request = {
locale: this.config.locale,
conversationId: "123456789",
binNumber
};
const response = await this.sendRequest("/payment/bin/check", request);
if (response.status !== "success") {
throw new Error(response.errorMessage || "BIN check failed");
}
return {
binNumber: response.binNumber || binNumber,
cardType: response.cardType || "",
cardAssociation: response.cardAssociation || "",
cardFamily: response.cardFamily || "",
bankName: response.bankName || "",
bankCode: response.bankCode || 0,
commercial: response.commercial === 1,
rawResponse: response
};
}
};
function generatePayTRHash(merchantId, userIp, merchantOid, email, paymentAmount, userBasket, noInstallment, maxInstallment, currency, testMode, merchantSalt) {
const hashStr = `${merchantId}${userIp}${merchantOid}${email}${paymentAmount}${userBasket}${noInstallment}${maxInstallment}${currency}${testMode}${merchantSalt}`;
return crypto2__default.createHmac("sha256", merchantSalt).update(hashStr).digest("base64");
}
function verifyPayTRCallback(merchantOid, merchantSalt, status, totalAmount, hash) {
const calculatedHash = crypto2__default.createHmac("sha256", merchantSalt).update(`${merchantOid}${merchantSalt}${status}${totalAmount}`).digest("base64");
return calculatedHash === hash;
}
function formatPayTRBasket(items) {
const basketArray = items.map((item) => [item.name, item.price, item.quantity]);
return JSON.stringify(basketArray);
}
function convertToKurus(amount) {
const amountFloat = parseFloat(amount);
return Math.round(amountFloat * 100).toString();
}
function generatePayTRToken(merchantId, merchantOid, returnAmount, merchantSalt) {
const tokenStr = `${merchantId}${merchantOid}${returnAmount}${merchantSalt}`;
return crypto2__default.createHmac("sha256", merchantSalt).update(tokenStr).digest("base64");
}
function createPayTRFormData(data) {
return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
}
// src/providers/paytr/index.ts
var PayTR = class extends PaymentProvider {
constructor(config) {
if (!config.merchantId) {
throw new Error("Merchant ID is required");
}
if (!config.merchantSalt) {
throw new Error("Merchant Salt is required");
}
super(config);
this.merchantId = config.merchantId;
this.merchantKey = config.apiKey;
this.merchantSalt = config.merchantSalt;
this.client = axios.create({
baseURL: this.config.baseUrl,
timeout: 3e4,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
}
/**
* PayTR status'ünü PaymentStatus'e çevir
*/
mapStatus(paytrStatus) {
switch (paytrStatus) {
case "success":
return "success" /* SUCCESS */;
case "failed":
return "failure" /* FAILURE */;
default:
return "pending" /* PENDING */;
}
}
/**
* Sepet itemlerini PayTR formatına çevir
*/
convertBasketItems(basketItems) {
return basketItems.map((item) => ({
name: item.name,
price: convertToKurus(item.price),
quantity: 1
}));
}
/**
* Direkt ödeme (PayTR'da iframe ile)
* NOT: PayTR direkt ödeme yerine iframe kullanır
*/
async createPayment(request) {
try {
const threeDSResponse = await this.initThreeDSPayment({
...request,
callbackUrl: request.callbackUrl || "https://example.com/callback"
});
return {
status: threeDSResponse.status,
paymentId: threeDSResponse.paymentId,
conversationId: threeDSResponse.conversationId,
errorCode: threeDSResponse.errorCode,
errorMessage: threeDSResponse.errorMessage,
rawResponse: {
...threeDSResponse.rawResponse,
note: "PayTR does not support direct payment, initiated 3DS payment instead"
}
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Payment failed",
rawResponse: error.response?.data
};
}
}
/**
* 3D Secure ödeme başlat (iframe)
*/
async initThreeDSPayment(request) {
try {
const basketItems = this.convertBasketItems(request.basketItems);
const userBasket = formatPayTRBasket(basketItems);
const paymentAmountKurus = convertToKurus(request.price);
const noInstallment = "0";
const maxInstallment = "0";
const currency = request.currency || "TL";
const testMode = this.config.baseUrl.includes("sandbox") ? "1" : "0";
const merchantOid = request.conversationId || `ORDER-${Date.now()}`;
const paymentHash = generatePayTRHash(
this.merchantId,
request.buyer.ip,
merchantOid,
request.buyer.email,
paymentAmountKurus,
userBasket,
noInstallment,
maxInstallment,
currency,
testMode,
this.merchantSalt
);
const paytrRequest = {
merchant_id: this.merchantId,
merchant_key: this.merchantKey,
merchant_salt: this.merchantSalt,
email: request.buyer.email,
payment_amount: paymentAmountKurus,
merchant_oid: merchantOid,
user_name: `${request.buyer.name} ${request.buyer.surname}`,
user_address: request.shippingAddress.address,
user_phone: request.buyer.gsmNumber || "05001234567",
merchant_ok_url: request.callbackUrl,
merchant_fail_url: request.callbackUrl,
user_basket: userBasket,
user_ip: request.buyer.ip,
timeout_limit: "30",
debug_on: "0",
test_mode: testMode,
no_installment: noInstallment,
max_installment: maxInstallment,
currency,
lang: this.config.locale || "tr",
paytr_token: paymentHash
};
const formData = createPayTRFormData(paytrRequest);
const response = await this.client.post(
"/odeme/api/get-token",
formData
);
if (response.data.status === "success" && response.data.token) {
const iframeUrl = `https://www.paytr.com/odeme/guvenli/${response.data.token}`;
const iframeHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PayTR \xD6deme</title>
<style>
body { margin: 0; padding: 0; overflow: hidden; }
iframe { width: 100%; height: 100vh; border: none; }
</style>
</head>
<body>
<iframe src="${iframeUrl}"></iframe>
</body>
</html>`;
return {
status: "pending" /* PENDING */,
threeDSHtmlContent: iframeHtml,
paymentId: response.data.token,
conversationId: merchantOid,
rawResponse: response.data
};
} else {
return {
status: "failure" /* FAILURE */,
errorMessage: response.data.reason || "Payment initialization failed",
rawResponse: response.data
};
}
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "3DS initialization failed",
rawResponse: error.response?.data
};
}
}
/**
* 3D Secure ödeme tamamla
*/
async completeThreeDSPayment(callbackData) {
try {
const isValid = verifyPayTRCallback(
callbackData.merchant_oid,
this.merchantSalt,
callbackData.status,
callbackData.total_amount,
callbackData.hash
);
if (!isValid) {
return {
status: "failure" /* FAILURE */,
errorMessage: "Invalid callback signature",
rawResponse: callbackData
};
}
return {
status: this.mapStatus(callbackData.status),
paymentId: callbackData.merchant_oid,
conversationId: callbackData.merchant_oid,
errorCode: callbackData.failed_reason_code,
errorMessage: callbackData.failed_reason_msg,
rawResponse: callbackData
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "3DS completion failed",
rawResponse: error
};
}
}
/**
* İade işlemi
*/
async refund(request) {
try {
const returnAmount = convertToKurus(request.price);
const refundToken = generatePayTRToken(
this.merchantId,
request.paymentId,
returnAmount,
this.merchantSalt
);
const refundRequest = {
merchant_id: this.merchantId,
merchant_oid: request.paymentId,
return_amount: returnAmount,
merchant_key: this.merchantKey,
merchant_salt: this.merchantSalt,
paytr_token: refundToken
};
const formData = createPayTRFormData(refundRequest);
const response = await this.client.post("/odeme/iade", formData);
if (response.data.status === "success") {
return {
status: "success" /* SUCCESS */,
refundId: response.data.merchant_oid,
conversationId: request.conversationId,
rawResponse: response.data
};
} else {
return {
status: "failure" /* FAILURE */,
errorCode: response.data.error_no,
errorMessage: response.data.error_message,
rawResponse: response.data
};
}
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Refund failed",
rawResponse: error.response?.data
};
}
}
/**
* İptal işlemi (PayTR'da iade ile aynı)
*/
async cancel(request) {
try {
const refundResult = await this.refund({
paymentId: request.paymentId,
price: "0",
// Tam iade için sıfır gönderilir
currency: "TRY",
ip: request.ip,
conversationId: request.conversationId
});
return {
status: refundResult.status,
conversationId: refundResult.conversationId,
errorCode: refundResult.errorCode,
errorMessage: refundResult.errorMessage,
rawResponse: refundResult.rawResponse
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Cancel failed",
rawResponse: error
};
}
}
/**
* Ödeme sorgulama
* NOT: PayTR API'si ödeme sorgulama endpoint'i sunmuyor
* Callback data'yı kullanarak durum kontrol edilmeli
*/
async getPayment(paymentId) {
return {
status: "pending" /* PENDING */,
paymentId,
errorMessage: "PayTR does not provide payment query endpoint. Use callback data to verify payment status.",
rawResponse: {
note: "Use completeThreeDSPayment with callback data to get payment status"
}
};
}
};
function createAkbankHash(params) {
const { merchantId, terminalId, orderId, amount, currency, storeKey, txnType = "Auth" } = params;
const hashData = `${merchantId}|${terminalId}|${orderId}|${amount}|${currency}|${txnType}|${storeKey}`;
return crypto2.createHash("sha512").update(hashData, "utf8").digest("base64");
}
function createAkbank3DHash(params) {
const {
merchantId,
terminalId,
orderId,
amount,
currency,
successUrl,
errorUrl,
secure3DStoreKey,
txnType = "Auth"
} = params;
const hashData = `${merchantId}|${terminalId}|${orderId}|${amount}|${currency}|${successUrl}|${errorUrl}|${txnType}|${secure3DStoreKey}`;
return crypto2.createHash("sha512").update(hashData, "utf8").digest("base64");
}
function verifyAkbank3DHash(params) {
const { merchantId, terminalId, orderId, secure3DHash, secure3DStoreKey, amount, currency } = params;
let hashData;
if (amount && currency) {
hashData = `${merchantId}|${terminalId}|${orderId}|${amount}|${currency}|${secure3DStoreKey}`;
} else {
hashData = `${merchantId}|${terminalId}|${orderId}|${secure3DStoreKey}`;
}
const calculatedHash = crypto2.createHash("sha512").update(hashData, "utf8").digest("base64");
return calculatedHash === secure3DHash;
}
function formatAmount(amount) {
return Math.round(amount * 100).toString();
}
function formatExpiry(month, year) {
const yearStr = year.length === 4 ? year.slice(-2) : year;
const monthStr = month.padStart(2, "0");
return `${yearStr}${monthStr}`;
}
function getCurrencyCode(currency) {
const currencyMap = {
TRY: "949",
USD: "840",
EUR: "978",
GBP: "826"
};
return currencyMap[currency.toUpperCase()] || "949";
}
// src/providers/akbank/index.ts
var Akbank = class extends PaymentProvider {
constructor(config) {
if (!config.merchantId) {
throw new Error("Merchant ID is required");
}
if (!config.terminalId) {
throw new Error("Terminal ID is required");
}
if (!config.storeKey) {
throw new Error("Store Key is required");
}
super(config);
this.merchantId = config.merchantId;
this.terminalId = config.terminalId;
this.storeKey = config.storeKey;
this.secure3DStoreKey = config.secure3DStoreKey;
this.client = axios.create({
baseURL: this.config.baseUrl,
timeout: 3e4,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
}
/**
* Akbank status'ünü PaymentStatus'e çevir
*/
mapStatus(akbankCode) {
if (akbankCode === "00" || akbankCode === "Success") {
return "success" /* SUCCESS */;
} else if (akbankCode === "Declined" || akbankCode === "Error") {
return "failure" /* FAILURE */;
}
return "pending" /* PENDING */;
}
/**
* Form data oluştur
*/
createFormData(data) {
return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
}
/**
* Direkt ödeme (3D Secure olmadan)
*/
async createPayment(request) {
try {
const amount = formatAmount(parseFloat(request.price));
const currency = getCurrencyCode(request.currency || "TRY");
const orderId = request.conversationId || `ORDER-${Date.now()}`;
const hash = createAkbankHash({
merchantId: this.merchantId,
terminalId: this.terminalId,
orderId,
amount,
currency,
storeKey: this.storeKey,
txnType: "Auth"
});
const akbankRequest = {
MERCHANTID: this.merchantId,
TERMINALID: this.terminalId,
AMOUNT: amount,
CURRENCY: currency,
ORDERID: orderId,
TXNTYPE: "Auth",
PAN: request.paymentCard.cardNumber,
EXPIRY: formatExpiry(request.paymentCard.expireMonth, request.paymentCard.expireYear),
CVV: request.paymentCard.cvc,
CARDOWNER: request.paymentCard.cardHolderName,
HASH: hash
};
const formData = this.createFormData(akbankRequest);
const response = await this.client.post("/servlet/PaymentGateway", formData);
return {
status: this.mapStatus(response.data.ProcReturnCode),
paymentId: response.data.OrderId,
conversationId: orderId,
errorCode: response.data.ErrMsg,
errorMessage: response.data.Response,
rawResponse: response.data
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Payment failed",
rawResponse: error.response?.data
};
}
}
/**
* 3D Secure ödeme başlat
*/
async initThreeDSPayment(request) {
try {
if (!this.secure3DStoreKey) {
throw new Error("3D Secure Store Key is required for 3DS payments");
}
const amount = formatAmount(parseFloat(request.price));
const currency = getCurrencyCode(request.currency || "TRY");
const orderId = request.conversationId || `ORDER-${Date.now()}`;
const hash = createAkbank3DHash({
merchantId: this.merchantId,
terminalId: this.terminalId,
orderId,
amount,
currency,
successUrl: request.callbackUrl,
errorUrl: request.callbackUrl,
secure3DStoreKey: this.secure3DStoreKey,
txnType: "Auth"
});
const akbankRequest = {
MERCHANTID: this.merchantId,
TERMINALID: this.terminalId,
AMOUNT: amount,
CURRENCY: currency,
ORDERID: orderId,
TXNTYPE: "Auth",
SUCCESSURL: request.callbackUrl,
ERRORURL: request.callbackUrl,
PAN: request.paymentCard.cardNumber,
EXPIRY: formatExpiry(request.paymentCard.expireMonth, request.paymentCard.expireYear),
CVV: request.paymentCard.cvc,
CARDOWNER: request.paymentCard.cardHolderName,
EMAIL: request.buyer.email,
HASH: hash
};
if (request.installment && request.installment > 1) {
akbankRequest.INSTALLMENT_COUNT = request.installment.toString();
}
const formData = this.createFormData(akbankRequest);
const response = await this.client.post("/servlet/3DGate", formData);
if (response.data.ProcReturnCode === "Success" && response.data.Message) {
return {
status: "pending" /* PENDING */,
threeDSHtmlContent: response.data.Message,
paymentId: response.data.OrderId,
conversationId: orderId,
rawResponse: response.data
};
} else {
return {
status: "failure" /* FAILURE */,
errorCode: response.data.ErrMsg,
errorMessage: response.data.Response,
rawResponse: response.data
};
}
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "3DS initialization failed",
rawResponse: error.response?.data
};
}
}
/**
* 3D Secure ödeme tamamla
*/
async completeThreeDSPayment(callbackData) {
try {
if (!this.secure3DStoreKey) {
throw new Error("3D Secure Store Key is required for 3DS payments");
}
const isValid = verifyAkbank3DHash({
merchantId: callbackData.MERCHANTID,
terminalId: callbackData.TERMINALID,
orderId: callbackData.ORDERID,
secure3DHash: callbackData.SECURE3DHASH,
secure3DStoreKey: this.secure3DStoreKey,
amount: callbackData.AMOUNT,
currency: callbackData.CURRENCY
});
if (!isValid) {
return {
status: "failure" /* FAILURE */,
errorMessage: "Invalid 3D Secure hash",
rawResponse: callbackData
};
}
const successMdStatuses = ["1", "2", "3", "4"];
if (callbackData.mdStatus && !successMdStatuses.includes(callbackData.mdStatus)) {
return {
status: "failure" /* FAILURE */,
errorMessage: `3D Authentication failed with mdStatus: ${callbackData.mdStatus}`,
rawResponse: callbackData
};
}
return {
status: this.mapStatus(callbackData.ProcReturnCode || "Success"),
paymentId: callbackData.ORDERID,
conversationId: callbackData.ORDERID,
errorMessage: callbackData.Response,
rawResponse: callbackData
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "3DS completion failed",
rawResponse: error
};
}
}
/**
* İade işlemi
*/
async refund(request) {
try {
const amount = formatAmount(parseFloat(request.price));
const currency = getCurrencyCode(request.currency || "TRY");
const hash = createAkbankHash({
merchantId: this.merchantId,
terminalId: this.terminalId,
orderId: request.paymentId,
amount,
currency,
storeKey: this.storeKey,
txnType: "Refund"
});
const akbankRequest = {
MERCHANTID: this.merchantId,
TERMINALID: this.terminalId,
ORDERID: request.paymentId,
AMOUNT: amount,
CURRENCY: currency,
TXNTYPE: "Refund",
HASH: hash
};
const formData = this.createFormData(akbankRequest);
const response = await this.client.post(
"/servlet/PaymentGateway",
formData
);
if (response.data.ProcReturnCode === "Success" || response.data.ProcReturnCode === "00") {
return {
status: "success" /* SUCCESS */,
refundId: response.data.RefundId || response.data.OrderId,
conversationId: request.conversationId,
rawResponse: response.data
};
} else {
return {
status: "failure" /* FAILURE */,
errorCode: response.data.ErrMsg,
errorMessage: response.data.Response,
rawResponse: response.data
};
}
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Refund failed",
rawResponse: error.response?.data
};
}
}
/**
* İptal işlemi
*/
async cancel(request) {
try {
const hash = createAkbankHash({
merchantId: this.merchantId,
terminalId: this.terminalId,
orderId: request.paymentId,
amount: "0",
currency: "949",
storeKey: this.storeKey,
txnType: "Void"
});
const akbankRequest = {
MERCHANTID: this.merchantId,
TERMINALID: this.terminalId,
ORDERID: request.paymentId,
TXNTYPE: "Void",
HASH: hash
};
const formData = this.createFormData(akbankRequest);
const response = await this.client.post(
"/servlet/PaymentGateway",
formData
);
if (response.data.ProcReturnCode === "Success" || response.data.ProcReturnCode === "00") {
return {
status: "success" /* SUCCESS */,
conversationId: request.conversationId,
rawResponse: response.data
};
} else {
return {
status: "failure" /* FAILURE */,
errorCode: response.data.ErrMsg,
errorMessage: response.data.Response,
rawResponse: response.data
};
}
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Cancel failed",
rawResponse: error.response?.data
};
}
}
/**
* Ödeme sorgulama
*/
async getPayment(paymentId) {
try {
const hash = createAkbankHash({
merchantId: this.merchantId,
terminalId: this.terminalId,
orderId: paymentId,
amount: "0",
currency: "949",
storeKey: this.storeKey,
txnType: "StatusInquiry"
});
const akbankRequest = {
MERCHANTID: this.merchantId,
TERMINALID: this.terminalId,
ORDERID: paymentId,
TXNTYPE: "StatusInquiry",
HASH: hash
};
const formData = this.createFormData(akbankRequest);
const response = await this.client.post("/servlet/PaymentGateway", formData);
return {
status: this.mapStatus(response.data.ProcReturnCode),
paymentId: response.data.OrderId,
conversationId: response.data.OrderId,
errorCode: response.data.ErrMsg,
errorMessage: response.data.Response,
rawResponse: response.data
};
} catch (error) {
return {
status: "failure" /* FAILURE */,
errorMessage: error.message || "Get payment failed",
rawResponse: error.response?.data
};
}
}
};
// src/core/BetterPayHandler.ts
var BetterPayHandler = class {
constructor(betterPay) {
this.betterPay = betterPay;
}
/**
* Ana request handler - tüm framework'ler için ortak
*/
async handle(request) {
try {
const cleanUrl = request.url.split("?")[0].replace(/\/$/, "");
if (cleanUrl.endsWith("/health") || cleanUrl.endsWith("/ok")) {
return this.healthCheck();
}
const context = this.parseRoute(request.url);
if (!context) {
return this.errorResponse(404, "Route not found");
}
if (!this.betterPay.isProviderEnabled(context.provider)) {
return this.errorResponse(
400,
`Provider '${context.provider}' is not enabled or configured`
);
}
return await this.handleAction(request, context);
} catch (error) {
return this.errorResponse(500, error.message || "Internal server error");
}
}
/**
* URL'den route bilgilerini parse et
*
* Pattern: /api/pay/:provider/:action
* Örnekler:
* - /api/pay/iyzico/payment
* - /api/pay/paytr/payment/init-3ds
* - /api/pay/iyzico/payment/123
*/
parseRoute(url) {
const cleanUrl = url.split("?")[0];
const normalizedUrl = cleanUrl.replace(/\/$/, "");
const pathMatch = normalizedUrl.match(/\/api\/pay\/(.+)/);
if (!pathMatch) {
return null;
}
const segments = pathMatch[1].split("/");
if (segments.length === 0) {
return null;
}
const provider = segments[0];
if (!Object.values(ProviderType).includes(provider)) {
return null;
}
const action = segments.slice(1).join("/");
const params = {};
const paymentIdMatch = action.match(/^payment\/([^/]+)$/);
if (paymentIdMatch) {
params.paymentId = paymentIdMatch[1];
}
return { provider, action, params };
}
/**
* Action'a göre ilgili provider metodunu çağır
*/
async handleAction(request, context) {
const provider = this.betterPay.use(context.provider);
const { action, params } = context;
try {
switch (action) {
case "payment":
return await this.handleCreatePayment(request, provider);
case "payment/init-3ds":
return await this.handleInitThreeDS(request, provider);
case "payment/complete-3ds":
case "callback":
return await this.handleCompleteThreeDS(request, provider);
case "refund":
return await this.handleRefund(request, provider);
case "cancel":
return await this.handleCancel(request, provider);
default:
if (params.paymentId && request.method === "GET") {
return await this.handleGetPayment(params.paymentId, provider);
}
return this.errorResponse(404, `Action '${action}' not found`);
}
} catch (error) {
return this.errorResponse(500, error.message || "Payment operation failed");
}
}
/**
* POST /api/pay/:provider/payment - Ödeme oluştur
*/
async handleCreatePayment(request, provider) {
if (request.method !== "POST") {
return this.errorResponse(405, "Method not allowed");
}
const paymentRequest = request.body;
const result = await provider.createPayment(paymentRequest);
return this.successResponse(result);
}
/**
* POST /api/pay/:provider/payment/init-3ds - 3DS ödeme başlat
*/
async handleInitThreeDS(request, provider) {
if (request.method !== "POST") {
return this.errorResponse(405, "Method not allowed");
}
const paymentRequest = request.body;
const result = await provider.initThreeDSPayment(paymentRequest);
return this.successResponse(result);
}
/**
* POST /api/pay/:provider/payment/complete-3ds - 3DS ödeme tamamla
* POST /api/pay/:provider/callback - Provider callback (alias)
*/
async handleCompleteThreeDS(request, provider) {
if (request.method !== "POST") {
return this.errorResponse(405, "Method not allowed");
}
const callbackData = request.body;
const result = await provider.completeThreeDSPayment(callbackData);
return this.successResponse(result);
}
/**
* POST /api/pay/:provider/refund - İade işlemi
*/
async handleRefund(request, provider) {
if (request.method !== "POST") {
return this.errorResponse(405, "Method not allowed");
}
const refundRequest = request.body;
const result = await provider.refund(refundRequest);
return this.successResponse(result);
}
/**
* POST /api/pay/:provider/cancel - İptal işlemi
*/
async handleCancel(request, provider) {
if (request.method !== "POST") {
return this.errorResponse(405, "Method not allowed");
}
const cancelRequest = request.body;
const result = await provider.cancel(cancelRequest);
return this.successResponse(result);
}
/**
* GET /api/pay/:provider/payment/:id - Ödeme sorgula
*/
async handleGetPayment(pay