UNPKG

vietqr-payment

Version:

Completed client side and server solution for generating the VietQR for payment with new specification updated in Oct 2022 by Napas (Vietnam)

287 lines (265 loc) 11.4 kB
//--------------------------------------------------------------------- // // VietQR Generator for JavaScript // // Copyright (c) 2023 The Bean Family // // Maintainer: Jean Nguyen // // URL: https://thebeanfamily.org // // Licensed under the MIT license: // http://www.opensource.org/licenses/mit-license.php // //--------------------------------------------------------------------- 'use strict'; //Contants and tools const SERVICE_CODE = { PAYMENT: "QRPUSH", TO_CARD: "QRIBFTTC", TO_ACCOUNT: "QRIBFTTA" }; const NAPAS_GUID = "A000000727"; const CURRENCY = { VND: "704", USD: "840" } var FIELDS = { is_dynamic_qr: false, merchant_category: "", merchant_name: "", merchant_city: "", postal_code: "", currency: "704", country_code: "VN", amount: "0", acq: "970403", merchant_id: "", service_code: SERVICE_CODE.TO_ACCOUNT, bill_number: "", mobile_number: "", store_label: "", loyalty_number: "", ref_label: "", customer_label: "", terminal_label: "", purpose_txn: "", additional_data: "", lang_ref: "", local_merchant_name: "", local_merchant_city: "", uuid: "", ipn_url: "", app_package_name: "" } const CRC = { stringToUtf8ByteArray(str) { // TODO(user): Use native implementations if/when available var out = [], p = 0; for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i); if (c < 128) { out[p++] = c; } else if (c < 2048) { out[p++] = (c >> 6) | 192; out[p++] = (c & 63) | 128; } else if ( ((c & 0xFC00) == 0xD800) && (i + 1) < str.length && ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) { // Surrogate Pair c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF); out[p++] = (c >> 18) | 240; out[p++] = ((c >> 12) & 63) | 128; out[p++] = ((c >> 6) & 63) | 128; out[p++] = (c & 63) | 128; } else { out[p++] = (c >> 12) | 224; out[p++] = ((c >> 6) & 63) | 128; out[p++] = (c & 63) | 128; } } return out; }, /** * Function này phải dùng stringToUtf8ByteArray để convert về Byte[] * @param {*} str * @param {*} offset * @returns */ getCrc16(str, offset = 0) { let data = this.stringToUtf8ByteArray(str); if (data == null || offset < 0 || offset > data.length - 1 || offset + length > data.length) { return 0; } let crc = 0xFFFF; for (let i = 0; i < str.length; ++i) { crc ^= data[offset + i] << 8; for (let j = 0; j < 8; ++j) { crc = (crc & 0x8000) > 0 ? (crc << 1) ^ 0x1021 : crc << 1; } } return (crc & 0xFFFF).toString(16).toUpperCase(); }, /** * Function này không dùng stringToUtf8ByteArray để convert về Byte[] * @param {Chuỗi cần check CRC} text * @param {true hoặc false, mặc định là true} hex_output * @returns {Chuỗi CRC} */ getCrc16_array(text, hex_output = true) { // adapted from https://github.com/damonlear/CRC16-CCITT // by https://stackoverflow.com/users/13045193/doubleunary // for https://stackoverflow.com/q/68235740/13045193 // Example: http://www.ip33.com/crc.html if (!Array.isArray(text)) text = [[text]]; const polynomial = 0x1021; let result = text.map(row => row.map(string => { if (!string.length) return null; const bytes = Array.from(String(string)) .map(char => char.charCodeAt(0) & 0xff); // gives 8 bits; higher bits get discarded let crc = 0xffff; bytes.forEach(byte => { for (let i = 0; i < 8; i++) { let bit = 1 === (byte >> (7 - i) & 1); let c15 = 1 === (crc >> 15 & 1); crc <<= 1; if (c15 ^ bit) crc ^= polynomial; } }); crc &= 0xffff; return hex_output ? crc.toString(16).toUpperCase() : crc; })); return result.toString(); }, /** * This function is used for replace special character and Vietnamese Utf-8 character to ASCII character * @param {*} str * @returns */ nonAccentVietnamese(str) { str = str.toLowerCase(); str = str.replace(new RegExp('/', 'g'), '-') // We can also use this instead of from line 11 to line 17 // str = str.replace(/\u00E0|\u00E1|\u1EA1|\u1EA3|\u00E3|\u00E2|\u1EA7|\u1EA5|\u1EAD|\u1EA9|\u1EAB|\u0103|\u1EB1|\u1EAF|\u1EB7|\u1EB3|\u1EB5/g, "a"); // str = str.replace(/\u00E8|\u00E9|\u1EB9|\u1EBB|\u1EBD|\u00EA|\u1EC1|\u1EBF|\u1EC7|\u1EC3|\u1EC5/g, "e"); // str = str.replace(/\u00EC|\u00ED|\u1ECB|\u1EC9|\u0129/g, "i"); // str = str.replace(/\u00F2|\u00F3|\u1ECD|\u1ECF|\u00F5|\u00F4|\u1ED3|\u1ED1|\u1ED9|\u1ED5|\u1ED7|\u01A1|\u1EDD|\u1EDB|\u1EE3|\u1EDF|\u1EE1/g, "o"); // str = str.replace(/\u00F9|\u00FA|\u1EE5|\u1EE7|\u0169|\u01B0|\u1EEB|\u1EE9|\u1EF1|\u1EED|\u1EEF/g, "u"); // str = str.replace(/\u1EF3|\u00FD|\u1EF5|\u1EF7|\u1EF9/g, "y"); // str = str.replace(/\u0111/g, "d"); str = str.replace(/à|á|ạ|ả|ã|â|ầ|ấ|ậ|ẩ|ẫ|ă|ằ|ắ|ặ|ẳ|ẵ/g, "a"); str = str.replace(/è|é|ẹ|ẻ|ẽ|ê|ề|ế|ệ|ể|ễ/g, "e"); str = str.replace(/ì|í|ị|ỉ|ĩ/g, "i"); str = str.replace(/ò|ó|ọ|ỏ|õ|ô|ồ|ố|ộ|ổ|ỗ|ơ|ờ|ớ|ợ|ở|ỡ/g, "o"); str = str.replace(/ù|ú|ụ|ủ|ũ|ư|ừ|ứ|ự|ử|ữ/g, "u"); str = str.replace(/ỳ|ý|ỵ|ỷ|ỹ/g, "y"); str = str.replace(/đ/g, "d"); // Some system encode vietnamese combining accent as individual utf-8 characters str = str.replace(/\u0300|\u0301|\u0303|\u0309|\u0323/g, ""); // Huyền sắc hỏi ngã nặng str = str.replace(/\u02C6|\u0306|\u031B/g, ""); // Â, Ê, Ă, Ơ, Ư return str.toUpperCase().trim(); } } class TLV { constructor(id = 0, name = "", length = 99, is_fixed = true, presense = "O", value = "" || []) { this.tagId = id; this.tagName = name; if (is_fixed) { this.tagLength = length; } else { this.tagLength = name.length; } this.tagValue = value; this.presense = presense; } toString() { let value = "" if (Array.isArray(this.tagValue)) { for (let de = 0; de < this.tagValue.length; de++) { if (this.tagValue[de] instanceof TLV) { // console.log(` subtag ${de}: ${this.tagValue[de]}`); if (this.tagValue[de].tagValue !== "") { // console.log(this.tagValue[de].toString()) value += this.tagValue[de].toString() } } } } else { value = this.tagValue } if (value === "") { return "" } else { this.tagLength = value.length return `${this.tagId >= 10 ? `${this.tagId}` : `0${this.tagId}`}${this.tagLength >= 10 ? `${this.tagLength}` : `0${this.tagLength}`}${value}` } } } //Classes class VIETQR { constructor() { this.data = [] this.fields = FIELDS } toString() { let str = "" // console.log(`Data: ${this.data.length}`) for (let de = 0; de < this.data.length; de++) { if (this.data[de] instanceof TLV) { // console.log(`tag ${de}: ${this.data[de].toString()}`) str += this.data[de].toString() } } let semi_vietqr = `${str}6304` let crc_value = CRC.getCrc16_array(semi_vietqr) return `${semi_vietqr}${crc_value}` } builder() { this.data[0] = new TLV(0, "Payload Format Indicator", 2, true, "M", "01") this.data[1] = new TLV(1, "QR Type", 2, true, "M", this.fields.is_dynamic_qr ? "12" : "11") this.data[38] = new TLV(38, "QR code service on NAPAS system", 99, false, "M", [ new TLV(0, "Global Unique Identifier - GUID", 10, true, "M", NAPAS_GUID), new TLV(1, "Payment network specific", 32, false, "M", [ new TLV(0, "Acquier ID/BNB ID", 6, true, "M", this.fields.acq), new TLV(1, "Merchant ID/Consumer ID", 19, false, "M", this.fields.merchant_id), ]), new TLV(2, "Service Code", 10, false, "C", this.fields.service_code), ]) this.data[52] = new TLV(52, "Merchant Category Code", 4, true, "O", this.fields.merchant_category) this.data[53] = new TLV(53, "Transaction Currency", 3, true, "M", this.fields.currency) this.data[54] = new TLV(54, "Transaction Amount", 13, false, "C", this.fields.is_dynamic_qr?this.fields.amount:"") this.data[55] = new TLV(55, "Tip or Convenience Indicator", 2, true, "O") this.data[56] = new TLV(56, "Value of Convenience Fee Fixed", 13, false, "O") this.data[57] = new TLV(57, "Value of Convenience Fee Percentage", 5, false, "O") this.data[58] = new TLV(58, "Country Code", 2, true, "M", this.fields.country_code) this.data[59] = new TLV(59, "Merchant Name", 25, false, "O", this.fields.merchant_name) this.data[60] = new TLV(60, "Merchant City", 15, false, "O", this.fields.merchant_city) this.data[61] = new TLV(61, "Postal Code", 10, false, "O", this.fields.postal_code) this.data[62] = new TLV(62, "Additional Data Field Template", 99, true, "O", [ null, new TLV(1, "Bill Number", 25, false, "C", this.fields.bill_number), new TLV(2, "Mobile Number", 25, false, "C", this.fields.mobile_number), new TLV(3, "Store Label", 25, false, "O", this.fields.store_label), new TLV(4, "Loyalty Number", 25, false, "O", this.fields.loyalty_number), new TLV(5, "Reference Label", 25, false, "C", this.fields.ref_label), new TLV(6, "Customer Label", 25, false, "C", this.fields.customer_label), new TLV(7, "Terminal Label", 25, false, "O", this.fields.terminal_label), new TLV(8, "Purpose of Transaction", 25, false, "C", CRC.nonAccentVietnamese(this.fields.purpose_txn)), new TLV(9, "Additional Consumer Data Request", 3, false, "O", this.fields.additional_data) ]) this.data[63] = new TLV(63, "CRC (Cyclic Redundancy Check)", 4, true, "M") this.data[64] = new TLV(64, "Merchant Information - Language Template", 2, true, "O", [ new TLV(0, "Language Preference", 2, true, "M", this.fields.lang_ref), new TLV(1, "Merchant Name - Alternate Language", 25, false, "M", this.fields.local_merchant_name), new TLV(2, "Merchant City - Alternate Language", 15, false, "O", this.fields.local_merchant_city), ]) return this.toString() } } //Create VIETQR object for export module.exports = { VIETQR, SERVICE_CODE, CURRENCY };