commit e42ccf61aaa1d2d532a3b9d4bb1c48edb2a8f94b Author: ryan.chan Date: Mon Oct 28 13:59:20 2019 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..047c82e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules/* +example/node_modules/* +example/package-lock.json +package-lock.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf98025 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Ryan Chan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..404194a --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +## hk-fps + +A Nodejs module that help to generate QR code content string of the Hong Kong Fast Payment System. + +## Installation + +Install with npm + + npm install node-fps-hk + +and in your code + + + var fps = require('node-fps-hk') + +## Usage + + + //import module + var fps = require('node-fps-hk') + + // set custom variables + fps.setMerchantID("0000001"); + fps.setBillNumber("0002"); + fps.setStoreLabel("0003"); + fps.setLoyaltyNumber("0004"); + fps.setCustomerLabel("0005"); + fps.setTerminalLabel("0006"); + fps.setPurposeOfTransaction("0007"); + fps.setMobileNumber("12345678"); + fps.setTransactionAmount("5000"); + fps.setReferenceLabel("ABCD"); + + //generate qr content string + var qrContent = fps.generate(); + +## Example + + cd ./example + npm install + node index.js + +visit `http://localhost:8080` + +## License +MIT + +## Useful Links +Please find the specification of the QR Code used in FPS : [https://fps.hkicl.com.hk/eng/fps/merchants/qr_code.php](https://fps.hkicl.com.hk/eng/fps/merchants/qr_code.php) + +The QR Code used the **CRC16 CCITT** check sum. Please find more details : [http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html](http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html) \ No newline at end of file diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..e416fa5 --- /dev/null +++ b/example/index.js @@ -0,0 +1,22 @@ +var fps = require('node-fps-hk') +var qrimage = require('qr-image'); +var http = require('http'); + +http.createServer(function (req, res) { + if (req.url == '/') { + fps.setMerchantID("0000001"); + fps.setBillNumber("0002"); + fps.setStoreLabel("0003"); + fps.setLoyaltyNumber("0004"); + fps.setCustomerLabel("0005"); + fps.setTerminalLabel("0006"); + fps.setPurposeOfTransaction("0007"); + fps.setMobileNumber("12345678"); + fps.setTransactionAmount("5000"); + fps.setReferenceLabel("ABCD"); + var string = fps.generate(); + var code = qrimage.image(string, { type: 'png' }); + res.setHeader('Content-type', 'image/png'); //sent qr image to client side + code.pipe(res); + } +}).listen(8080); diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..2f3f9a5 --- /dev/null +++ b/example/package.json @@ -0,0 +1,17 @@ +{ + "name": "example", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "http": "0.0.0", + "node-fps-hk": "file:..", + "qr-image": "^3.2.0" + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..2df3bb0 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./src/index.js') \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd02f8a --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "node-fps-hk", + "version": "0.1.0", + "description": "A tool to generate the qrcode content string used by Fast Payment System in Hong Kong", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ryanchanplc/node-fps-hk.git" + }, + "files": [ + "/src", + "./index.js" + ], + "author": "Ryan Chan", + "license": "MIT", + "devDependencies": { + "mocha": "^6.2.2", + "rewire": "^4.0.1", + "should": "^13.2.3" + }, + "keywords": [ + "fps", + "hk", + "payment", + "fast payment system" + ] +} \ No newline at end of file diff --git a/src/crc.js b/src/crc.js new file mode 100644 index 0000000..5d16299 --- /dev/null +++ b/src/crc.js @@ -0,0 +1,32 @@ +function computeCheckSum(input, seed) { + var result = seed; + var temp; + var crc_table = generateCRC16Table(); + for (var i = 0, len = input.length; i < len; ++i) { + temp = (input[i] ^ (result >> 8)) & 0xff; + result = crc_table[temp] ^ (result << 8); + } + return result; +} + +function generateCRC16Table() { + var table = []; + var poly = 0x1021; + var temp; + for (var i = 0; i < 256; ++i) { + temp = (i << 8) & 0xFFFF; + for (var j = 0; j < 8; ++j) { + var bit = (temp & 0x8000) + temp <<= 1; + if (bit) { + temp ^= poly; + } + } + table[i] = temp & 0xFFFF; + } + return table; +} + +module.exports = { + computeCheckSum: computeCheckSum +}; diff --git a/src/id.js b/src/id.js new file mode 100644 index 0000000..0d5d69b --- /dev/null +++ b/src/id.js @@ -0,0 +1,68 @@ +module.exports = Object.freeze({ + /* + * 00 Payload Format Indicator + */ + PAYLOAD_FORMAT: '00', + /* + * 01 Point of Initiation Method + */ + POINT_OF_INITIATION: '01', + /* + * 26 Reserved for the Faster Payment System for use in Hong Kong + */ + MERCHANT_ACC_INFO: '26', + /* + * 26 00 Globally Unique Identifier + */ + MERCHANT_ACC_INFO_GLOBALLY_UID: '00', + /* + * 26 02 Merchant ID + */ + MERCHANT_ACC_INFO_MERCHANT_ID: '02', + /* + * 52 Point of Initiation Method + */ + MERCHANT_CAT_CODE: '52', + /* + * 53 Transaction Currency + */ + TRANSACTION_CURRENCY: '53', + /* + * 54 Transaction Amount + */ + TRANSACTION_AMOUNT: '54', + /* + * 58 Country Code + */ + COUNTRY_CODE: '58', + /* + * 59 Merchant Name + */ + MERCHANT_NAME: '59', + /* + * 60 Merchant City + */ + MERCHANT_CITY: '60', + /* + * 62 Additional Data Template + */ + ADDITIONAL_DATA: '62', + ADDITIONAL_DATA_BILL_NUMBER: '01', + ADDITIONAL_DATA_MOBILE_NUMBER: '02', + ADDITIONAL_DATA_STORE_LABEL: '03', + ADDITIONAL_DATA_LOYALTY_NUMBER: '04', + ADDITIONAL_DATA_REFERENCE_LABEL: '05', + ADDITIONAL_DATA_CUSTOMER_LABEL: '06', + ADDITIONAL_DATA_TERMINAL_LABEL: '07', + ADDITIONAL_DATA_PURPOSE_OF_TRANSACTION: '08', + ADDITIONAL_DATA_CUSTOMER_DATA_REQUEST: '09', + /* + * 63 Cyclic Redundancy Check + */ + CRC_CHECK: '63', //63 + /* + * 64 Merchant Information - Language Template + */ + MERCHANT_INFO: '64', + +}); \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..568b794 --- /dev/null +++ b/src/index.js @@ -0,0 +1,158 @@ +const ID = require("./id"); +var Payload = require("./payload"); +var CRC = require("./crc"); + +var _uid = "hk.com.hkicl"; +var _merchantID = ""; +var _transactionAmount = ""; +var _billNumber = ""; +var _mobileNumber = ""; +var _storeLabel = ""; +var _loyaltyNumber = ""; +var _referenceLabel = ""; +var _customerLabel = ""; +var _terminalLabel = ""; +var _purposeOfTransaction = ""; +var _additionalCustomerDataRequest = ""; + +function getPayLoadFormat() { + return new Payload(ID.PAYLOAD_FORMAT, "01").toString(); +} + +function getPointofInitiation() { + if (_transactionAmount || _transactionAmount != "") { + return new Payload(ID.POINT_OF_INITIATION, "12").toString(); + } else { + return new Payload(ID.POINT_OF_INITIATION, "11").toString(); + } +} + +function getMerhantCategoryCode() { + return new Payload(ID.MERCHANT_CAT_CODE, "0000").toString(); +} + +function getTransactionCurrency() { + return new Payload(ID.TRANSACTION_CURRENCY, "344").toString(); +} + +function getCountryCode() { + return new Payload(ID.COUNTRY_CODE, "HK").toString(); +} + +function getMerchantName() { + return new Payload(ID.MERCHANT_NAME, "NA").toString(); +} + +function getMerchantCity() { + return new Payload(ID.MERCHANT_CITY, "HK").toString(); +} + +function getMerchantAccountInfo() { + var uid = new Payload(ID.MERCHANT_ACC_INFO_GLOBALLY_UID, _uid).toString(); + var merc = new Payload(ID.MERCHANT_ACC_INFO_MERCHANT_ID, _merchantID).toString(); + return new Payload(ID.MERCHANT_ACC_INFO, uid + merc).toString(); +} + +function getTransactionAmount() { + return new Payload(ID.TRANSACTION_AMOUNT, _transactionAmount).toString(); +} + +function getAdditionalInformation() { + var payload = "" + payload += new Payload(ID.ADDITIONAL_DATA_BILL_NUMBER, _billNumber); + payload += new Payload(ID.ADDITIONAL_DATA_MOBILE_NUMBER, _mobileNumber); + payload += new Payload(ID.ADDITIONAL_DATA_STORE_LABEL, _storeLabel); + payload += new Payload(ID.ADDITIONAL_DATA_LOYALTY_NUMBER, _loyaltyNumber); + payload += new Payload(ID.ADDITIONAL_DATA_REFERENCE_LABEL, _referenceLabel); + payload += new Payload(ID.ADDITIONAL_DATA_CUSTOMER_LABEL, _customerLabel); + payload += new Payload(ID.ADDITIONAL_DATA_TERMINAL_LABEL, _terminalLabel); + payload += new Payload(ID.ADDITIONAL_DATA_PURPOSE_OF_TRANSACTION, _purposeOfTransaction); + payload += new Payload(ID.ADDITIONAL_DATA_CUSTOMER_DATA_REQUEST, _additionalCustomerDataRequest); + + return new Payload(ID.ADDITIONAL_DATA, payload).toString(); +} + +function addCheckSum(string) { + var checkSum = getCheckSUM(string + ID.CRC_CHECK + "04"); + return new Payload(ID.CRC_CHECK, checkSum).toString(); +} + +function getCheckSUM(string) { + var input = Buffer.from(string, 'utf8'); + var output = CRC.computeCheckSum(input, 0xffff); + var o = Buffer.from([output >> 8, output & 0xff]); + return o.toString('hex').toUpperCase(); +} + +function setMerchantID(value) { + _merchantID = value; +} + +function setTransactionAmount(value) { + _transactionAmount = parseFloat(value).toFixed(2).toString(); +} +function setBillNumber(value) { + _billNumber = value; +} +function setMobileNumber(value) { + _mobileNumber = value; +} +function setStoreLabel(value) { + _storeLabel = value; +} + +function setLoyaltyNumber(value) { + _loyaltyNumber = value; +} + +function setReferenceLabel(value) { + _referenceLabel = value; +} + +function setCustomerLabel(value) { + _customerLabel = value; +} + +function setTerminalLabel(value) { + _terminalLabel = value; +} + +function setPurposeOfTransaction(value) { + _purposeOfTransaction = value; +} + +function setAdditionalCustomerDataRequest(value) { + _additionalCustomerDataRequest = value; +} + +function generate() { + var result = ""; + result += getPayLoadFormat(); + result += getPointofInitiation(); + result += getMerchantAccountInfo(); + result += getMerhantCategoryCode(); + result += getTransactionCurrency(); + result += getTransactionAmount(); + result += getCountryCode(); + result += getMerchantName(); + result += getMerchantCity(); + result += getAdditionalInformation(); + result += addCheckSum(result); + + return result; +} + +module.exports = { + setAdditionalCustomerDataRequest, + setBillNumber, + setCustomerLabel, + setMerchantID, + setMobileNumber, + setPurposeOfTransaction, + setLoyaltyNumber, + setReferenceLabel, + setStoreLabel, + setTerminalLabel, + setTransactionAmount, + generate +} \ No newline at end of file diff --git a/src/payload.js b/src/payload.js new file mode 100644 index 0000000..c21c39d --- /dev/null +++ b/src/payload.js @@ -0,0 +1,22 @@ +var Payload = function (_id, _value) { + this.id = _id; + this.value = _value; + + if (this.value) + this.length = _value.length; + else + this.length = 0; +}; + +Payload.prototype.toString = function () { + if (this.value == "") + return "" + else + return this.id + this.pad(parseInt(this.length)) + this.value; +} + +Payload.prototype.pad = function (num) { + return (num < 10) ? '0' + num.toString() : num.toString(); +} + +module.exports = Payload; \ No newline at end of file diff --git a/test/crctable.js b/test/crctable.js new file mode 100644 index 0000000..942e0d4 --- /dev/null +++ b/test/crctable.js @@ -0,0 +1,258 @@ +module.exports = [ + 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 +]; diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..6de2291 --- /dev/null +++ b/test/test.js @@ -0,0 +1,33 @@ +var should = require('should'); +var rewire = require("rewire"); +var crc = rewire('../src/crc'); +var fps = rewire('../src/index'); +const expected_CRC = require('./crctable.js'); +const testContent = "00020101021226270012hk.com.hkicl0207000000152040000530334454075000.005802HK5902NA6002HK62680104000202081234567803040003040400040504ABCD0604000507040006080400076304" +const checkSUM = "8D1D"; +describe('#checkCRCTable', () => { + + it('check the crc generated table', done => { + var generateCRC16Table = crc.__get__('generateCRC16Table'); + var crcArray = generateCRC16Table(); + crcArray.should.eql(expected_CRC) + done(); + }) + + it('check crc checksum', done => { + fps.setMerchantID("0000001"); + fps.setBillNumber("0002"); + fps.setStoreLabel("0003"); + fps.setLoyaltyNumber("0004"); + fps.setCustomerLabel("0005"); + fps.setTerminalLabel("0006"); + fps.setPurposeOfTransaction("0007"); + fps.setMobileNumber("12345678"); + fps.setTransactionAmount("5000"); + fps.setReferenceLabel("ABCD"); + var qrContent = fps.generate(); + + qrContent.should.equal(testContent + checkSUM) + done(); + }) +}); \ No newline at end of file