import { XMLParser, XMLBuilder } from 'fast-xml-parser' import { getErrorDescription } from '../helpers' import type { PaymentLink, OrderItem, CreateHashParams, Extra as ExtraType } from './types' // Use a simple cache for hash values to avoid recalculating const hashCache = new Map<string, string>() const createHash = async ({ origin, clientId }: CreateHashParams): Promise<string> => { const cacheKey = `${origin}-${clientId}` // Return cached value if available if (hashCache.has(cacheKey)) { return hashCache.get(cacheKey)! } const stringToHash = `${origin}-ORIGIN-${clientId}` const hashBuffer = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(stringToHash) ) const hash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer))) // Cache the result hashCache.set(cacheKey, hash) return hash } // Optimized to avoid unnecessary object creation const buildOrderItems = (items: OrderItem[]) => items.map(({ id, itemnumber, productcode, qty, desc, price }) => { const orderItem: Record<string, any> = { OrderItem: {} } const item = orderItem.OrderItem if (id) item.id = id if (itemnumber) item.itemnumber = itemnumber if (productcode) item.productcode = productcode if (qty) item.qty = qty if (desc) item.desc = desc if (price) item.price = price if (price && qty) item.Total = price * qty return orderItem }) // More efficient implementation using for...of loop const filterUndefinedValues = (obj: Record<string, any>) => { const result: Record<string, any> = {} for (const [key, value] of Object.entries(obj)) { if (value !== undefined) { result[key] = value } } return result } // Reuse parser and builder instances const xmlParser = new XMLParser() const xmlBuilder = new XMLBuilder({ format: true }) /** * Handles the creation of a payment link by interacting with the NestPay API. * @docs NestPay® | Merchant Integration API Manual. Page: 50 * * @param {PaymentLink} param - The input parameters for the payment link creation. * @param {object} param.data - The data required for the payment link. * @param {string} [param.data.OrderId] - The unique identifier for the order. * @param {string} [param.data.Type='Auth'] - The type of transaction (e.g., 'Auth'). * @param {number} [param.data.Total] - The total amount for the transaction. * @param {number} [param.data.Currency=941] - The currency code for the transaction (default is 941). * @param {number} [param.data.Instalment] - The number of instalments for the payment. * @param {OrderItem[]} [param.data.OrderItemList] - The list of items in the order. * @param {object} [param.data.PbOrder={ OrderType: 0 }] - Additional order details. * @param {object} [param.data.BillTo] - Billing information for the customer. * @param {ExtraType} [param.data.Extra={ PAYMENTLINKTYPE: 'SINGLE_LINK_PAYMENT' }] - Additional parameters for the payment link. * @param {object} param.env - The environment variables for the API interaction. * @param {string} param.env.CLIENT_USERNAME - The username for the NestPay client. * @param {string} param.env.CLIENT_PASSWORD - The password for the NestPay client. * @param {string} param.env.CLIENT_ID - The client ID for the NestPay client. * @param {string} param.env.NESTPAY_API_URL - The URL for the NestPay API. * * @returns {Promise<object>} The response data from the NestPay API. * @returns {object} [response.error] - The error details if the request fails. * @returns {string} [response.error.message] - The error message. * @returns {number} [response.error.status] - The HTTP status code of the error. * @returns {string} [response.error.statusText] - The HTTP status text of the error. * @returns {object} [responseData] - The parsed response data from the API. * @returns {string} [responseData.ProcReturnCode] - The processing return code from the API. * @returns {string} [responseData.OrderId] - The order ID returned by the API. * @returns {object} [responseData.Extra] - Additional response details. * @returns {string} [responseData.Extra.PAYMENTLINKEXPIRATIONDATE] - The expiration date of the payment link. * @returns {string} [responseData.Extra.PAYMENTLINKTOKEN] - The token for the payment link. * @returns {string} [responseData.Extra.PAYMENTLINKURL] - The URL for the payment link. * @returns {string} [responseData.Extra.PAYMENTLINKTYPE] - The type of the payment link. * * @throws {Error} If an unexpected error occurs during the process. */ export async function paymentLink({ data, env }: PaymentLink) { try { const { OrderId, Type = 'Auth', Total, Currency = 941, Instalment, OrderItemList, PbOrder = { OrderType: 0 }, BillTo, Extra = {} as ExtraType } = data const { PAYMENTLINKTYPE = 'SINGLE_LINK_PAYMENT', PAYMENTLINKEXPIRY, PAYMENTLINKEXPIRYUNIT, PAYMENTLINKLANGUAGE, PAYMENTLINKAMOUNT_EDITABLE, PAYMENTLINKCUSTOMERPHONEEDITABLE, PAYMENTLINKITEMIDEDITABLE, PAYMENTLINKCUSTOMERNAMEEDITABLE, PAYMENTLINKADDRESS_EDITABLE, PAYMENTLINKITEMDESCRIPTIONEDITABLE, PAYMENTLINKCUSTOMEREMAILEDITABLE } = Extra const origin = PAYMENTLINKTYPE === 'SINGLE_LINK_PAYMENT' ? 'SPL' : 'MPL' const ORIGIN = await createHash({ origin, clientId: env.CLIENT_ID }) // Build Extra object more efficiently const extraObj: Record<string, any> = { PAYMENTLINKTYPE, ORIGIN } if (PAYMENTLINKEXPIRY) extraObj.PAYMENTLINKEXPIRY = PAYMENTLINKEXPIRY if (PAYMENTLINKEXPIRYUNIT) extraObj.PAYMENTLINKEXPIRYUNIT = PAYMENTLINKEXPIRYUNIT if (PAYMENTLINKLANGUAGE) extraObj.PAYMENTLINKLANGUAGE = PAYMENTLINKLANGUAGE if (PAYMENTLINKAMOUNT_EDITABLE) extraObj.PAYMENTLINKAMOUNT_EDITABLE = PAYMENTLINKAMOUNT_EDITABLE if (PAYMENTLINKITEMIDEDITABLE) extraObj.PAYMENTLINKITEMIDEDITABLE = PAYMENTLINKITEMIDEDITABLE if (PAYMENTLINKCUSTOMERPHONEEDITABLE) extraObj.PAYMENTLINKCUSTOMERPHONEEDITABLE = PAYMENTLINKCUSTOMERPHONEEDITABLE if (PAYMENTLINKCUSTOMERNAMEEDITABLE) extraObj.PAYMENTLINKCUSTOMERNAMEEDITABLE = PAYMENTLINKCUSTOMERNAMEEDITABLE if (PAYMENTLINKADDRESS_EDITABLE) extraObj.PAYMENTLINKADDRESS_EDITABLE = PAYMENTLINKADDRESS_EDITABLE if (PAYMENTLINKITEMDESCRIPTIONEDITABLE) extraObj.PAYMENTLINKITEMDESCRIPTIONEDITABLE = PAYMENTLINKITEMDESCRIPTIONEDITABLE if (PAYMENTLINKCUSTOMEREMAILEDITABLE) extraObj.PAYMENTLINKCUSTOMEREMAILEDITABLE = PAYMENTLINKCUSTOMEREMAILEDITABLE // Build request object more efficiently const cc5Request: Record<string, any> = { Name: env.CLIENT_USERNAME, Password: env.CLIENT_PASSWORD, ClientId: env.CLIENT_ID, Type, Currency, Extra: extraObj } if (PAYMENTLINKTYPE === 'SINGLE_LINK_PAYMENT' && OrderId) { cc5Request.OrderId = OrderId } if (Instalment) cc5Request.Instalment = Instalment if (Total) cc5Request.Total = Total if (PbOrder) cc5Request.PbOrder = filterUndefinedValues(PbOrder) if (BillTo) cc5Request.BillTo = filterUndefinedValues(BillTo) if (OrderItemList?.length) { cc5Request.OrderItemList = buildOrderItems(OrderItemList) } const xmlData = { CC5Request: cc5Request } const xmlContent = xmlBuilder.build(xmlData) const response = await fetch(env.NESTPAY_API_URL, { method: 'POST', headers: { 'content-type': 'text/xml; charset=utf-8', Accept: '*/*' }, body: xmlContent }) const responseText = await response.text() const parsed = xmlParser.parse(responseText) const responseData = parsed?.CC5Response || {} if (responseData.ProcReturnCode === '99') { return { error: { ...getErrorDescription(responseData.Extra.ERRORCODE), ...(responseData.OrderId && { OrderId: responseData.OrderId }) } } } return responseData } catch (error) { return { error: { message: error instanceof Error ? error.message : 'Unknown error occurred', status: 500, statusText: 'Internal Server Error' } } } }