Last active
March 20, 2025 10:20
-
-
Save daliborgogic/df6728d8aeb13054049a7596f653146a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface PaymentLink { | |
data: PaymentLinkData | |
env: Env | |
error?: { | |
status: number | |
statusText: string | |
message: string | |
cause?: string | |
} | |
} | |
export interface OrderItem { | |
// Id of item | |
id?: string | |
// Item number | |
itemnumber?: string | |
// Product code | |
productcode?: string | |
// Quantity | |
qty?: number | |
price?: number | |
desc?: string | |
} | |
export interface CreateHashParams { | |
// Origin value will be SPL for Sinlgle Payment Link & MPL for Multiple Payment Link | |
origin: string | |
// Merchant Id | |
clientId: string | |
} | |
export interface Extra { | |
// Defines requested link type. Values may be SINGLE_LINK_PAY MENT and MULTIPLE_LINK_PA YMENT. | |
PAYMENTLINKTYPE: 'SINGLE_LINK_PAYMENT' | 'MULTIPLE_LINK_PAYMENT' | |
// Lınk Expiration Date Amount | |
// Lınk Expiration Date Amount | |
PAYMENTLINKEXPIRY?: number | |
// Defines unit type of Extra. PAYMENTLINKEXPIRY parameter | |
PAYMENTLINKEXPIRYUNIT?: 'D' | 'W' | 'M' | |
// Defines unit type of Extra. PAYMENTLINKEXPIRY parameter | |
PAYMENTLINKLANGUAGE?: string | |
// Defines whether amount can be changed in link page or not | |
PAYMENTLINKAMOUNT_EDITABLE?: 'true' | 'false' | |
// Defines whether phone info can be changed in link page or not | |
PAYMENTLINKCUSTOMERPHONEEDITABLE?: 'true' | 'false' | |
// Defines whether item id can be changed in link page or not | |
PAYMENTLINKITEMIDEDITABLE?: boolean | |
// Defines whether customer name can be changed in link page or not | |
PAYMENTLINKCUSTOMERNAMEEDITABLE?: 'true' | 'false' | |
// Defines whether address can be changed in link page or not | |
PAYMENTLINKADDRESS_EDITABLE?: 'true' | 'false' | |
// Defines whether item description can be changed in link page or not | |
PAYMENTLINKITEMDESCRIPTIONEDITABLE?: 'true' | 'false' | |
// Defines whether email info can be changed in link page or not | |
PAYMENTLINKCUSTOMEREMAILEDITABLE?: 'true' | 'false' | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Using NestPay Generated Payment Links and Tokens
After sending the payment link request to NestPay's API, you'll receive a response that contains the payment link URL and a token. Here's how to use them:
Response Structure
When your payment link request is successful, NestPay will return an XML response containing:
Using the Payment Link
Extract the payment link URL from the response (typically in the
URL
orPaymentLink
field).Share the link with your customer through:
Customer experience:
PAYMENTLINK*EDITABLE
parameters)Using the Token
The token serves several purposes:
Implementation Example
Here's how you might handle the response in your code:
Best Practices
Store payment link information in your database:
Implement webhook handling to receive payment notifications from NestPay
Provide status updates to customers after they've received the payment link
Handle expiration by checking the status of payment links and potentially regenerating them if needed
Implement proper error handling for cases where payment link creation fails
Store Payment Link Information in Database
Send Payment Link to Customer
Complete Payment Flow Diagram