import assert from 'node:assert/strict'; import { createCipheriv, createSign, generateKeyPairSync, randomBytes } from 'node:crypto'; import { WechatPayClient, WechatPayError } from '../dist/payments/wechat-pay-client.js'; const merchantKeys = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); const platformKeys = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); const credential = { appId: 'wx-test-app', merchantId: '1900000109', serialNo: 'MERCHANT-SERIAL', privateKeyPem: merchantKeys.privateKey, apiV3Key: '0123456789abcdef0123456789abcdef', platformCertificates: { 'PLATFORM-SERIAL': platformKeys.publicKey } }; const requests = []; const responses = [ { prepay_id: 'wx-prepay-test' }, { trade_state: 'SUCCESS', transaction_id: 'wx-transaction-test' }, { refund_id: 'wx-refund-test', status: 'PROCESSING' }, { download_url: 'https://api.mch.weixin.qq.com/v3/billdownload/file?token=sanitized' }, { out_bill_no: 'CLP501', transfer_bill_no: 'wx-transfer-501', state: 'WAIT_USER_CONFIRM', package_info: 'package-info' } ]; const transport = { async request(input) { requests.push(input); return { status: 200, headers: {}, body: JSON.stringify(responses.shift()) }; } }; const client = new WechatPayClient(transport); const prepay = await client.createJsapiPrepay(credential, { description: 'M05-B sanitized order', outTradeNo: 'PAY-M05B-001', notifyUrl: 'https://api.txyundm.cn/app-api/pay/wechat/notify', amountCents: 3600, payerOpenId: 'openid-sanitized' }); assert.equal(prepay.prepayId, 'wx-prepay-test'); assert.equal(prepay.paymentParams.package, 'prepay_id=wx-prepay-test'); assert.equal(prepay.paymentParams.signType, 'RSA'); assert.match(prepay.paymentParams.paySign, /^[A-Za-z0-9+/]+=*$/); assert.match(requests[0].headers.Authorization, /mchid="1900000109"/); assert.equal(requests[0].body.includes(merchantKeys.privateKey), false); assert.equal( (await client.queryTransaction(credential, 'PAY-M05B-001')).trade_state, 'SUCCESS' ); assert.equal((await client.createRefund(credential, { outTradeNo: 'PAY-M05B-001', outRefundNo: 'REF-M05B-001', reason: 'sanitized refund', notifyUrl: 'https://api.txyundm.cn/app-api/pay/wechat/refund-notify', refundCents: 1200, totalCents: 3600 })).status, 'PROCESSING'); assert.match(requests[2].body, /"refund":1200/); assert.match((await client.downloadTradeBill( credential, '2026-06-21', 'ALL' )).download_url, /^https:/); const transfer = await client.createMerchantTransfer(credential, { outBillNo: 'CLP501', openid: 'openid-sanitized', amountCents: 800, remark: '保洁结算501', sceneId: '1000', reportInfos: [{ infoType: '岗位类型', infoContent: '保洁员' }] }); assert.equal(transfer.state, 'WAIT_USER_CONFIRM'); assert.equal(transfer.packageInfo, 'package-info'); assert.match(requests[4].url, /\/v3\/fund-app\/mch-transfer\/transfer-bills$/); assert.match(requests[4].body, /"out_bill_no":"CLP501"/); assert.match(requests[4].body, /"transfer_amount":800/); assert.equal(requests[4].headers.Authorization.includes(merchantKeys.privateKey), false); const notification = encryptedNotification({ out_trade_no: 'PAY-M05B-001', transaction_id: 'wx-transaction-test', trade_state: 'SUCCESS', amount: { total: 3600, currency: 'CNY' }, payer: { openid: 'must-not-be-persisted' } }, credential.apiV3Key); const timestamp = '1782057600'; const nonce = 'notification-nonce'; const signature = sign( platformKeys.privateKey, `${timestamp}\n${nonce}\n${notification}\n` ); const decrypted = client.verifyAndDecrypt(credential, { timestamp, nonce, serial: 'PLATFORM-SERIAL', signature }, notification); assert.equal(decrypted.transaction_id, 'wx-transaction-test'); assert.equal(decrypted.amount.total, 3600); assert.throws( () => client.verifyAndDecrypt(credential, { timestamp, nonce, serial: 'PLATFORM-SERIAL', signature }, `${notification} `), (error) => error instanceof WechatPayError && error.code === 'WECHAT_SIGNATURE_INVALID' ); console.log('PASS: M05-B Wechat Pay signing, API requests, notification verification and AES-GCM decryption.'); function encryptedNotification(resource, key) { const nonce = randomBytes(12).toString('base64url').slice(0, 12); const associatedData = 'transaction'; const cipher = createCipheriv('aes-256-gcm', Buffer.from(key), Buffer.from(nonce)); cipher.setAAD(Buffer.from(associatedData)); const ciphertext = Buffer.concat([ cipher.update(JSON.stringify(resource), 'utf8'), cipher.final(), cipher.getAuthTag() ]).toString('base64'); return JSON.stringify({ id: 'notification-m05b-001', event_type: 'TRANSACTION.SUCCESS', resource: { algorithm: 'AEAD_AES_256_GCM', ciphertext, nonce, associated_data: associatedData } }); } function sign(privateKey, message) { const signer = createSign('RSA-SHA256'); signer.update(message); signer.end(); return signer.sign(privateKey, 'base64'); }