Skip to content

Instantly share code, notes, and snippets.

@natsu90
Forked from chengkiang/paynow.js
Last active March 22, 2024 20:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save natsu90/f45dc88b38a037325ad9095163b82b42 to your computer and use it in GitHub Desktop.
Save natsu90/f45dc88b38a037325ad9095163b82b42 to your computer and use it in GitHub Desktop.
MY DuitNow QR Code Generator Sample
String.prototype.padLeft = function (n, str) {
if (n < String(this).length) {
return this.toString();
}
else {
return Array(n - String(this).length + 1).join(str || '0') + this;
}
}
function crc16(s) {
var crcTable = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5,
0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b,
0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,
0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c,
0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401,
0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b,
0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6,
0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738,
0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5,
0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969,
0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96,
0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc,
0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03,
0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd,
0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6,
0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a,
0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb,
0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1,
0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c,
0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2,
0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb,
0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447,
0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8,
0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2,
0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9,
0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827,
0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,
0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0,
0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d,
0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07,
0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba,
0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74,
0x2e93, 0x3eb2, 0x0ed1, 0x1ef0];
var crc = 0xFFFF;
var j, i;
for (i = 0; i < s.length; i++) {
c = s.charCodeAt(i);
if (c > 255) {
throw new RangeError();
}
j = (c ^ (crc >> 8)) & 0xFF;
crc = crcTable[j] ^ (crc << 8);
}
return ((crc ^ 0) & 0xFFFF).toString(16).toUpperCase().padStart(4, '0');
}
function generateDuitNowStr( opts ) {
let p = [
{ id: '00', value: '02' }, // ID 00: Payload Format Indicator, PayNow is using 01, but DuitNow is using 02
{
id: '26', value: // ID 26: Merchant Account Info Template
[
{ id: '00', value: 'A0000006150001' }, // DuitNow Application Identifier
{ id: '01', value: opts.app || '588734' },// unknown value, 588734 is from MAE app
{ id: '02', value: opts.account } // unknown Account Number format
]
},
{ id: '52', value: opts.category || '0000' }, // ID 52: Merchant Category Code
{ id: '53', value: '458' }, // ID 53: Currency. MYR is 458
{ id: '58', value: 'MY' }, // ID 58: 2-letter Country Code (MY)
{ id: '59', value: opts.name ? opts.name.substring(0, 25) : 'NA' }, // ID 59: Merchant Name
{ id: '60', value: opts.city ? opts.city.substring(0, 15) : 'MY' } // ID 60: Merchant City
]
// add Merchant Postcode
if (opts.postcode) {
p.push({ id: '61', value: opts.postcode })
}
// add Amount if any
if (opts.amount) {
p.push({ id: '01', value: '12' }) // ID 01: Point of Initiation Method 11: static, 12: dynamic
p.push({ id: '54', value: opts.amount.toFixed(2) }) // ID 54: Transaction Amount
} else {
p.push({ id: '01', value: '11' }) // ID 01: Point of Initiation Method 11: static, 12: dynamic
}
// add Expiry Datetime
if (opts.expiry) {
p[p.findIndex(a => a.id == '26')].value.push({ id: '03', value: opts.expiry }) // Expiry datetime in Unix time in miliseconds
}
// add Ref Numbers
if (opts.ref || opts.ref3 || opts.ref5 || opts.ref6 || opts.ref7 || opts.ref8) {
let refObj = p.find(x => x.id === '62')
if (!refObj) {
refObj = { id: '62', value: []}
}
if (opts.ref) {
refObj.value.push({ id: '01', value: opts.ref }) // ID 01: Bill Number
}
if (opts.ref3) {
refObj.value.push({ id: '03', value: opts.ref3 }) // ID 03: Store Label
}
if (opts.ref5) {
refObj.value.push({ id: '05', value: opts.ref5 }) // ID 05: Reference Label
}
if (opts.ref6) {
refObj.value.push({ id: '06', value: opts.ref6 }) // ID 06: Customer Label
}
if (opts.ref7) {
refObj.value.push({ id: '07', value: opts.ref7 }) // ID 07: Terminal Label
}
if (opts.ref8) {
refObj.value.push({ id: '08', value: opts.ref8 }) // ID 08: Purpose of Transaction
}
p.push(refObj)
}
// add Extra Ref Number
if (opts.ref82) {
p.push({ id: '82', value: opts.ref82 })
}
// sorting object by ID
p.map((q) => Array.isArray(q.value) ? q.value.sort((a, b) => a.id.localeCompare(b.id)) : q)
p.sort((a, b) => a.id.localeCompare(b.id))
// start generating QR string
let str = p.reduce((final, current) => {
if (Array.isArray(current.value)) { //nest loop
current.value = current.value.reduce((f, c) => {
f += c.id + c.value.length.toString().padLeft(2) + c.value;
return f
}, "")
}
final += current.id + current.value.length.toString().padLeft(2) + current.value;
return final
}, "")
// Here we add "6304" to the previous string
// ID 63 (Checksum) 04 (4 characters)
// Do a CRC16 of the whole string including the "6304"
// then append it to the end.
str += '6304' + crc16(str + '6304');
return str;
}
module.exports = {
generateDuitNowStr
}
{
"version": "1.0.0",
"name": "duitnow-js",
"main": "duitnow.js"
}
@fazman
Copy link

fazman commented Dec 8, 2023

Opts.app seems to be the bank acquirer ID
https://fliphtml5.com/qvqkn/ticr/BIC_CODE_2022_v1.9.1_%28For_Distribution%29/

Any idea how the Account Number is generated?

@natsu90
Copy link
Author

natsu90 commented Dec 8, 2023

@fazman unfortunately AFAIK @paynet-my is gatekeeping this so there is no way we can get the Account Number except through the Acquirer from their Duitnow QR generator. the Account Number is totally different value from regular bank Account Number and DuitNow ID.

@shontee
Copy link

shontee commented Mar 7, 2024

any idea ...how's ref82 being generate

@natsu90
Copy link
Author

natsu90 commented Mar 21, 2024

@shontee you don't need ref82 to generate DuitNowQR, you won't find this in normal DuitNowQR. i only found this in Razer Merchant so far. i believe the ref82 value is used by them to match which account is belong to, to send the webhook when any payment is received.

@shontee
Copy link

shontee commented Mar 21, 2024 via email

@natsu90
Copy link
Author

natsu90 commented Mar 21, 2024

@shontee oh good to know. i wish my Razer Merchant account still active so i can confirm this.

@pengham
Copy link

pengham commented Mar 22, 2024

Hi. I am currently trying to use your code to produce a Dynamic DuitNow QR but failed. It seems that every Dynamic DuitNow QR generated will expire after 60 sec (and this 60 sec is not set in QR code).

Maybank using ID:05 of ID:62
CIMB using ID:01, ID:03 (YYYYMMDDHHMMSS) and ID:82 (SHA256) of ID:62

Somehow all the information in ID:62 is sent to PayNet for verification and callback purpose and it is not possible to generate Dynamic DuitNow QR ourselves.

@natsu90
Copy link
Author

natsu90 commented Mar 22, 2024

@pengham this is true, we can't generate a dynamic DuitNow QR ourselves, somehow our DuitNow QR is implemented quite differently than our counterparts; PayNow (SG) and PromptPay (TH). we have to go through a Acquirer or banking app, thanks to greedy @paynet-my. a temporary account number is generated when we're requesting a dynamic QR, and the account number only valid for 60 seconds at most.

thanks, good to know about ID:62, though i never have to use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment