UNPKG

better-payment

Version:

Unified payment gateway library for Turkish payment providers

1,571 lines (1,564 loc) 61 kB
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