Add buildVnpayCallbackData and buildMomoCallbackData fixture helpers that generate valid HMAC signatures for E2E payment callback tests. Co-Authored-By: Paperclip <noreply@paperclip.ing>
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import * as crypto from 'crypto';
|
|
|
|
/**
|
|
* Generates a valid VNPay callback payload with a correct HMAC-SHA512 signature.
|
|
*
|
|
* Used in E2E tests to simulate a real VNPay callback without needing
|
|
* the actual sandbox environment.
|
|
*/
|
|
export function buildVnpayCallbackData(
|
|
hashSecret: string,
|
|
opts: {
|
|
orderId: string;
|
|
amountVND: number;
|
|
responseCode?: string;
|
|
transactionNo?: string;
|
|
},
|
|
): Record<string, string> {
|
|
const params: Record<string, string> = {
|
|
vnp_TxnRef: opts.orderId,
|
|
vnp_ResponseCode: opts.responseCode ?? '00',
|
|
vnp_Amount: (opts.amountVND * 100).toString(),
|
|
vnp_TransactionNo: opts.transactionNo ?? `VNP${Date.now()}`,
|
|
vnp_BankCode: 'NCB',
|
|
vnp_CardType: 'ATM',
|
|
vnp_OrderInfo: `E2E test payment ${opts.orderId}`,
|
|
vnp_PayDate: formatVnpayDate(new Date()),
|
|
vnp_TmnCode: process.env.VNPAY_TMN_CODE ?? 'TESTCODE',
|
|
vnp_TransactionStatus: opts.responseCode === '00' ? '00' : '02',
|
|
};
|
|
|
|
const sorted = sortObject(params);
|
|
const signData = new URLSearchParams(sorted).toString();
|
|
const hmac = crypto.createHmac('sha512', hashSecret);
|
|
const secureHash = hmac.update(Buffer.from(signData, 'utf-8')).digest('hex');
|
|
|
|
return { ...sorted, vnp_SecureHash: secureHash };
|
|
}
|
|
|
|
/**
|
|
* Generates a valid MoMo IPN callback payload with a correct HMAC-SHA256 signature.
|
|
*/
|
|
export function buildMomoCallbackData(
|
|
secretKey: string,
|
|
opts: {
|
|
orderId: string;
|
|
amount: number;
|
|
resultCode?: number;
|
|
transId?: string;
|
|
},
|
|
): Record<string, string> {
|
|
const resultCode = opts.resultCode ?? 0;
|
|
const transId = opts.transId ?? `MOMO${Date.now()}`;
|
|
const requestId = crypto.randomUUID();
|
|
const extraData = '';
|
|
const message = resultCode === 0 ? 'Successful.' : 'Transaction failed.';
|
|
const orderInfo = `E2E test payment ${opts.orderId}`;
|
|
const orderType = 'momo_wallet';
|
|
const payType = 'qr';
|
|
const responseTime = Date.now().toString();
|
|
const partnerCode = process.env.MOMO_PARTNER_CODE ?? 'TESTPARTNER';
|
|
const accessKey = process.env.MOMO_ACCESS_KEY ?? 'TESTACCESSKEY';
|
|
|
|
const rawSignature = [
|
|
`accessKey=${accessKey}`,
|
|
`amount=${opts.amount}`,
|
|
`extraData=${extraData}`,
|
|
`message=${message}`,
|
|
`orderId=${opts.orderId}`,
|
|
`orderInfo=${orderInfo}`,
|
|
`orderType=${orderType}`,
|
|
`partnerCode=${partnerCode}`,
|
|
`payType=${payType}`,
|
|
`requestId=${requestId}`,
|
|
`responseTime=${responseTime}`,
|
|
`resultCode=${resultCode}`,
|
|
`transId=${transId}`,
|
|
].join('&');
|
|
|
|
const signature = crypto
|
|
.createHmac('sha256', secretKey)
|
|
.update(rawSignature)
|
|
.digest('hex');
|
|
|
|
return {
|
|
orderId: opts.orderId,
|
|
amount: opts.amount.toString(),
|
|
extraData,
|
|
message,
|
|
orderInfo,
|
|
orderType,
|
|
partnerCode,
|
|
payType,
|
|
requestId,
|
|
responseTime,
|
|
resultCode: resultCode.toString(),
|
|
transId,
|
|
signature,
|
|
};
|
|
}
|
|
|
|
function formatVnpayDate(date: Date): string {
|
|
return date.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
}
|
|
|
|
function sortObject(obj: Record<string, string>): Record<string, string> {
|
|
const sorted: Record<string, string> = {};
|
|
for (const key of Object.keys(obj).sort()) {
|
|
sorted[key] = obj[key]!;
|
|
}
|
|
return sorted;
|
|
}
|