diff --git a/.eslintrc b/.eslintrc index b717627..4235824 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,67 +1,24 @@ { - "parser": "babel-eslint", - - "parserOptions": { - "sourceType": "module" - }, - - "extends": "eslint:recommended", - - "globals": {}, - - "env": { - "browser": true, - "node": true, - "es6": true, - "jasmine": true, - "mocha": true - }, - - "plugins": [], - - "ecmaFeatures": { - "arrowFunctions": true, - "binaryLiterals": true, - "blockBindings": true, - "classes": true, - "defaultParams": true, - "destructuring": true, - "forOf": true, - "generators": true, - "modules": true, - "objectLiteralComputedProperties": true, - - "objectLiteralDuplicateProperties": true, - "objectLiteralShorthandMethods": true, - "objectLiteralShorthandProperties": true, - "octalLiterals": true, - "regexUFlag": true, - "regexYFlag": true, - "spread": true, - "superInFunctions": true, - "templateStrings": true, - "unicodeCodePointEscapes": true, - "globalReturn": true, - "jsx": true - }, - - - "rules": { - "indent": 0, - "quotes": [2, "single"], - "linebreak-style": [2, "unix"], - "semi": [2, "always"], - "no-console": 0, - "no-case-declarations": 0, - "no-class-assign": 0, - "no-const-assign": 0, - "no-dupe-class-members": 0, - "no-empty-pattern": 0, - "no-new-symbol": 0, - "no-self-assign": 0, - "no-this-before-super": 0, - "no-unexpected-multiline": 0, - "no-unused-labels": 0, - "constructor-super": 0, - }, + "env": { + "node": true, + "mocha": true + }, + "extends": "airbnb", + "rules": { + "prefer-template": 0, + "prefer-rest-params": 0, + "strict": 0, + "no-unused-expressions": 0, + "no-param-reassign": 0, + "max-len": [ + 2, + 100, + 2, + { + "ignoreComments": true, + "ignoreUrls": true, + "ignorePattern": "\/(.*)\/;" + } + ] + } } diff --git a/.gitignore b/.gitignore index b93d42d..4114993 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ## My additions .tmp .idea +.vscode node_modules public dist diff --git a/.hound.yml b/.hound.yml index 3be1f25..bbeeb8b 100644 --- a/.hound.yml +++ b/.hound.yml @@ -1,3 +1,5 @@ +javascript: + enabled: false eslint: enabled: true config_file: .eslintrc diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index dd5de77..0000000 --- a/.jshintignore +++ /dev/null @@ -1,2 +0,0 @@ -/server/client/ -/node_modules/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 9cbe80c..0000000 --- a/.jshintrc +++ /dev/null @@ -1,48 +0,0 @@ -{ - "asi": false, - "bitwise": true, - "browser": true, - "camelcase": false, - "curly": true, - "forin": true, - "immed": true, - "latedef": "nofunc", - "maxlen": 120, - "newcap": true, - "noarg": true, - "noempty": true, - "nonew": true, - "predef": [ - "$", - "__dirname", - "after", - "afterEach", - "angular", - "assert", - "before", - "beforeEach", - "by", - "browser", - "chai", - "console", - "describe", - "element", - "expect", - "exports", - "it", - "inject", - "jQuery", - "jasmine", - "module", - "moment", - "process", - "require", - "should", - "sinon" - ], - "quotmark": true, - "strict": false, - "trailing": true, - "undef": true, - "unused": true -} diff --git a/package.json b/package.json index 631c9f9..1e4c0a2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,10 @@ "babel-eslint": "^6.0.4", "babel-preset-es2015": "^6.6.0", "babel-preset-stage-0": "^6.5.0", - "eslint": "^2.8.0", + "eslint": "^2.10.2", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.8.0", + "eslint-plugin-jsx-a11y": "^1.2.2", "eslint-plugin-react": "^5.1.1", "nodemon": "^1.9.2" }, diff --git a/server/config/index.js b/server/config/index.js index 291d930..7aa6be0 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -9,7 +9,7 @@ module.exports = (value) => { const environments = { development: envVariables, staging: envVariables, - production: envVariables + production: envVariables, }; return environments[value] ? environments[value] : environments.development; -} +}; diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index c582d9a..d9dfcb8 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -3,89 +3,89 @@ module.exports = [{ return_code: 0, status_code: 200, - message: 'Transaction carried successfully' + message: 'Transaction carried successfully', }, { return_code: 9, status_code: 400, - message: 'The merchant ID provided does not exist in our systems' + message: 'The merchant ID provided does not exist in our systems', }, { return_code: 10, status_code: 400, - message: 'The phone number(MSISDN) provided isn’t registered on M-PESA' + message: 'The phone number(MSISDN) provided isn’t registered on M-PESA', }, { return_code: 30, status_code: 400, - message: 'Missing reference ID' + message: 'Missing reference ID', }, { return_code: 31, status_code: 400, - message: 'The request amount is invalid or blank' + message: 'The request amount is invalid or blank', }, { return_code: 36, status_code: 400, - message: 'Incorrect credentials are provided in the request' + message: 'Incorrect credentials are provided in the request', }, { return_code: 40, status_code: 400, - message: 'Missing required parameters' + message: 'Missing required parameters', }, { return_code: 41, status_code: 400, - message: 'MSISDN(phone number) is in incorrect format' + message: 'MSISDN(phone number) is in incorrect format', }, { return_code: 32, status_code: 401, - message: 'The merchant/paybill account in the request hasn’t been activated' + message: 'The merchant/paybill account in the request hasn’t been activated', }, { return_code: 33, status_code: 401, - message: 'The merchant/paybill account hasn’t been approved to transact' + message: 'The merchant/paybill account hasn’t been approved to transact', }, { return_code: 1, status_code: 402, - message: 'Client has insufficient funds to complete the transaction' + message: 'Client has insufficient funds to complete the transaction', }, { return_code: 3, status_code: 402, - message: 'The amount to be transacted is less than the minimum single transfer amount allowed' + message: 'The amount to be transacted is less than the minimum single transfer amount allowed', }, { return_code: 4, status_code: 402, - message: 'The amount to be transacted is more than the maximum single transfer amount allowed' + message: 'The amount to be transacted is more than the maximum single transfer amount allowed', }, { return_code: 8, status_code: 402, - message: 'The client has reached his/her maximum transaction limit for the day' + message: 'The client has reached his/her maximum transaction limit for the day', }, { return_code: 35, status_code: 409, - message: 'A duplicate request has been detected' + message: 'A duplicate request has been detected', }, { return_code: 43, status_code: 409, - message: "Duplicate merchant transaction ID detected", + message: 'Duplicate merchant transaction ID detected', }, { return_code: 12, status_code: 409, - message: 'The transaction details are different from original captured request details' + message: 'The transaction details are different from original captured request details', }, { return_code: 6, status_code: 503, - message: 'Transaction could not be confirmed possibly due to the operation failing' + message: 'Transaction could not be confirmed possibly due to the operation failing', }, { return_code: 11, status_code: 503, - message: 'The system is unable to complete the transaction' + message: 'The system is unable to complete the transaction', }, { return_code: 34, status_code: 503, - message: 'A delay is being experienced while processing requests' + message: 'A delay is being experienced while processing requests', }, { return_code: 29, status_code: 503, - message: 'The system is inaccessible; The system may be down' + message: 'The system is inaccessible; The system may be down', }, { return_code: 5, status_code: 504, - message: 'Duration provided to complete the transaction has expired' + message: 'Duration provided to complete the transaction has expired', }]; diff --git a/server/controllers/ConfirmPayment.js b/server/controllers/ConfirmPayment.js index 1ee6b94..225c05a 100644 --- a/server/controllers/ConfirmPayment.js +++ b/server/controllers/ConfirmPayment.js @@ -2,7 +2,7 @@ module.exports = class ConfirmPayment { constructor(data) { - let transactionConfirmRequest = typeof data.transactionID !== undefined ? + const transactionConfirmRequest = typeof data.transactionID !== undefined ? '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; @@ -25,4 +25,4 @@ module.exports = class ConfirmPayment { requestBody() { return this.body; } -} +}; diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index 8c7c2b0..8049215 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -16,7 +16,9 @@ module.exports = class PaymentRequest { ${data.referenceID} ${data.amountInDoubleFloat} ${data.clientPhoneNumber} - ${data.extraMerchantPayload ? JSON.stringify(data.extraMerchantPayload) : ''} + + ${data.extraMerchantPayload ? JSON.stringify(data.extraMerchantPayload) : ''} + ${process.env.CALLBACK_URL} ${process.env.CALLBACK_METHOD} ${data.timeStamp} @@ -28,4 +30,4 @@ module.exports = class PaymentRequest { requestBody() { return this.body; } -} +}; diff --git a/server/controllers/PaymentStatus.js b/server/controllers/PaymentStatus.js index 7ed13b0..af8f939 100644 --- a/server/controllers/PaymentStatus.js +++ b/server/controllers/PaymentStatus.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = class PaymentStatus { constructor(data) { - let transactionStatusRequest = typeof data.transactionID !== undefined ? + const transactionStatusRequest = typeof data.transactionID !== undefined ? '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; @@ -24,4 +24,4 @@ module.exports = class PaymentStatus { requestBody() { return this.body; } -} +}; diff --git a/server/controllers/SOAPRequest.js b/server/controllers/SOAPRequest.js index 8c60a33..d9f02c8 100644 --- a/server/controllers/SOAPRequest.js +++ b/server/controllers/SOAPRequest.js @@ -6,13 +6,13 @@ module.exports = class SOAPRequest { constructor(payment, parser) { this.parser = parser; this.requestOptions = { - 'method': 'POST', - 'uri': process.env.ENDPOINT, - 'rejectUnauthorized': false, - 'body': payment.requestBody(), - 'headers': { - 'content-type': 'application/xml; charset=utf-8' - } + method: 'POST', + uri: process.env.ENDPOINT, + rejectUnauthorized: false, + body: payment.requestBody(), + headers: { + 'content-type': 'application/xml; charset=utf-8', + }, }; } @@ -25,8 +25,8 @@ module.exports = class SOAPRequest { return; } - let parsedResponse = this.parser.parse(body); - let json = parsedResponse.toJSON(); + const parsedResponse = this.parser.parse(body); + const json = parsedResponse.toJSON(); // Anything that is not "00" as the // SOAP response code is a Failure @@ -40,4 +40,4 @@ module.exports = class SOAPRequest { }); }); } -} +}; diff --git a/server/errors/ResponseError.js b/server/errors/ResponseError.js index 3ca65f2..ee578af 100644 --- a/server/errors/ResponseError.js +++ b/server/errors/ResponseError.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = function ResponseError(error, res) { - let err = new Error('description' in error ? error.description : error); + const err = new Error('description' in error ? error.description : error); err.status = 'status_code' in error ? error.status_code : 500; return res.status(err.status).json({ response: error }); -} +}; diff --git a/server/routes/index.js b/server/routes/index.js index 55401db..5b85592 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,7 +1,7 @@ 'use strict'; const uuid = require('node-uuid'); const request = require('request'); -const ResponseError = require('../errors/ResponseError'); +const responseError = require('../errors/ResponseError'); const ParseResponse = require('../utils/ParseResponse'); const requiredParams = require('../validators/requiredParams'); const PaymentRequest = require('../controllers/PaymentRequest'); @@ -12,12 +12,10 @@ const SOAPRequest = require('../controllers/SOAPRequest'); module.exports = (router) => { /* Check the status of the API system */ - router.get('/', (req, res) => { - return res.json({ 'status': 200 }); - }); + router.get('/', (req, res) => res.json({ status: 200 })); router.post('/payment/request', requiredParams, (req, res) => { - let paymentDetails = { + const paymentDetails = { // transaction reference ID referenceID: (req.body.referenceID || uuid.v4()), // product, service or order ID @@ -26,12 +24,12 @@ module.exports = (router) => { clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), extraPayload: req.body.extraPayload, timeStamp: req.timeStamp, - encryptedPassword: req.encryptedPassword + encryptedPassword: req.encryptedPassword, }; - let payment = new PaymentRequest(paymentDetails); - let parser = new ParseResponse('processcheckoutresponse'); - let request = new SOAPRequest(payment, parser); + const payment = new PaymentRequest(paymentDetails); + const parser = new ParseResponse('processcheckoutresponse'); + const soapRequest = new SOAPRequest(payment, parser); // remove encryptedPassword // should not be added to response object @@ -40,72 +38,72 @@ module.exports = (router) => { // convert paymentDetails properties to underscore notation // to match the SAG JSON response for (const key of Object.keys(paymentDetails)) { - let newkey = key.replace(/[A-Z]{1,}/g, match => '_' + match.toLowerCase()); + const newkey = key.replace(/[A-Z]{1,}/g, match => '_' + match.toLowerCase()); paymentDetails[newkey] = paymentDetails[key]; delete paymentDetails[key]; } // make the payment requets and process response - request.post() + soapRequest.post() .then(response => res.json({ - response: Object.assign({}, response, paymentDetails) + response: Object.assign({}, response, paymentDetails), })) - .catch(error => ResponseError(error, res)); + .catch(error => responseError(error, res)); }); router.get('/payment/confirm/:id', (req, res) => { - let payment = new ConfirmPayment({ + const payment = new ConfirmPayment({ transactionID: req.params.id, // eg. '99d0b1c0237b70f3dc63f36232b9984c' timeStamp: req.timeStamp, - encryptedPassword: req.encryptedPassword + encryptedPassword: req.encryptedPassword, }); - let parser = new ParseResponse('transactionconfirmresponse'); - let confirm = new SOAPRequest(payment, parser); + const parser = new ParseResponse('transactionconfirmresponse'); + const confirm = new SOAPRequest(payment, parser); // process ConfirmPayment response confirm.post() - .then(response => res.json({ response: response })) - .catch(error => ResponseError(error, res)); + .then(response => res.json({ response })) + .catch(error => responseError(error, res)); }); router.get('/payment/status/:id', (req, res) => { - let payment = new PaymentStatus({ + const payment = new PaymentStatus({ transactionID: req.params.id, timeStamp: req.timeStamp, - encryptedPassword: req.encryptedPassword + encryptedPassword: req.encryptedPassword, }); - let parser = new ParseResponse('transactionstatusresponse'); - let status = new SOAPRequest(payment, parser); + const parser = new ParseResponse('transactionstatusresponse'); + const status = new SOAPRequest(payment, parser); // process PaymentStatus response status.post() - .then(response => res.json({ response: response })) - .catch(error => ResponseError(error, res)); + .then(response => res.json({ response })) + .catch(error => responseError(error, res)); }); // the SAG pings a callback request provided // via SOAP POST, HTTP POST or GET request router.all('/payment/success', (req, res) => { const keys = Object.keys(req.body); - let response = {}; - let baseURL = `${req.protocol}://${req.hostname}:${process.env.PORT}`; - let testEndpoint = `${baseURL}/api/v1/thumbs/up`; - let endpoint = 'MERCHANT_ENDPOINT' in process.env ? process.env.MERCHANT_ENDPOINT : testEndpoint; - console.log('endpoint:', endpoint) + const response = {}; + const baseURL = `${req.protocol}://${req.hostname}:${process.env.PORT}`; + const testEndpoint = `${baseURL}/api/v1/thumbs/up`; + const endpoint = 'MERCHANT_ENDPOINT' in process.env ? + process.env.MERCHANT_ENDPOINT : testEndpoint; for (const x of keys) { - let prop = x.toLowerCase().replace(/\-/g, ''); + const prop = x.toLowerCase().replace(/\-/g, ''); response[prop] = req.body[x]; } const requestParams = { - 'method': 'POST', - 'uri': endpoint, - 'rejectUnauthorized': false, - 'body': JSON.stringify(response), - 'headers': { - 'content-type': 'application/json; charset=utf-8' - } + method: 'POST', + uri: endpoint, + rejectUnauthorized: false, + body: JSON.stringify(response), + headers: { + 'content-type': 'application/json; charset=utf-8', + }, }; // make a request to the merchant's endpoint @@ -120,9 +118,7 @@ module.exports = (router) => { // for testing last POST response // if MERCHANT_ENDPOINT has not been provided - router.all('/thumbs/up', (req, res) => { - return res.sendStatus(200); - }); + router.all('/thumbs/up', (req, res) => res.sendStatus(200)); return router; }; diff --git a/server/utils/GenEncryptedPassword.js b/server/utils/GenEncryptedPassword.js index 1b9c271..e74cb16 100644 --- a/server/utils/GenEncryptedPassword.js +++ b/server/utils/GenEncryptedPassword.js @@ -4,11 +4,15 @@ const crypto = require('crypto'); module.exports = class GenEncryptedPassword { constructor(timeStamp) { - let concatenatedString = [process.env.PAYBILL_NUMBER, process.env.PASSKEY, timeStamp].join(''); - let hash = crypto.createHash('sha256'); + const concatenatedString = [ + process.env.PAYBILL_NUMBER, + process.env.PASSKEY, + timeStamp, + ].join(''); + const hash = crypto.createHash('sha256'); this.hashedPassword = hash.update(concatenatedString).digest('hex'); // or 'binary' this.hashedPassword = new Buffer(this.hashedPassword).toString('base64'); // this.hashedPassword = this.hashedPassword.toUpperCase(); // console.log('hashedPassword ==> ', this.hashedPassword); } -} +}; diff --git a/server/utils/ParseResponse.js b/server/utils/ParseResponse.js index 92e86df..5ee960f 100644 --- a/server/utils/ParseResponse.js +++ b/server/utils/ParseResponse.js @@ -10,25 +10,25 @@ module.exports = class ParseResponse { } parse(soapResponse) { - let XMLHeader = /\<\?[\w\s\=\.\-\'\"]+\?\>/gi; - let soapHeaderPrefixes = /(\<([\w\-]+\:[\w\-]+\s)([\w\=\-\:\"\'\\\/\.]+\s?)+?\>)/gi; + const XMLHeader = /<\?[\w\s=.\-'"]+\?>/gi; + const soapHeaderPrefixes = /(<([\w\-]+:[\w\-]+\s)([\w=\-:"'\\\/\.]+\s?)+?>)/gi; // Remove the XML header tag soapResponse = soapResponse.replace(XMLHeader, ''); // Get the element PREFIXES from the soap wrapper - let soapInstance = soapResponse.match(soapHeaderPrefixes); + const soapInstance = soapResponse.match(soapHeaderPrefixes); let soapPrefixes = soapInstance[0].match(/((xmlns):[\w\-]+)+/gi); soapPrefixes = soapPrefixes.map(prefix => prefix.split(':')[1].replace(/\s+/gi, '')); // Now clean the SOAP elements in the response soapPrefixes.forEach(prefix => { - let xmlPrefixes = new RegExp(prefix + ':', 'gmi'); + const xmlPrefixes = new RegExp(prefix + ':', 'gmi'); soapResponse = soapResponse.replace(xmlPrefixes, ''); }); // Remove xmlns from the soap wrapper - soapResponse = soapResponse.replace(/(xmlns)\:/gmi, ''); + soapResponse = soapResponse.replace(/(xmlns):/gmi, ''); // lowercase and trim before returning it this.response = soapResponse.toLowerCase().trim(); @@ -37,7 +37,7 @@ module.exports = class ParseResponse { toJSON() { this.json = {}; - let $ = cheerio.load(this.response, { xmlMode: true }); + const $ = cheerio.load(this.response, { xmlMode: true }); // Get the children tagName and its values $(this.bodyTagName).children().each((i, el) => { @@ -61,6 +61,6 @@ module.exports = class ParseResponse { } extractCode() { - return _.find(statusCodes, (o) => o.return_code == this.json.return_code); + return _.find(statusCodes, (o) => o.return_code === this.json.return_code); } -} +}; diff --git a/server/utils/ucFirst.js b/server/utils/ucFirst.js index aad3380..a097862 100644 --- a/server/utils/ucFirst.js +++ b/server/utils/ucFirst.js @@ -2,24 +2,24 @@ // ucFirst (typeof String): // returns String with first character uppercased module.exports = (string) => { - let word = string, - ucFirstWord = ''; + const word = string; + let ucFirstWord = ''; for (let x = 0, length = word.length; x < length; x++) { // get the character's ASCII code - let character = word[x], + let character = word[x]; // check to see if the character is capitalised/in uppercase using REGEX - isUpperCase = /[A-Z]/g.test(character), - asciiCode = character.charCodeAt(0); + const isUpperCase = /[A-Z]/g.test(character); + const asciiCode = character.charCodeAt(0); - if ((asciiCode >= 65 && asciiCode <= (65 + 25)) || (asciiCode >= 97 && asciiCode <= (97 + 25))) { + if ((asciiCode >= 65 && asciiCode <= (65 + 25)) || + (asciiCode >= 97 && asciiCode <= (97 + 25))) { // If the 1st letter is not in uppercase if (!isUpperCase && x === 0) { // capitalize the letter, then convert it back to decimal value character = String.fromCharCode(asciiCode - 32); - } - // lowercase any of the letters that are not in the 1st postion that are in uppercase - else if (isUpperCase && x > 0) { + } else if (isUpperCase && x > 0) { + // lowercase any of the letters that are not in the 1st postion that are in uppercase // lower case the letter, converting it back to decimal value character = String.fromCharCode(asciiCode + 32); } diff --git a/server/validators/requiredParams.js b/server/validators/requiredParams.js index 2e9230c..95f6d38 100644 --- a/server/validators/requiredParams.js +++ b/server/validators/requiredParams.js @@ -3,7 +3,7 @@ module.exports = (req, res, next) => { 'referenceID', 'merchantTransactionID', 'totalAmount', - 'phoneNumber' + 'phoneNumber', ]; if ('phoneNumber' in req.body) { @@ -22,7 +22,7 @@ module.exports = (req, res, next) => { } if (/^[\d]+$/g.test(req.body.totalAmount)) { - req.body.totalAmount = (parseInt(req.body.totalAmount)).toFixed(2) + req.body.totalAmount = (parseInt(req.body.totalAmount, 10)).toFixed(2); } } else { return res.status(400).send('No [totalAmount] parameter was found'); @@ -34,11 +34,11 @@ module.exports = (req, res, next) => { // anything that is not a required param // should be added to the extraPayload object for (const key of bodyParamKeys) { - if (requiredBodyParams.indexOf(key) == -1) { + if (requiredBodyParams.indexOf(key) === -1) { req.body.extraPayload[key] = req.body[key]; delete req.body[key]; } } - next(); + return next(); };