From c59870a0d8b76554e2f6b993aed491ffad552c4e Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 17 May 2016 23:57:10 +0300 Subject: [PATCH 01/96] Moved request call to it's own class --- server/controllers/request.js | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 server/controllers/request.js diff --git a/server/controllers/request.js b/server/controllers/request.js new file mode 100644 index 0000000..018484c --- /dev/null +++ b/server/controllers/request.js @@ -0,0 +1,42 @@ +import request from 'request'; + + +export default 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' + } + }; + } + + post() { + return new Promise((resolve, reject) => { + // Make the soap request to the SAG URI + request(this.requestOptions, (_error, response, body) => { + if (_error) { + reject(_error); + return; + } + + let parsedResponse = this.parser.parse(body); + let json = parsedResponse.toJSON(); + + // Anything that is not "00" as the + // SOAP response code is a Failure + if (json.httpCode !== 200) { + reject(json); + return; + } + + // Else everything went well + resolve(json); + }); + }); + } +} From c7dfb434e9c45c9c7f0f62e446b63320d268de9e Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Wed, 18 May 2016 00:01:51 +0300 Subject: [PATCH 02/96] Refactored payment request class and route to use the soap request and response parser as injectors --- server/controllers/parse-response.js | 16 +++++++++--- server/controllers/payment-request.js | 35 +++------------------------ server/routes/index.js | 18 ++++++++++---- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/server/controllers/parse-response.js b/server/controllers/parse-response.js index 80ae67c..a00d00a 100644 --- a/server/controllers/parse-response.js +++ b/server/controllers/parse-response.js @@ -4,13 +4,19 @@ import statusCodes from '../config/status-codes'; export default class ParseResponse { - constructor(soapResponse, bodyTagName) { + constructor(bodyTagName) { this.bodyTagName = bodyTagName; + } + + parse(soapResponse) { + let XMLHeader = /\<\?[\w\s\=\.\-\'\"]+\?\>/gi; + let soapHeaderPrefixes = /(\<([\w\-]+\:[\w\-]+\s)([\w\=\-\:\"\'\\\/\.]+\s?)+?\>)/gi; + // Remove the XML header tag - soapResponse = soapResponse.replace(/\<\?[\w\s\=\.\-\'\"]+\?\>/gmi, ''); + soapResponse = soapResponse.replace(XMLHeader, ''); // Get the element PREFIXES from the soap wrapper - let soapInstance = soapResponse.match(/(\<([\w\-]+\:[\w\-]+\s)([\w\=\-\:\"\'\\\/\.]+\s?)+?\>)/gi); + let soapInstance = soapResponse.match(soapHeaderPrefixes); let soapPrefixes = soapInstance[0].match(/((xmlns):[\w\-]+)+/gi); soapPrefixes = soapPrefixes.map((prefix) => { return prefix.split(':')[1].replace(/\s+/gi, ''); @@ -18,7 +24,8 @@ export default class ParseResponse { // Now clean the SOAP elements in the response soapPrefixes.forEach((prefix) => { - soapResponse = soapResponse.replace(new RegExp(prefix + ':', 'gmi'), ''); + let xmlPrefixes = new RegExp(prefix + ':', 'gmi'); + soapResponse = soapResponse.replace(xmlPrefixes, ''); }); // Remove xmlns from the soap wrapper @@ -26,6 +33,7 @@ export default class ParseResponse { // lowercase and trim before returning it this.response = soapResponse.toLowerCase().trim(); + return this; } toJSON() { diff --git a/server/controllers/payment-request.js b/server/controllers/payment-request.js index c664e19..f5b1e4d 100644 --- a/server/controllers/payment-request.js +++ b/server/controllers/payment-request.js @@ -1,15 +1,13 @@ -import request from 'request'; import moment from 'moment'; import EncryptPassword from './encrypt'; -import ParseResponse from './parse-response'; export default class PaymentRequest { - static construct(data) { + constructor(data) { data.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" data.encryptedPassword = new EncryptPassword(data.timeStamp).hashedPassword; - return ` + this.body = ` ${process.env.PAYBILL_NUMBER} @@ -32,32 +30,7 @@ export default class PaymentRequest { `; } - static send(soapBody) { - return new Promise((resolve, reject) => { - request({ - 'method': 'POST', - 'uri': process.env.ENDPOINT, - 'rejectUnauthorized': false, - 'body': soapBody, - 'headers': { - 'content-type': 'application/xml; charset=utf-8' - } - }, (err, response, body) => { - if (err) { - reject(err); - return; - } - - // console.log('RESPONSE: ', body); - let parsed = new ParseResponse(body, 'processcheckoutresponse'); - let json = parsed.toJSON(); - - if (json.httpCode !== 200) { - reject(json); - return; - } - resolve(json); - }); - }); + requestBody() { + return this.body; } } diff --git a/server/routes/index.js b/server/routes/index.js index 67d9cc8..ad825ae 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,8 +1,10 @@ import uuid from 'node-uuid'; +import ParseResponse from '../controllers/parse-response'; import ResponseError from '../controllers/errorhandler'; import PaymentRequest from '../controllers/payment-request'; import ConfirmPayment from '../controllers/payment-confirm'; import PaymentStatus from '../controllers/payment-status'; +import SOAPRequest from '../controllers/request'; export default function(router) { @@ -12,16 +14,22 @@ export default function(router) { }); router.get('/payment/request', function(req, res) { - let request = PaymentRequest.send(PaymentRequest.construct({ + let extraPayload = { 'extra': 'info', 'as': 'object' }; + let paymentDetails = { referenceID: uuid.v4(), // product, service or order ID merchantTransactionID: uuid.v1(), // time-based amountInDoubleFloat: '10.00', clientPhoneNumber: '254723001575', - extraMerchantPayload: JSON.stringify({ 'extra': 'info', 'as': 'object' }) - })); + extraMerchantPayload: JSON.stringify(extraPayload) + }; + + let payment = new PaymentRequest(paymentDetails); + let parser = new ParseResponse('processcheckoutresponse'); + let soapRequest = new SOAPRequest(payment, parser); - // process request response - request.then((response) => res.json(response)) + // make the payment requets and process response + soapRequest.post() + .then((response) => res.json(response)) .catch((_error) => ResponseError.handler(_error, res)); }); From bb873aef14e90599482012c3d7722dbe5ae48d6a Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Wed, 18 May 2016 00:22:39 +0300 Subject: [PATCH 03/96] Refactored payment confirm class to incorporate changes --- server/controllers/payment-confirm.js | 33 ++++----------------------- server/routes/index.js | 13 +++++++---- 2 files changed, 12 insertions(+), 34 deletions(-) diff --git a/server/controllers/payment-confirm.js b/server/controllers/payment-confirm.js index acb1f70..c796501 100644 --- a/server/controllers/payment-confirm.js +++ b/server/controllers/payment-confirm.js @@ -5,7 +5,7 @@ import ParseResponse from './parse-response'; export default class ConfirmPayment { - static construct(data) { + constructor(data) { data.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" data.encryptedPassword = new EncryptPassword(data.timeStamp).hashedPassword; @@ -13,7 +13,7 @@ export default class ConfirmPayment { '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; - return ` + this.body = ` ${process.env.PAYBILL_NUMBER} @@ -29,32 +29,7 @@ export default class ConfirmPayment { `; } - static send(soapBody) { - return new Promise((resolve, reject) => { - request({ - 'method': 'POST', - 'uri': process.env.ENDPOINT, - 'rejectUnauthorized': false, - 'body': soapBody, - 'headers': { - 'content-type': 'application/xml; charset=utf-8' - } - }, (err, response, body) => { - if (err) { - reject(err); - return; - } - - // console.log('RESPONSE: ', body); - let parsed = new ParseResponse(body, 'transactionconfirmresponse'); - let json = parsed.toJSON(); - - if (json.httpCode !== 200) { - reject(json); - return; - } - resolve(json); - }); - }); + requestBody() { + return this.body; } } diff --git a/server/routes/index.js b/server/routes/index.js index ad825ae..011a488 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -25,21 +25,24 @@ export default function(router) { let payment = new PaymentRequest(paymentDetails); let parser = new ParseResponse('processcheckoutresponse'); - let soapRequest = new SOAPRequest(payment, parser); + let request = new SOAPRequest(payment, parser); // make the payment requets and process response - soapRequest.post() + request.post() .then((response) => res.json(response)) .catch((_error) => ResponseError.handler(_error, res)); }); router.get('/payment/confirm/:id', function(req, res) { - let confirm = ConfirmPayment.send(ConfirmPayment.construct({ + let payment = new ConfirmPayment({ transactionID: req.params.id // eg. '99d0b1c0237b70f3dc63f36232b9984c' - })); + }); + let parser = new ParseResponse('transactionconfirmresponse'); + let confirm = new SOAPRequest(payment, parser); // process ConfirmPayment response - confirm.then((response) => res.json(response)) + confirm.post() + .then((response) => res.json(response)) .catch((_error) => ResponseError.handler(_error, res)); }); From 0634501f46db4ba4c21e558e163f9ce7fad92837 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Wed, 18 May 2016 00:31:16 +0300 Subject: [PATCH 04/96] Refactored payment status class --- server/controllers/payment-confirm.js | 2 -- server/controllers/payment-status.js | 35 +++------------------------ server/routes/index.js | 9 ++++--- 3 files changed, 10 insertions(+), 36 deletions(-) diff --git a/server/controllers/payment-confirm.js b/server/controllers/payment-confirm.js index c796501..9faeb5e 100644 --- a/server/controllers/payment-confirm.js +++ b/server/controllers/payment-confirm.js @@ -1,7 +1,5 @@ -import request from 'request'; import moment from 'moment'; import EncryptPassword from './encrypt'; -import ParseResponse from './parse-response'; export default class ConfirmPayment { diff --git a/server/controllers/payment-status.js b/server/controllers/payment-status.js index 1d984ad..17e851c 100644 --- a/server/controllers/payment-status.js +++ b/server/controllers/payment-status.js @@ -1,11 +1,9 @@ -import request from 'request'; import moment from 'moment'; import EncryptPassword from './encrypt'; -import ParseResponse from './parse-response'; export default class PaymentStatus { - static construct(data) { + constructor(data) { data.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" data.encryptedPassword = new EncryptPassword(data.timeStamp).hashedPassword; @@ -13,7 +11,7 @@ export default class PaymentStatus { '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; - return ` + this.body = ` ${process.env.PAYBILL_NUMBER} @@ -29,32 +27,7 @@ export default class PaymentStatus { `; } - static send(soapBody) { - return new Promise((resolve, reject) => { - request({ - 'method': 'POST', - 'uri': process.env.ENDPOINT, - 'rejectUnauthorized': false, - 'body': soapBody, - 'headers': { - 'content-type': 'application/xml; charset=utf-8' - } - }, (err, response, body) => { - if (err) { - reject(err); - return; - } - - // console.log('RESPONSE: ', body); - let parsed = new ParseResponse(body, 'transactionstatusresponse'); - let json = parsed.toJSON(); - - if (json.httpCode !== 200) { - reject(json); - return; - } - resolve(json); - }); - }); + requestBody(soapBody) { + return this.body; } } diff --git a/server/routes/index.js b/server/routes/index.js index 011a488..ef2ff32 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -47,12 +47,15 @@ export default function(router) { }); router.get('/payment/status/:id', function(req, res) { - let status = PaymentStatus.send(PaymentStatus.construct({ + let payment = new PaymentStatus({ transactionID: req.params.id // eg. '99d0b1c0237b70f3dc63f36232b9984c' - })); + }); + let parser = new ParseResponse('transactionstatusresponse'); + let status = new SOAPRequest(payment, parser); // process PaymentStatus response - status.then((response) => res.json(response)) + status.post() + .then((response) => res.json(response)) .catch((_error) => ResponseError.handler(_error, res)); }); From a094d731dac0548b11ea2a9c25ff5c24133cbbb6 Mon Sep 17 00:00:00 2001 From: Master Yoda Date: Wed, 18 May 2016 09:09:41 +0300 Subject: [PATCH 05/96] Fix readme typos --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 43038d1..b1b7dc2 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ __What MPESA G2 API should have been in the 21st century.__ -__MPESA API RESTful mediator__. Basically converts all merchant requests to the dreaded ancient SOAP/XML -requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. +__MPESA API RESTful mediator__. Basically converts all merchant requests to the dreaded ancient SOAP/XML +requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. -In short, it'll deal with all of the SOAP shinanigans while you REST. +In short, it'll deal with all of the SOAP shenanigans while you REST. The aim of __Project Mulla__, is to create a REST API that interfaces with the ugly MPESA G2 API. -Sounds like a broken reacord, but it was to emphasize what we hope to achieve :) +Sounds like a broken record, but it was to emphasize what we hope to achieve :) ### Since We Know, SOAP! Yuck! @@ -27,11 +27,11 @@ You saw the __LICENSE__ but you were like __*TL;DR*__. Here's sort of WHY I have 2. I almost need people, organisations and companies to have to admit they use my software. -3. I have no social incentive to "give" my software free to companies just after nothing else but profit. +3. I have no social incentive to "give" my software free to companies just after nothing else but profit. Open source to open source, corporation to corporation. -4. To keep you honest. You now have to tell your bosses you’re using my gear. And it will scare - the shit out of them. Why? They risk opensourcing all of their codebase. +4. To keep you honest. You now have to tell your bosses you’re using my gear. And it will scare + the shit out of them. Why? They risk open-sourcing all of their codebase. 5. Some companies who use our software do not give back. The irony of the situation is that, in order to improve my motivation to do open source, I have to charge for it. From ac2bf8ac54755cf58e1b70f4cba318686ae2d051 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 19 May 2016 01:52:03 +0300 Subject: [PATCH 06/96] Refactored the status code httpCode and returnCode object properties to underscore notation --- server/config/status-codes.js | 84 ++++++++++++++-------------- server/controllers/errorhandler.js | 2 +- server/controllers/parse-response.js | 14 ++--- server/controllers/payment-status.js | 2 +- server/controllers/request.js | 2 +- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/server/config/status-codes.js b/server/config/status-codes.js index 95468b0..5e7d813 100644 --- a/server/config/status-codes.js +++ b/server/config/status-codes.js @@ -1,85 +1,85 @@ export default [{ - returnCode: 0, - httpCode: 200, + return_code: 0, + http_code: 200, message: 'Transaction carried successfully' }, { - returnCode: 9, - httpCode: 400, + return_code: 9, + http_code: 400, message: 'The merchant ID provided does not exist in our systems' }, { - returnCode: 10, - httpCode: 400, + return_code: 10, + http_code: 400, message: 'The phone number(MSISDN) provided isn’t registered on M-PESA' }, { - returnCode: 30, - httpCode: 400, + return_code: 30, + http_code: 400, message: 'Missing reference ID' }, { - returnCode: 31, - httpCode: 400, + return_code: 31, + http_code: 400, message: 'The request amount is invalid or blank' }, { - returnCode: 36, - httpCode: 400, + return_code: 36, + http_code: 400, message: 'Incorrect credentials are provided in the request' }, { - returnCode: 40, - httpCode: 400, + return_code: 40, + http_code: 400, message: 'Missing required parameters' }, { - returnCode: 41, - httpCode: 400, + return_code: 41, + http_code: 400, message: 'MSISDN(phone number) is in incorrect format' }, { - returnCode: 32, - httpCode: 401, + return_code: 32, + http_code: 401, message: 'The merchant/paybill account in the request hasn’t been activated' }, { - returnCode: 33, - httpCode: 401, + return_code: 33, + http_code: 401, message: 'The merchant/paybill account hasn’t been approved to transact' }, { - returnCode: 1, - httpCode: 402, + return_code: 1, + http_code: 402, message: 'Client has insufficient funds to complete the transaction' }, { - returnCode: 3, - httpCode: 402, + return_code: 3, + http_code: 402, message: 'The amount to be transacted is less than the minimum single transfer amount allowed' }, { - returnCode: 4, - httpCode: 402, + return_code: 4, + http_code: 402, message: 'The amount to be transacted is more than the maximum single transfer amount allowed' }, { - returnCode: 8, - httpCode: 402, + return_code: 8, + http_code: 402, message: 'The client has reached his/her maximum transaction limit for the day' }, { - returnCode: 35, - httpCode: 409, + return_code: 35, + http_code: 409, message: 'A duplicate request has been detected' }, { - returnCode: 12, - httpCode: 409, + return_code: 12, + http_code: 409, message: 'The transaction details are different from original captured request details' }, { - returnCode: 6, - httpCode: 503, + return_code: 6, + http_code: 503, message: 'Transaction could not be confirmed possibly due to the operation failing' }, { - returnCode: 11, - httpCode: 503, + return_code: 11, + http_code: 503, message: 'The system is unable to complete the transaction' }, { - returnCode: 34, - httpCode: 503, + return_code: 34, + http_code: 503, message: 'A delay is being experienced while processing requests' }, { - returnCode: 29, - httpCode: 503, + return_code: 29, + http_code: 503, message: 'The system is inaccessible; The system may be down' }, { - returnCode: 5, - httpCode: 504, + return_code: 5, + http_code: 504, message: 'Duration provided to complete the transaction has expired' }]; diff --git a/server/controllers/errorhandler.js b/server/controllers/errorhandler.js index 5f3ea5a..79c3017 100644 --- a/server/controllers/errorhandler.js +++ b/server/controllers/errorhandler.js @@ -1,7 +1,7 @@ export default class ResponseError{ static handler(_error, res) { let err = new Error('description' in _error ? _error.description : _error); - err.status = 'httpCode' in _error ? _error.httpCode : 500; + err.status = 'http_code' in _error ? _error.http_code : 500; return res.status(err.status).json({ response: _error }); } } diff --git a/server/controllers/parse-response.js b/server/controllers/parse-response.js index a00d00a..8ddab15 100644 --- a/server/controllers/parse-response.js +++ b/server/controllers/parse-response.js @@ -39,14 +39,15 @@ export default class ParseResponse { toJSON() { this.json = {}; let $ = cheerio.load(this.response, { xmlMode: true }); - $(this.bodyTagName).children().each((i, el) => { - if (el.children.length > 1) { - console.log('Has more than one child.'); - } + // Get the children tagName and its values + $(this.bodyTagName).children().each((i, el) => { + // if (el.children.length > 1) return; if (el.children.length === 1) { // console.log(el.name, el.children[0].data); - this.json[el.name] = el.children[0].data.replace(/\s{2,}/gi, ' ').replace(/\n/gi, '').trim(); + let value = el.children[0].data.replace(/\s{2,}/gi, ' '); + value = value.replace(/\n/gi, '').trim(); + this.json[el.name] = value; } }); @@ -57,11 +58,10 @@ export default class ParseResponse { // Get the equivalent HTTP CODE to respond with this.json = _.assignIn(this.extractCode(), this.json); - delete this.json.return_code; return this.json; } extractCode() { - return _.find(statusCodes, (o) => o.returnCode == this.json.return_code); + return _.find(statusCodes, (o) => o.return_code == this.json.return_code); } } diff --git a/server/controllers/payment-status.js b/server/controllers/payment-status.js index 17e851c..fe05f97 100644 --- a/server/controllers/payment-status.js +++ b/server/controllers/payment-status.js @@ -27,7 +27,7 @@ export default class PaymentStatus { `; } - requestBody(soapBody) { + requestBody() { return this.body; } } diff --git a/server/controllers/request.js b/server/controllers/request.js index 018484c..d7fe9fd 100644 --- a/server/controllers/request.js +++ b/server/controllers/request.js @@ -29,7 +29,7 @@ export default class SOAPRequest { // Anything that is not "00" as the // SOAP response code is a Failure - if (json.httpCode !== 200) { + if (json.http_code !== 200) { reject(json); return; } From d5fb076d5d4a36e045d9519e834ae8fcea0979b5 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 19 May 2016 02:09:01 +0300 Subject: [PATCH 07/96] Made the error handler an exported function --- server/controllers/errorhandler.js | 7 ------- server/controllers/response-error.js | 5 +++++ server/routes/index.js | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 server/controllers/errorhandler.js create mode 100644 server/controllers/response-error.js diff --git a/server/controllers/errorhandler.js b/server/controllers/errorhandler.js deleted file mode 100644 index 79c3017..0000000 --- a/server/controllers/errorhandler.js +++ /dev/null @@ -1,7 +0,0 @@ -export default class ResponseError{ - static handler(_error, res) { - let err = new Error('description' in _error ? _error.description : _error); - err.status = 'http_code' in _error ? _error.http_code : 500; - return res.status(err.status).json({ response: _error }); - } -} diff --git a/server/controllers/response-error.js b/server/controllers/response-error.js new file mode 100644 index 0000000..88eb465 --- /dev/null +++ b/server/controllers/response-error.js @@ -0,0 +1,5 @@ +export default function ResponseError(_error, res) { + let err = new Error('description' in _error ? _error.description : _error); + err.status = 'http_code' in _error ? _error.http_code : 500; + return res.status(err.status).json({ response: _error }); +} diff --git a/server/routes/index.js b/server/routes/index.js index ef2ff32..f170369 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,6 +1,6 @@ import uuid from 'node-uuid'; import ParseResponse from '../controllers/parse-response'; -import ResponseError from '../controllers/errorhandler'; +import ResponseError from '../controllers/response-error'; import PaymentRequest from '../controllers/payment-request'; import ConfirmPayment from '../controllers/payment-confirm'; import PaymentStatus from '../controllers/payment-status'; @@ -30,7 +30,7 @@ export default function(router) { // make the payment requets and process response request.post() .then((response) => res.json(response)) - .catch((_error) => ResponseError.handler(_error, res)); + .catch((_error) => ResponseError(_error, res)); }); router.get('/payment/confirm/:id', function(req, res) { @@ -43,7 +43,7 @@ export default function(router) { // process ConfirmPayment response confirm.post() .then((response) => res.json(response)) - .catch((_error) => ResponseError.handler(_error, res)); + .catch((_error) => ResponseError(_error, res)); }); router.get('/payment/status/:id', function(req, res) { @@ -56,7 +56,7 @@ export default function(router) { // process PaymentStatus response status.post() .then((response) => res.json(response)) - .catch((_error) => ResponseError.handler(_error, res)); + .catch((_error) => ResponseError(_error, res)); }); return router; From b98f6e589f3eb16550dfe7ef0f5ed2af7bee9f03 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 19 May 2016 02:17:20 +0300 Subject: [PATCH 08/96] Removed prefixed underscore from error variable --- server/controllers/response-error.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/controllers/response-error.js b/server/controllers/response-error.js index 88eb465..4086a6c 100644 --- a/server/controllers/response-error.js +++ b/server/controllers/response-error.js @@ -1,5 +1,5 @@ -export default function ResponseError(_error, res) { - let err = new Error('description' in _error ? _error.description : _error); - err.status = 'http_code' in _error ? _error.http_code : 500; - return res.status(err.status).json({ response: _error }); +export default function ResponseError(error, res) { + let err = new Error('description' in error ? error.description : error); + err.status = 'http_code' in error ? error.http_code : 500; + return res.status(err.status).json({ response: error }); } From d2541956493bf1c27ed6f8bb06dbff50f925b4d5 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 19 May 2016 02:18:42 +0300 Subject: [PATCH 09/96] Updated to ES2015 syntax style where there was the need to do so --- server/controllers/parse-response.js | 6 ++---- server/controllers/request.js | 6 +++--- server/routes/index.js | 22 +++++++++++----------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/server/controllers/parse-response.js b/server/controllers/parse-response.js index 8ddab15..6b15c0d 100644 --- a/server/controllers/parse-response.js +++ b/server/controllers/parse-response.js @@ -18,12 +18,10 @@ export default class ParseResponse { // Get the element PREFIXES from the soap wrapper let soapInstance = soapResponse.match(soapHeaderPrefixes); let soapPrefixes = soapInstance[0].match(/((xmlns):[\w\-]+)+/gi); - soapPrefixes = soapPrefixes.map((prefix) => { - return prefix.split(':')[1].replace(/\s+/gi, ''); - }); + soapPrefixes = soapPrefixes.map(prefix => prefix.split(':')[1].replace(/\s+/gi, '')); // Now clean the SOAP elements in the response - soapPrefixes.forEach((prefix) => { + soapPrefixes.forEach(prefix => { let xmlPrefixes = new RegExp(prefix + ':', 'gmi'); soapResponse = soapResponse.replace(xmlPrefixes, ''); }); diff --git a/server/controllers/request.js b/server/controllers/request.js index d7fe9fd..e280c31 100644 --- a/server/controllers/request.js +++ b/server/controllers/request.js @@ -18,9 +18,9 @@ export default class SOAPRequest { post() { return new Promise((resolve, reject) => { // Make the soap request to the SAG URI - request(this.requestOptions, (_error, response, body) => { - if (_error) { - reject(_error); + request(this.requestOptions, (error, response, body) => { + if (error) { + reject(error); return; } diff --git a/server/routes/index.js b/server/routes/index.js index f170369..bd8bd46 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -7,13 +7,13 @@ import PaymentStatus from '../controllers/payment-status'; import SOAPRequest from '../controllers/request'; -export default function(router) { +export default (router) => { /* Check the status of the API system */ - router.get('/', function(req, res) { + router.get('/', (req, res) => { return res.json({ 'status': 200 }); }); - router.get('/payment/request', function(req, res) { + router.get('/payment/request', (req, res) => { let extraPayload = { 'extra': 'info', 'as': 'object' }; let paymentDetails = { referenceID: uuid.v4(), // product, service or order ID @@ -29,11 +29,11 @@ export default function(router) { // make the payment requets and process response request.post() - .then((response) => res.json(response)) - .catch((_error) => ResponseError(_error, res)); + .then(response => res.json(response)) + .catch(error => ResponseError(error, res)); }); - router.get('/payment/confirm/:id', function(req, res) { + router.get('/payment/confirm/:id', (req, res) => { let payment = new ConfirmPayment({ transactionID: req.params.id // eg. '99d0b1c0237b70f3dc63f36232b9984c' }); @@ -42,11 +42,11 @@ export default function(router) { // process ConfirmPayment response confirm.post() - .then((response) => res.json(response)) - .catch((_error) => ResponseError(_error, res)); + .then(response => res.json(response)) + .catch(error => ResponseError(error, res)); }); - router.get('/payment/status/:id', function(req, res) { + router.get('/payment/status/:id', (req, res) => { let payment = new PaymentStatus({ transactionID: req.params.id // eg. '99d0b1c0237b70f3dc63f36232b9984c' }); @@ -55,8 +55,8 @@ export default function(router) { // process PaymentStatus response status.post() - .then((response) => res.json(response)) - .catch((_error) => ResponseError(_error, res)); + .then(response => res.json(response)) + .catch(error => ResponseError(error, res)); }); return router; From 27c8ed4d2bd7a1b7acc492967c70783403ebfac0 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 19 May 2016 11:47:50 +0300 Subject: [PATCH 10/96] Updating the license summary to what it actually entails --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 43038d1..5f34b87 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,21 @@ Developers should not go through the __trauma__ involved with dealing with SOAP/ ### This project uses GPL3 LICENSE -You saw the __LICENSE__ but you were like __*TL;DR*__. Here's sort of WHY I have used this restricting license: +__*TL;DR*__ Here's what the license entails: ```markdown -1. If you disagree with it then don’t use my software. It’s as simple as that. - -2. I almost need people, organisations and companies to have to admit they use my software. - -3. I have no social incentive to "give" my software free to companies just after nothing else but profit. - Open source to open source, corporation to corporation. +1. You have to include the license and copyright notice with each and every distribution/code base. +2. You can be copied, modified and distrubuted by anyone. +3. You can used privately. +3. You can use this sofware for commercial purposes. +6. If you dare build your business solely from this code, you risk open-sourcing the whole code base. +4. Any modifications of this code base MUST be distributed with the same license, GPLv3. +5. If you modifiy it, you have to indicate changes made to the code. +7. This sofware is provided without warranty. +8. The software author or license can not be held liable for any damages inflicted by the software. +``` -4. To keep you honest. You now have to tell your bosses you’re using my gear. And it will scare - the shit out of them. Why? They risk opensourcing all of their codebase. +More information on about the [LICENSE can be found here](http://choosealicense.com/licenses/gpl-3.0/) -5. Some companies who use our software do not give back. The irony of the situation is that, - in order to improve my motivation to do open source, I have to charge for it. -``` *__PLEASE NOTE:__ All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with.* From 5764f4a4e73d016b231fbd6f0b2ce98e1888c9df Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 19 May 2016 11:55:53 +0300 Subject: [PATCH 11/96] Fixing grammatical and typo issues on README --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c9090f1..03d1646 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,15 @@ Developers should not go through the __trauma__ involved with dealing with SOAP/ __*TL;DR*__ Here's what the license entails: ```markdown -1. You have to include the license and copyright notice with each and every distribution/code base. -2. You can be copied, modified and distrubuted by anyone. -3. You can used privately. -3. You can use this sofware for commercial purposes. -6. If you dare build your business solely from this code, you risk open-sourcing the whole code base. -4. Any modifications of this code base MUST be distributed with the same license, GPLv3. -5. If you modifiy it, you have to indicate changes made to the code. -7. This sofware is provided without warranty. -8. The software author or license can not be held liable for any damages inflicted by the software. +1. Anyone can copy, modify and distrubute this software. +2. You have to include the license and copyright notice with each and every distribution. +3. You can use this software privately. +4. You can use this sofware for commercial purposes. +5. If you dare build your business solely from this code, you risk open-sourcing the whole code base. +6. If you modifiy it, you have to indicate changes made to the code. +7. Any modifications of this code base MUST be distributed with the same license, GPLv3. +8. This sofware is provided without warranty. +9. The software author or license can not be held liable for any damages inflicted by the software. ``` More information on about the [LICENSE can be found here](http://choosealicense.com/licenses/gpl-3.0/) From ea835a4977bf1458ec8f3dce9ad2be3afa114025 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 20 May 2016 10:23:05 +0300 Subject: [PATCH 12/96] Re-enabled logging --- index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index e20d129..a3e9c6f 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,7 @@ app.set('view engine', 'jade'); // Uncomment this for Morgan to intercept all Error instantiations // For now, they churned out via a JSON response -// app.use(morgan('dev')); +app.use(morgan('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false @@ -83,4 +83,5 @@ var server = app.listen(process.env.PORT || 3000, () => { }); //expose app -export {app as default}; +export { app as + default }; From 6659dc535a52d39029afdcf4b206fc2dd0aff3e5 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 20 May 2016 10:51:04 +0300 Subject: [PATCH 13/96] Fixed #11 encypted password should be generated in a middleware --- index.js | 12 ++++++++---- server/controllers/payment-confirm.js | 3 --- server/controllers/payment-request.js | 3 --- server/controllers/payment-status.js | 3 --- server/routes/index.js | 12 +++++++++--- server/utils/generatePassword.js | 11 +++++++++++ 6 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 server/utils/generatePassword.js diff --git a/index.js b/index.js index a3e9c6f..a227d7a 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import session from 'express-session'; import connectMongo from 'connect-mongo'; import models from './models'; import routes from './routes'; +import genTransactionPassword from './utils/generatePassword'; const app = express(); @@ -17,7 +18,7 @@ const apiVersion = 1; const config = configSetUp(process.env.NODE_ENV); const MongoStore = connectMongo(session); -// -- make the models available everywhere in the app -- +// make the models available everywhere in the app app.set('models', models); app.set('webTokenSecret', config.webTokenSecret); @@ -49,6 +50,10 @@ app.use(session({ }) })); +// on payment transaction requests, +// generate and password to req object +app.use(`/api/v${apiVersion}/payment*`, genTransactionPassword); + // get an instance of the router for api routes app.use(`/api/v${apiVersion}`, routes(express.Router())); @@ -82,6 +87,5 @@ var server = app.listen(process.env.PORT || 3000, () => { ' mode', server.address().port, app.get('env')); }); -//expose app -export { app as - default }; +// expose app +export { app as default }; diff --git a/server/controllers/payment-confirm.js b/server/controllers/payment-confirm.js index 9faeb5e..183db67 100644 --- a/server/controllers/payment-confirm.js +++ b/server/controllers/payment-confirm.js @@ -4,9 +4,6 @@ import EncryptPassword from './encrypt'; export default class ConfirmPayment { constructor(data) { - data.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" - data.encryptedPassword = new EncryptPassword(data.timeStamp).hashedPassword; - let transactionConfirmRequest = typeof data.transactionID !== undefined ? '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; diff --git a/server/controllers/payment-request.js b/server/controllers/payment-request.js index f5b1e4d..e487e93 100644 --- a/server/controllers/payment-request.js +++ b/server/controllers/payment-request.js @@ -4,9 +4,6 @@ import EncryptPassword from './encrypt'; export default class PaymentRequest { constructor(data) { - data.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" - data.encryptedPassword = new EncryptPassword(data.timeStamp).hashedPassword; - this.body = ` diff --git a/server/controllers/payment-status.js b/server/controllers/payment-status.js index fe05f97..3becc42 100644 --- a/server/controllers/payment-status.js +++ b/server/controllers/payment-status.js @@ -4,9 +4,6 @@ import EncryptPassword from './encrypt'; export default class PaymentStatus { constructor(data) { - data.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" - data.encryptedPassword = new EncryptPassword(data.timeStamp).hashedPassword; - let transactionStatusRequest = typeof data.transactionID !== undefined ? '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; diff --git a/server/routes/index.js b/server/routes/index.js index bd8bd46..61e1adb 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -20,7 +20,9 @@ export default (router) => { merchantTransactionID: uuid.v1(), // time-based amountInDoubleFloat: '10.00', clientPhoneNumber: '254723001575', - extraMerchantPayload: JSON.stringify(extraPayload) + extraMerchantPayload: JSON.stringify(extraPayload), + timeStamp: req.timeStamp, + encryptedPassword: req.encryptedPassword }; let payment = new PaymentRequest(paymentDetails); @@ -35,7 +37,9 @@ export default (router) => { router.get('/payment/confirm/:id', (req, res) => { let payment = new ConfirmPayment({ - transactionID: req.params.id // eg. '99d0b1c0237b70f3dc63f36232b9984c' + transactionID: req.params.id, // eg. '99d0b1c0237b70f3dc63f36232b9984c' + timeStamp: req.timeStamp, + encryptedPassword: req.encryptedPassword }); let parser = new ParseResponse('transactionconfirmresponse'); let confirm = new SOAPRequest(payment, parser); @@ -48,7 +52,9 @@ export default (router) => { router.get('/payment/status/:id', (req, res) => { let payment = new PaymentStatus({ - transactionID: req.params.id // eg. '99d0b1c0237b70f3dc63f36232b9984c' + transactionID: req.params.id, + timeStamp: req.timeStamp, + encryptedPassword: req.encryptedPassword, }); let parser = new ParseResponse('transactionstatusresponse'); let status = new SOAPRequest(payment, parser); diff --git a/server/utils/generatePassword.js b/server/utils/generatePassword.js new file mode 100644 index 0000000..64c6fbe --- /dev/null +++ b/server/utils/generatePassword.js @@ -0,0 +1,11 @@ +import moment from 'moment'; +import EncryptPassword from '../controllers/encrypt'; + +const genTransactionPassword = (req, res, next) => { + req.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" + req.encryptedPassword = new EncryptPassword(req.timeStamp).hashedPassword; + // console.log('encryptedPassword:', req.encryptedPassword); + next(); +} + +export default genTransactionPassword; From 375419c93d213a54b727e1d247bec62a2eeed93a Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 20 May 2016 15:55:28 +0300 Subject: [PATCH 14/96] Enabled the body parser to parse x-www-form-urlencoded data --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index a227d7a..20f7456 100644 --- a/index.js +++ b/index.js @@ -31,7 +31,7 @@ app.set('view engine', 'jade'); app.use(morgan('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ - extended: false + extended: true })); app.use(cookieParser()); // uncomment after placing your favicon in /public From ce07146d42e85f668d0d7b6d830459a9f337f69a Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 20 May 2016 15:55:54 +0300 Subject: [PATCH 15/96] Created the route to handle a success event from SAG from a completed transcation --- server/routes/index.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/server/routes/index.js b/server/routes/index.js index 61e1adb..a00b60b 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,4 +1,5 @@ import uuid from 'node-uuid'; +import request from 'request'; import ParseResponse from '../controllers/parse-response'; import ResponseError from '../controllers/response-error'; import PaymentRequest from '../controllers/payment-request'; @@ -65,5 +66,33 @@ export default (router) => { .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 = {}; + + for (const x of keys) { + let prop = x.toLowerCase().replace(/\-/g, ''); + response[prop] = req.body[x]; + } + + // make a request to the merchant's endpoint + request({ + 'method': 'POST', + 'uri': process.env.MERCHANT_ENDPOINT, + 'rejectUnauthorized': false, + 'body': JSON.stringify(response), + 'headers': { + 'content-type': 'application/json; charset=utf-8' + } + }, (error, response, body) => { + // merchant should respond with + // an 'ok' or 'success' + }); + + res.status(200).status('ok'); // or 'success' + }); + return router; } From cff07e8af8ad07769b47e2a73311b701f9f85381 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 20 May 2016 23:55:12 +0300 Subject: [PATCH 16/96] Fixed eslint issues --- server/controllers/payment-confirm.js | 4 ---- server/controllers/payment-request.js | 4 ---- server/controllers/payment-status.js | 4 ---- server/routes/index.js | 4 ++-- server/utils/generatePassword.js | 2 +- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/server/controllers/payment-confirm.js b/server/controllers/payment-confirm.js index 183db67..d326f08 100644 --- a/server/controllers/payment-confirm.js +++ b/server/controllers/payment-confirm.js @@ -1,7 +1,3 @@ -import moment from 'moment'; -import EncryptPassword from './encrypt'; - - export default class ConfirmPayment { constructor(data) { let transactionConfirmRequest = typeof data.transactionID !== undefined ? diff --git a/server/controllers/payment-request.js b/server/controllers/payment-request.js index e487e93..f06909f 100644 --- a/server/controllers/payment-request.js +++ b/server/controllers/payment-request.js @@ -1,7 +1,3 @@ -import moment from 'moment'; -import EncryptPassword from './encrypt'; - - export default class PaymentRequest { constructor(data) { this.body = ` diff --git a/server/controllers/payment-status.js b/server/controllers/payment-status.js index 3becc42..eb89148 100644 --- a/server/controllers/payment-status.js +++ b/server/controllers/payment-status.js @@ -1,7 +1,3 @@ -import moment from 'moment'; -import EncryptPassword from './encrypt'; - - export default class PaymentStatus { constructor(data) { let transactionStatusRequest = typeof data.transactionID !== undefined ? diff --git a/server/routes/index.js b/server/routes/index.js index a00b60b..ec45704 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -55,7 +55,7 @@ export default (router) => { let 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); @@ -95,4 +95,4 @@ export default (router) => { }); return router; -} +}; diff --git a/server/utils/generatePassword.js b/server/utils/generatePassword.js index 64c6fbe..f680495 100644 --- a/server/utils/generatePassword.js +++ b/server/utils/generatePassword.js @@ -6,6 +6,6 @@ const genTransactionPassword = (req, res, next) => { req.encryptedPassword = new EncryptPassword(req.timeStamp).hashedPassword; // console.log('encryptedPassword:', req.encryptedPassword); next(); -} +}; export default genTransactionPassword; From c771ad03499334d5ee0f77b1f85fad9e1ca0a2c6 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 00:03:29 +0300 Subject: [PATCH 17/96] Updated eslint to use AIRBNB standard/plugin --- .eslintrc | 87 +++++++++++++--------------------------------------- package.json | 5 ++- 2 files changed, 26 insertions(+), 66 deletions(-) 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/package.json b/package.json index c13d5ce..f536427 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,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" } From c85c70e65362b4eefb47f0ef71b1c816f6389b17 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 00:35:06 +0300 Subject: [PATCH 18/96] Used airbnb file naming convention to name the server scripts --- index.js | 2 +- server/config/{status-codes.js => statusCodes.js} | 0 .../{payment-confirm.js => ConfirmPayment.js} | 0 .../{parse-response.js => ParseResponse.js} | 2 +- .../{payment-request.js => PaymentRequest.js} | 0 .../{payment-status.js => PaymentStatus.js} | 0 server/controllers/{request.js => SOAPRequest.js} | 0 .../response-error.js => errors/ResponseError.js} | 0 server/routes/index.js | 10 +++++----- .../encrypt.js => utils/GenEncryptedPassword.js} | 3 ++- .../{generatePassword.js => genTransactionPassword.js} | 0 11 files changed, 9 insertions(+), 8 deletions(-) rename server/config/{status-codes.js => statusCodes.js} (100%) rename server/controllers/{payment-confirm.js => ConfirmPayment.js} (100%) rename server/controllers/{parse-response.js => ParseResponse.js} (97%) rename server/controllers/{payment-request.js => PaymentRequest.js} (100%) rename server/controllers/{payment-status.js => PaymentStatus.js} (100%) rename server/controllers/{request.js => SOAPRequest.js} (100%) rename server/{controllers/response-error.js => errors/ResponseError.js} (100%) rename server/{controllers/encrypt.js => utils/GenEncryptedPassword.js} (91%) rename server/utils/{generatePassword.js => genTransactionPassword.js} (100%) diff --git a/index.js b/index.js index 20f7456..2082c7c 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ import session from 'express-session'; import connectMongo from 'connect-mongo'; import models from './models'; import routes from './routes'; -import genTransactionPassword from './utils/generatePassword'; +import genTransactionPassword from './utils/genTransactionPassword'; const app = express(); diff --git a/server/config/status-codes.js b/server/config/statusCodes.js similarity index 100% rename from server/config/status-codes.js rename to server/config/statusCodes.js diff --git a/server/controllers/payment-confirm.js b/server/controllers/ConfirmPayment.js similarity index 100% rename from server/controllers/payment-confirm.js rename to server/controllers/ConfirmPayment.js diff --git a/server/controllers/parse-response.js b/server/controllers/ParseResponse.js similarity index 97% rename from server/controllers/parse-response.js rename to server/controllers/ParseResponse.js index 6b15c0d..af9f15e 100644 --- a/server/controllers/parse-response.js +++ b/server/controllers/ParseResponse.js @@ -1,6 +1,6 @@ import cheerio from 'cheerio'; import _ from 'lodash'; -import statusCodes from '../config/status-codes'; +import statusCodes from '../config/statusCodes'; export default class ParseResponse { diff --git a/server/controllers/payment-request.js b/server/controllers/PaymentRequest.js similarity index 100% rename from server/controllers/payment-request.js rename to server/controllers/PaymentRequest.js diff --git a/server/controllers/payment-status.js b/server/controllers/PaymentStatus.js similarity index 100% rename from server/controllers/payment-status.js rename to server/controllers/PaymentStatus.js diff --git a/server/controllers/request.js b/server/controllers/SOAPRequest.js similarity index 100% rename from server/controllers/request.js rename to server/controllers/SOAPRequest.js diff --git a/server/controllers/response-error.js b/server/errors/ResponseError.js similarity index 100% rename from server/controllers/response-error.js rename to server/errors/ResponseError.js diff --git a/server/routes/index.js b/server/routes/index.js index ec45704..817ae9d 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,10 +1,10 @@ import uuid from 'node-uuid'; import request from 'request'; -import ParseResponse from '../controllers/parse-response'; -import ResponseError from '../controllers/response-error'; -import PaymentRequest from '../controllers/payment-request'; -import ConfirmPayment from '../controllers/payment-confirm'; -import PaymentStatus from '../controllers/payment-status'; +import ResponseError from '../errors/ResponseError'; +import ParseResponse from '../controllers/ParseResponse'; +import PaymentRequest from '../controllers/PaymentRequest'; +import ConfirmPayment from '../controllers/ConfirmPayment'; +import PaymentStatus from '../controllers/PaymentStatus'; import SOAPRequest from '../controllers/request'; diff --git a/server/controllers/encrypt.js b/server/utils/GenEncryptedPassword.js similarity index 91% rename from server/controllers/encrypt.js rename to server/utils/GenEncryptedPassword.js index e00cb64..bb726fb 100644 --- a/server/controllers/encrypt.js +++ b/server/utils/GenEncryptedPassword.js @@ -1,6 +1,7 @@ import crypto from 'crypto'; -export default class EncryptedPassword { + +export default class GenEncryptedPassword { constructor(timeStamp) { let concatenatedString = [process.env.PAYBILL_NUMBER, process.env.PASSKEY, timeStamp].join(''); let hash = crypto.createHash('sha256'); diff --git a/server/utils/generatePassword.js b/server/utils/genTransactionPassword.js similarity index 100% rename from server/utils/generatePassword.js rename to server/utils/genTransactionPassword.js From ee42249845d39451ae1123dbd7520f469d74d5b9 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 00:52:03 +0300 Subject: [PATCH 19/96] /payment/request now accepts params from req.body --- server/routes/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/routes/index.js b/server/routes/index.js index 817ae9d..03fb85a 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -15,13 +15,13 @@ export default (router) => { }); router.get('/payment/request', (req, res) => { - let extraPayload = { 'extra': 'info', 'as': 'object' }; let paymentDetails = { - referenceID: uuid.v4(), // product, service or order ID - merchantTransactionID: uuid.v1(), // time-based - amountInDoubleFloat: '10.00', - clientPhoneNumber: '254723001575', - extraMerchantPayload: JSON.stringify(extraPayload), + // transaction reference ID + referenceID: (req.body.referenceID || uuid.v4()), + // product, service or order ID + merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), + amountInDoubleFloat: (req.body.totalAmount || '10.00'), + clientPhoneNumber: (req.body.phoneNumber || '254723001575'), timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword }; From 98d8b4513a948b8d8438ce34d4b1bd854f16f62c Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 01:03:40 +0300 Subject: [PATCH 20/96] Fixed #13 Any other value in req.query object should be treated as extra payload data --- server/routes/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/server/routes/index.js b/server/routes/index.js index 03fb85a..66f34bc 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -15,6 +15,24 @@ export default (router) => { }); router.get('/payment/request', (req, res) => { + const requiredBodyParams = [ + 'referenceID', + 'merchantTransactionID', + 'totalAmount', + 'phoneNumber' + ]; + + const extraPayload = {}; + const bodyParamKeys = Object.keys(req.body); + + // anything that is not required should be added + // to the extraPayload object + for (const key of bodyParamKeys) { + if (!requiredBodyParams.includes(key)) { + extraPayload[key] = req.body[key]; + } + } + let paymentDetails = { // transaction reference ID referenceID: (req.body.referenceID || uuid.v4()), @@ -22,6 +40,7 @@ export default (router) => { merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), amountInDoubleFloat: (req.body.totalAmount || '10.00'), clientPhoneNumber: (req.body.phoneNumber || '254723001575'), + extraPayload: JSON.stringify(extraPayload), timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword }; From 57b5fd8de1940a08bd2551eebe129a5b448ee965 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 11:13:40 +0300 Subject: [PATCH 21/96] Added /thumbs/up route as a testing response for SAG post --- server/routes/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/server/routes/index.js b/server/routes/index.js index 66f34bc..e2bf3d1 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -90,6 +90,7 @@ export default (router) => { router.all('/payment/success', (req, res) => { const keys = Object.keys(req.body); let response = {}; + let localhost = `${req.hostname}:${process.env.PORT}/respond/ok`; for (const x of keys) { let prop = x.toLowerCase().replace(/\-/g, ''); @@ -99,18 +100,23 @@ export default (router) => { // make a request to the merchant's endpoint request({ 'method': 'POST', - 'uri': process.env.MERCHANT_ENDPOINT, + 'uri': (process.env.MERCHANT_ENDPOINT || localhost), 'rejectUnauthorized': false, 'body': JSON.stringify(response), 'headers': { 'content-type': 'application/json; charset=utf-8' } }, (error, response, body) => { - // merchant should respond with - // an 'ok' or 'success' + // merchant should respond with 'ok' + // status 200 + res.status(200).status(body); }); - res.status(200).status('ok'); // or 'success' + // for testing last POST response + // if MERCHANT_ENDPOINT has not been provided + router.post('/respond/ok', (req, res) => { + res.status(200).send('ok'); + }); }); return router; From 7f5b7d026bb3da2596ebc51126903f5dbfda3580 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 11:23:51 +0300 Subject: [PATCH 22/96] Moved the SOAP parser class to ./utils folder --- server/routes/index.js | 6 +++--- server/{controllers => utils}/ParseResponse.js | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename server/{controllers => utils}/ParseResponse.js (100%) diff --git a/server/routes/index.js b/server/routes/index.js index e2bf3d1..7db1bdd 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,7 +1,7 @@ import uuid from 'node-uuid'; import request from 'request'; import ResponseError from '../errors/ResponseError'; -import ParseResponse from '../controllers/ParseResponse'; +import ParseResponse from '../utils/ParseResponse'; import PaymentRequest from '../controllers/PaymentRequest'; import ConfirmPayment from '../controllers/ConfirmPayment'; import PaymentStatus from '../controllers/PaymentStatus'; @@ -90,7 +90,7 @@ export default (router) => { router.all('/payment/success', (req, res) => { const keys = Object.keys(req.body); let response = {}; - let localhost = `${req.hostname}:${process.env.PORT}/respond/ok`; + let localhost = `${req.hostname}:${process.env.PORT}/thumbs/up`; for (const x of keys) { let prop = x.toLowerCase().replace(/\-/g, ''); @@ -114,7 +114,7 @@ export default (router) => { // for testing last POST response // if MERCHANT_ENDPOINT has not been provided - router.post('/respond/ok', (req, res) => { + router.post('/thumbs/up', (req, res) => { res.status(200).send('ok'); }); }); diff --git a/server/controllers/ParseResponse.js b/server/utils/ParseResponse.js similarity index 100% rename from server/controllers/ParseResponse.js rename to server/utils/ParseResponse.js From 664807eb1219d365fabe50d423f57afc471cfef2 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 12:32:36 +0300 Subject: [PATCH 23/96] Completed and fixed #12 --- server/routes/index.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/server/routes/index.js b/server/routes/index.js index 7db1bdd..8f59754 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -90,34 +90,41 @@ export default (router) => { router.all('/payment/success', (req, res) => { const keys = Object.keys(req.body); let response = {}; - let localhost = `${req.hostname}:${process.env.PORT}/thumbs/up`; + 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) for (const x of keys) { let prop = x.toLowerCase().replace(/\-/g, ''); response[prop] = req.body[x]; } - // make a request to the merchant's endpoint - request({ + const requestParams = { 'method': 'POST', - 'uri': (process.env.MERCHANT_ENDPOINT || localhost), + 'uri': endpoint, 'rejectUnauthorized': false, 'body': JSON.stringify(response), 'headers': { 'content-type': 'application/json; charset=utf-8' } - }, (error, response, body) => { - // merchant should respond with 'ok' - // status 200 - res.status(200).status(body); - }); + }; - // for testing last POST response - // if MERCHANT_ENDPOINT has not been provided - router.post('/thumbs/up', (req, res) => { - res.status(200).send('ok'); + // make a request to the merchant's endpoint + request(requestParams, (error, response, body) => { + if (error) { + res.sendStatus(500); + return; + } + res.sendStatus(200); }); }); + // for testing last POST response + // if MERCHANT_ENDPOINT has not been provided + router.all('/thumbs/up', (req, res) => { + return res.sendStatus(200); + }); + return router; }; From b93c1181e39143db0ce0f72d4152fea7b8ef5291 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 12:57:30 +0300 Subject: [PATCH 24/96] Fixed referencing of the GenEncryptedPassword file and class --- server/routes/index.js | 4 ++-- server/utils/genTransactionPassword.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/routes/index.js b/server/routes/index.js index 8f59754..7e549f7 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -38,8 +38,8 @@ export default (router) => { referenceID: (req.body.referenceID || uuid.v4()), // product, service or order ID merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), - amountInDoubleFloat: (req.body.totalAmount || '10.00'), - clientPhoneNumber: (req.body.phoneNumber || '254723001575'), + amountInDoubleFloat: (req.body.totalAmount || process.env.TEST_AMOUNT), + clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), extraPayload: JSON.stringify(extraPayload), timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword diff --git a/server/utils/genTransactionPassword.js b/server/utils/genTransactionPassword.js index f680495..72007be 100644 --- a/server/utils/genTransactionPassword.js +++ b/server/utils/genTransactionPassword.js @@ -1,9 +1,9 @@ import moment from 'moment'; -import EncryptPassword from '../controllers/encrypt'; +import GenEncryptedPassword from './GenEncryptedPassword'; const genTransactionPassword = (req, res, next) => { req.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" - req.encryptedPassword = new EncryptPassword(req.timeStamp).hashedPassword; + req.encryptedPassword = new GenEncryptedPassword(req.timeStamp).hashedPassword; // console.log('encryptedPassword:', req.encryptedPassword); next(); }; From dfb9eb0dc0554132afc84636c38c138c1cf2fcdc Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 13:19:16 +0300 Subject: [PATCH 25/96] Added missing return code: 43 --- server/config/statusCodes.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index 5e7d813..d544aad 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -58,6 +58,10 @@ export default [{ return_code: 35, http_code: 409, message: 'A duplicate request has been detected' +}, { + return_code: 43, + http_code: 409, + message: "Duplicate merchant transaction ID detected", }, { return_code: 12, http_code: 409, From 918b18f8915898aff792e373aa67fb6098aa4f29 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 13:21:10 +0300 Subject: [PATCH 26/96] Added payment request details to JSON response --- server/routes/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/routes/index.js b/server/routes/index.js index 7e549f7..1adf10f 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -49,9 +49,14 @@ export default (router) => { let parser = new ParseResponse('processcheckoutresponse'); let request = new SOAPRequest(payment, parser); + // remove encryptedPassword and extraPayload + // should not be added to response object + delete paymentDetails.encryptedPassword; + delete paymentDetails.extraPayload; + // make the payment requets and process response request.post() - .then(response => res.json(response)) + .then(response => res.json(Object.assign({}, response, paymentDetails))) .catch(error => ResponseError(error, res)); }); From ab0264cc0d2d8f103e470c0339bf4db027b17448 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 13:22:30 +0300 Subject: [PATCH 27/96] Replaced include method with indexOf while checking for extraParams --- server/routes/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/index.js b/server/routes/index.js index 1adf10f..be66cb4 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -28,7 +28,7 @@ export default (router) => { // anything that is not required should be added // to the extraPayload object for (const key of bodyParamKeys) { - if (!requiredBodyParams.includes(key)) { + if (requiredBodyParams.indexOf(key) == -1) { extraPayload[key] = req.body[key]; } } From 5609ecb605604b33d8e784f523c0226ffc798462 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 13:23:37 +0300 Subject: [PATCH 28/96] Made /payment/request a POST request --- server/routes/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/index.js b/server/routes/index.js index be66cb4..481a44e 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -14,7 +14,7 @@ export default (router) => { return res.json({ 'status': 200 }); }); - router.get('/payment/request', (req, res) => { + router.post('/payment/request', (req, res) => { const requiredBodyParams = [ 'referenceID', 'merchantTransactionID', From 6d8c9f724ace796dbdbc25020c0a9ae9e986b1c7 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 15:23:15 +0300 Subject: [PATCH 29/96] Finished #20 Add sample request and response to README --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 03d1646..618b35b 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,59 @@ __MPESA API RESTful mediator__. Basically converts all merchant requests to the requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. -In short, it'll deal with all of the SOAP shenanigans while you REST. +In short, it'll deal with all of the SOAP shenanigans while you REST. 😄 -The aim of __Project Mulla__, is to create a REST API that interfaces with the ugly MPESA G2 API. +The aim of __Project Mulla__, is to create a REST API that interfaces with the __ugly MPESA G2 API.__ -Sounds like a broken record, but it was to emphasize what we hope to achieve :) - -### Since We Know, SOAP! Yuck! +### Yes We Know! SOAP! Yuck! Developers should not go through the __trauma__ involved with dealing with SOAP/XML in the 21st century. ---- +# Example of how it works + +Once __Project Mulla__ is set up, up and running in whichever clould platform you prefer(we recommend `Heroku.com`). Your 1st request once your customer/client has consumed your services or purchasing products from you is to innitiate a payment request. + +##### Initiate Payment Request: + +_Method_: __`POST`__ + +_Endpoint_: __`https://awesome-service.com/api/v1/payment/request`__ + +_Parameters_: +- __`phoneNumber`__ - The phone number of your client +- __`totalAmount`__ - The total amount you are charging the client +- __`referenceID`__ - The reference ID of the order or service __[optional; one is provided for you if missing]__ +- __`merchantTransactionID`__ - This specific order's or service's transaction ID __[optional; one is provided for you if missing]__ + +_Response:_ + +```http +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 510 +Content-Type: application/json; charset=utf-8 +Date: Sat, 21 May 2016 10:03:37 GMT +ETag: W/"1fe-jy66YehfhiFHWoyTNHpSnA" +X-Powered-By: Express +set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly + +{ + "amountInDoubleFloat": "10.00", + "clientPhoneNumber": "0723001575", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "description": "success", + "extraPayload": "{}", + "http_code": 200, + "merchantTransactionID": "4938a780-1f3b-11e6-acc6-5dabc98661b9", + "message": "Transaction carried successfully", + "referenceID": "f765b1ef-6890-44f2-bc7a-9be23013da1c", + "return_code": "00", + "timeStamp": "20160521130337", + "trx_id": "6c1b1dcc796ed6c1d5ea6d03d34ddb7f" +} +``` -### This project uses GPL3 LICENSE +# This project uses GPL3 LICENSE __*TL;DR*__ Here's what the license entails: From f539cc88bc57f9dcff687e5d9abfd38ed9e377c9 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 21 May 2016 15:23:15 +0300 Subject: [PATCH 30/96] Finished #20 Add sample request and response to README --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 03d1646..45bafb2 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,59 @@ __MPESA API RESTful mediator__. Basically converts all merchant requests to the requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. -In short, it'll deal with all of the SOAP shenanigans while you REST. +In short, it'll deal with all of the SOAP shenanigans while you REST. 😄 -The aim of __Project Mulla__, is to create a REST API that interfaces with the ugly MPESA G2 API. +The aim of __Project Mulla__, is to create a REST API that interfaces with the __ugly MPESA G2 API.__ -Sounds like a broken record, but it was to emphasize what we hope to achieve :) - -### Since We Know, SOAP! Yuck! +### Yes We Know! SOAP! Yuck! Developers should not go through the __trauma__ involved with dealing with SOAP/XML in the 21st century. ---- +# Example of how it works + +Once __Project Mulla__ is set up, up and running in whichever clould platform you prefer(we recommend `Heroku.com`). Your 1st request once your customer/client has consumed your services or purchasing products from you is to innitiate a payment request. + +##### Initiate Payment Request: + +_Method_: __`POST`__ + +_Endpoint_: __`https://awesome-service.com/api/v1/payment/request`__ + +_Parameters_: +- __`phoneNumber`__ - The phone number of your client +- __`totalAmount`__ - The total amount you are charging the client +- __`referenceID`__ - The reference ID of the order or service __[optional; one is provided for you if missing]__ +- __`merchantTransactionID`__ - This specific order's or service's transaction ID __[optional; one is provided for you if missing]__ + +_Response:_ + +```http +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 510 +Content-Type: application/json; charset=utf-8 +Date: Sat, 21 May 2016 10:03:37 GMT +ETag: W/"1fe-jy66YehfhiFHWoyTNHpSnA" +X-Powered-By: Express +set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly + +{ + "amountInDoubleFloat": "10.00", + "clientPhoneNumber": "0723001575", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "description": "success", + "extraPayload": "{}", + "http_code": 200, + "merchantTransactionID": "4938a780-1f3b-11e6-acc6-5dabc98661b9", + "message": "Transaction carried successfully", + "referenceID": "f765b1ef-6890-44f2-bc7a-9be23013da1c", + "return_code": "00", + "timeStamp": "20160521130337", + "trx_id": "6c1b1dcc796ed6c1d5ea6d03d34ddb7f" +} +``` -### This project uses GPL3 LICENSE +# This project uses GPL3 LICENSE __*TL;DR*__ Here's what the license entails: From 5cca8a9ab81536bc9f2026dff20eefab217c1ead Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 00:05:37 +0300 Subject: [PATCH 31/96] Finshed #19 Wrap successful responses in response object --- server/routes/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/routes/index.js b/server/routes/index.js index 481a44e..6c79e5f 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -56,7 +56,9 @@ export default (router) => { // make the payment requets and process response request.post() - .then(response => res.json(Object.assign({}, response, paymentDetails))) + .then(response => res.json({ + response: Object.assign({}, response, paymentDetails) + })) .catch(error => ResponseError(error, res)); }); @@ -71,7 +73,7 @@ export default (router) => { // process ConfirmPayment response confirm.post() - .then(response => res.json(response)) + .then(response => res.json({ response: response })) .catch(error => ResponseError(error, res)); }); @@ -86,7 +88,7 @@ export default (router) => { // process PaymentStatus response status.post() - .then(response => res.json(response)) + .then(response => res.json({ response: response })) .catch(error => ResponseError(error, res)); }); From 504d5d653fa3ee2bb2c7df2379226b386899c548 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 00:10:10 +0300 Subject: [PATCH 32/96] Updated response JSON example in README --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fdbc1a9..e0abe43 100644 --- a/README.md +++ b/README.md @@ -43,18 +43,19 @@ X-Powered-By: Express set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly { - "amountInDoubleFloat": "10.00", - "clientPhoneNumber": "0723001575", - "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", - "description": "success", - "extraPayload": "{}", - "http_code": 200, - "merchantTransactionID": "4938a780-1f3b-11e6-acc6-5dabc98661b9", - "message": "Transaction carried successfully", - "referenceID": "f765b1ef-6890-44f2-bc7a-9be23013da1c", - "return_code": "00", - "timeStamp": "20160521130337", - "trx_id": "6c1b1dcc796ed6c1d5ea6d03d34ddb7f" + "response": { + "return_code": "00", + "http_code": 200, + "message": "Transaction carried successfully", + "description": "success", + "trx_id": "b3f28c05ae72ff3cb23fb70b2b33ad4d", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "referenceID": "f765b1ef-6890-44f2-bc7a-9be23013da1c", + "timeStamp": "20160522000459", + "clientPhoneNumber": "254723001575", + "merchantTransactionID": "4938a780-1f3b-11e6-acc6-5dabc98661b9", + "amountInDoubleFloat": "450.00" + } } ``` From 3f408c0458c0ad9ebba07ccc77faa2fcc98bf6de Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 09:10:35 +0300 Subject: [PATCH 33/96] Updated /payment/request paymentDetails to use underscore notation on response --- server/controllers/PaymentRequest.js | 2 +- server/routes/index.js | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index f06909f..ad41241 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -14,7 +14,7 @@ export default class PaymentRequest { ${data.referenceID} ${data.amountInDoubleFloat} ${data.clientPhoneNumber} - ${(data.extraMerchantPayload || '')} + ${data.extraMerchantPayload ? JSON.stringify(data.extraMerchantPayload) : ''} ${process.env.CALLBACK_URL} ${process.env.CALLBACK_METHOD} ${data.timeStamp} diff --git a/server/routes/index.js b/server/routes/index.js index 6c79e5f..034de39 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -40,7 +40,7 @@ export default (router) => { merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), amountInDoubleFloat: (req.body.totalAmount || process.env.TEST_AMOUNT), clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), - extraPayload: JSON.stringify(extraPayload), + extraPayload: extraPayload, timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword }; @@ -49,10 +49,17 @@ export default (router) => { let parser = new ParseResponse('processcheckoutresponse'); let request = new SOAPRequest(payment, parser); - // remove encryptedPassword and extraPayload + // remove encryptedPassword // should not be added to response object delete paymentDetails.encryptedPassword; - delete paymentDetails.extraPayload; + + // 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()); + paymentDetails[newkey] = paymentDetails[key]; + delete paymentDetails[key]; + } // make the payment requets and process response request.post() From 38821091f91d9f7bac5d4bd3d53915cb0ca7929b Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 09:11:22 +0300 Subject: [PATCH 34/96] Updated express and some of dependencies --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c13d5ce..40e4bbb 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "dependencies": { "body-parser": "~1.13.2", "cheerio": "^0.20.0", - "connect-mongo": "^1.1.0", + "connect-mongo": "^1.2.0", "cookie-parser": "~1.3.5", "debug": "~2.2.0", "dotenv": "^2.0.0", - "express": "~4.13.1", + "express": "^4.13.4", "express-session": "^1.13.0", "jade": "~1.11.0", "lodash": "^4.12.0", @@ -46,5 +46,8 @@ "eslint": "^2.8.0", "eslint-plugin-react": "^5.1.1", "nodemon": "^1.9.2" + }, + "engines": { + "node": ">=4.2.0" } } From d2359abc6678c630203b18bb5794a3c8e9d0938d Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 13:21:30 +0300 Subject: [PATCH 35/96] Finished #10 Refactor ES6 import and any ES2015 feature not supported by Argon (Node.js v4.4.4.) --- environment.js | 3 +- index.js | 40 +++++++++++++------------- package.json | 4 +-- server/config/database.js | 20 +++++++------ server/config/index.js | 4 ++- server/config/statusCodes.js | 4 ++- server/controllers/ConfirmPayment.js | 4 ++- server/controllers/PaymentRequest.js | 4 ++- server/controllers/PaymentStatus.js | 3 +- server/controllers/SOAPRequest.js | 5 ++-- server/errors/ResponseError.js | 4 ++- server/models/index.js | 23 ++++++++------- server/routes/index.js | 23 ++++++++------- server/utils/GenEncryptedPassword.js | 5 ++-- server/utils/ParseResponse.js | 9 +++--- server/utils/genTransactionPassword.js | 8 ++++-- server/utils/ucFirst.js | 15 ++++++---- 17 files changed, 102 insertions(+), 76 deletions(-) diff --git a/environment.js b/environment.js index 40b7d4e..96f2c73 100644 --- a/environment.js +++ b/environment.js @@ -1,4 +1,5 @@ -import dotenv from 'dotenv'; +'use strict'; +const dotenv = require('dotenv'); if ((process.env.NODE_ENV || 'development') === 'development') { // load the applications environment diff --git a/index.js b/index.js index 2082c7c..a24d9f9 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,21 @@ -import './environment'; -import express from 'express'; -import path from 'path'; -import configSetUp from './config'; -// import favicon from 'serve-favicon'; -import morgan from 'morgan'; -import cookieParser from 'cookie-parser'; -import bodyParser from 'body-parser'; -import session from 'express-session'; -import connectMongo from 'connect-mongo'; -import models from './models'; -import routes from './routes'; -import genTransactionPassword from './utils/genTransactionPassword'; +'use strict'; +require('./environment'); +let express = require('express'); +let app = express(); +let path = require('path'); +let config = require('./server/config')(process.env.NODE_ENV); +// let favicon = require('serve-favicon'); +let morgan = require('morgan'); +let cookieParser = require('cookie-parser'); +let bodyParser = require('body-parser'); +let session = require('express-session'); +let MongoStore = require('connect-mongo')(session); +let models = require('./server/models'); +let routes = require('./server/routes'); +let genTransactionPassword = require('./server/utils/genTransactionPassword'); +let apiVersion = 1; -const app = express(); -const apiVersion = 1; -const config = configSetUp(process.env.NODE_ENV); -const MongoStore = connectMongo(session); // make the models available everywhere in the app app.set('models', models); @@ -38,7 +37,7 @@ app.use(cookieParser()); // app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); // not using express less // app.use(require('less-middleware')(path.join(__dirname, 'server/public'))); -app.use(express.static(path.join(__dirname, './public'))); +app.use(express.static(path.join(__dirname, './server/public'))); app.use(session({ secret: config.expressSessionKey, maxAge: new Date(Date.now() + 3600000), @@ -46,7 +45,8 @@ app.use(session({ resave: true, saveUninitialized: true, store: new MongoStore({ - mongooseConnection: models.mongoose.connection + mongooseConnection: models.mongoose.connection, + collection: 'session' }) })); @@ -88,4 +88,4 @@ var server = app.listen(process.env.PORT || 3000, () => { }); // expose app -export { app as default }; +exports.default = app; diff --git a/package.json b/package.json index 40e4bbb..59e6d72 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "develop": "nodemon -w ./server --exec npm start", "lint": "eslint ./server", - "prestart": "./node_modules/.bin/babel ./server ./*.js --out-dir dist", - "start": "node dist/index.js" + "babel-compile": "./node_modules/.bin/babel ./server ./*.js --out-dir dist", + "start": "node index.js" }, "repository": { "type": "git", diff --git a/server/config/database.js b/server/config/database.js index efe1b33..85c030f 100644 --- a/server/config/database.js +++ b/server/config/database.js @@ -1,27 +1,29 @@ -import Mongoose from 'mongoose'; -Mongoose.connect(process.env.DATABASE); +'use strict'; + +const mongoose = require('mongoose'); +mongoose.connect(process.env.DATABASE); // When successfully connected -Mongoose.connection.on('connected', function() { +mongoose.connection.on('connected', () => { console.log('Mongoose has connected to the database specified.'); }); // If the connection throws an error -Mongoose.connection.on('error', function(err) { +mongoose.connection.on('error', err => { console.log('Mongoose default connection error: ' + err); }); // When the connection is disconnected -Mongoose.connection.on('disconnected', function() { +mongoose.connection.on('disconnected', () => { console.log('Mongoose default connection disconnected'); }); -// If the Node process ends, close the Mongoose connection -process.on('SIGINT', function() { - Mongoose.connection.close(function() { +// If the Node process ends, close the mongoose connection +process.on('SIGINT', () => { + mongoose.connection.close(() => { console.log('Mongoose disconnected on application exit'); process.exit(0); }); }); -export { Mongoose as default }; +module.exports = mongoose; diff --git a/server/config/index.js b/server/config/index.js index 5094d59..c90a448 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -1,4 +1,6 @@ -export default function(value) { +'use strict'; + +module.exports = (value) => { var envVariables = { host: process.env.HOST, database: process.env.DATABASE, diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index d544aad..b9c47a5 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -1,4 +1,6 @@ -export default [{ +'use strict'; + +module.exports = [{ return_code: 0, http_code: 200, message: 'Transaction carried successfully' diff --git a/server/controllers/ConfirmPayment.js b/server/controllers/ConfirmPayment.js index d326f08..1ee6b94 100644 --- a/server/controllers/ConfirmPayment.js +++ b/server/controllers/ConfirmPayment.js @@ -1,4 +1,6 @@ -export default class ConfirmPayment { +'use strict'; + +module.exports = class ConfirmPayment { constructor(data) { let transactionConfirmRequest = typeof data.transactionID !== undefined ? '' + data.transactionID + '' : diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index ad41241..8c7c2b0 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -1,4 +1,6 @@ -export default class PaymentRequest { +'use strict'; + +module.exports = class PaymentRequest { constructor(data) { this.body = ` diff --git a/server/controllers/PaymentStatus.js b/server/controllers/PaymentStatus.js index eb89148..7ed13b0 100644 --- a/server/controllers/PaymentStatus.js +++ b/server/controllers/PaymentStatus.js @@ -1,4 +1,5 @@ -export default class PaymentStatus { +'use strict'; +module.exports = class PaymentStatus { constructor(data) { let transactionStatusRequest = typeof data.transactionID !== undefined ? '' + data.transactionID + '' : diff --git a/server/controllers/SOAPRequest.js b/server/controllers/SOAPRequest.js index e280c31..e58b5be 100644 --- a/server/controllers/SOAPRequest.js +++ b/server/controllers/SOAPRequest.js @@ -1,7 +1,8 @@ -import request from 'request'; +'use strict'; +const request = require('request'); -export default class SOAPRequest { +module.exports = class SOAPRequest { constructor(payment, parser) { this.parser = parser; this.requestOptions = { diff --git a/server/errors/ResponseError.js b/server/errors/ResponseError.js index 4086a6c..5f2bc84 100644 --- a/server/errors/ResponseError.js +++ b/server/errors/ResponseError.js @@ -1,4 +1,6 @@ -export default function ResponseError(error, res) { +'use strict'; + +module.exports = function ResponseError(error, res) { let err = new Error('description' in error ? error.description : error); err.status = 'http_code' in error ? error.http_code : 500; return res.status(err.status).json({ response: error }); diff --git a/server/models/index.js b/server/models/index.js index c7f0a34..ab1cac1 100644 --- a/server/models/index.js +++ b/server/models/index.js @@ -1,21 +1,24 @@ -// instantiate the database connection -import mongoose from '../config/database'; -import path from 'path'; -import fs from 'fs'; -import ucFirst from '../utils/ucfirst'; +'use strict'; + +const mongoose = require('../config/database'); +const path = require('path'); +const fs = require('fs'); +const ucFirst = require('../utils/ucfirst'); +const Schema = mongoose.Schema; // load models -const Schema = mongoose.Schema; fs.readdirSync(__dirname) .filter(function(file) { - return (file.indexOf('.') !== 0) && (file !== path.basename(module.filename)); + return (file.indexOf('.') !== 0) && + (file !== path.basename(module.filename)); }) .forEach(function(file) { if (file.slice(-3) !== '.js') return; - var modelName = file.replace('.js', ''); - module.exports[ucFirst(modelName)] = require(path.join(__dirname, modelName))(mongoose, Schema); + let modelName = file.replace('.js', ''); + let modelPath = path.join(__dirname, modelName) + module.exports[ucFirst(modelName)] = require(modelPath)(mongoose, Schema); }); // export connection -export default { mongoose, Schema }; +module.exports = { mongoose, Schema }; diff --git a/server/routes/index.js b/server/routes/index.js index 034de39..3a15935 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,14 +1,15 @@ -import uuid from 'node-uuid'; -import request from 'request'; -import ResponseError from '../errors/ResponseError'; -import ParseResponse from '../utils/ParseResponse'; -import PaymentRequest from '../controllers/PaymentRequest'; -import ConfirmPayment from '../controllers/ConfirmPayment'; -import PaymentStatus from '../controllers/PaymentStatus'; -import SOAPRequest from '../controllers/request'; - - -export default (router) => { +'use strict'; +const uuid = require('node-uuid'); +const request = require('request'); +const ResponseError = require('../errors/ResponseError'); +const ParseResponse = require('../utils/ParseResponse'); +const PaymentRequest = require('../controllers/PaymentRequest'); +const ConfirmPayment = require('../controllers/ConfirmPayment'); +const PaymentStatus = require('../controllers/PaymentStatus'); +const SOAPRequest = require('../controllers/SOAPRequest'); + + +module.exports = (router) => { /* Check the status of the API system */ router.get('/', (req, res) => { return res.json({ 'status': 200 }); diff --git a/server/utils/GenEncryptedPassword.js b/server/utils/GenEncryptedPassword.js index bb726fb..1b9c271 100644 --- a/server/utils/GenEncryptedPassword.js +++ b/server/utils/GenEncryptedPassword.js @@ -1,7 +1,8 @@ -import crypto from 'crypto'; +'use strict'; +const crypto = require('crypto'); -export default class GenEncryptedPassword { +module.exports = class GenEncryptedPassword { constructor(timeStamp) { let concatenatedString = [process.env.PAYBILL_NUMBER, process.env.PASSKEY, timeStamp].join(''); let hash = crypto.createHash('sha256'); diff --git a/server/utils/ParseResponse.js b/server/utils/ParseResponse.js index af9f15e..92e86df 100644 --- a/server/utils/ParseResponse.js +++ b/server/utils/ParseResponse.js @@ -1,9 +1,10 @@ -import cheerio from 'cheerio'; -import _ from 'lodash'; -import statusCodes from '../config/statusCodes'; +'use strict'; +const cheerio = require('cheerio'); +const _ = require('lodash'); +const statusCodes = require('../config/statusCodes'); -export default class ParseResponse { +module.exports = class ParseResponse { constructor(bodyTagName) { this.bodyTagName = bodyTagName; } diff --git a/server/utils/genTransactionPassword.js b/server/utils/genTransactionPassword.js index 72007be..9b70265 100644 --- a/server/utils/genTransactionPassword.js +++ b/server/utils/genTransactionPassword.js @@ -1,5 +1,7 @@ -import moment from 'moment'; -import GenEncryptedPassword from './GenEncryptedPassword'; +'use strict'; +const moment = require('moment'); +const GenEncryptedPassword = require('./GenEncryptedPassword'); + const genTransactionPassword = (req, res, next) => { req.timeStamp = moment().format('YYYYMMDDHHmmss'); // In PHP => "YmdHis" @@ -8,4 +10,4 @@ const genTransactionPassword = (req, res, next) => { next(); }; -export default genTransactionPassword; +module.exports = genTransactionPassword; diff --git a/server/utils/ucFirst.js b/server/utils/ucFirst.js index 5d4665b..aad3380 100644 --- a/server/utils/ucFirst.js +++ b/server/utils/ucFirst.js @@ -1,11 +1,13 @@ -// ucFirst (typeof String): returns the String in question but changes the First Character to an Upper case -export default function(string) { - var word = string, +'use strict'; +// ucFirst (typeof String): +// returns String with first character uppercased +module.exports = (string) => { + let word = string, ucFirstWord = ''; - for (var x = 0, length = word.length; x < length; x++) { + for (let x = 0, length = word.length; x < length; x++) { // get the character's ASCII code - var 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); @@ -25,5 +27,6 @@ export default function(string) { ucFirstWord += character; } + return ucFirstWord; -} +}; From b8ec6f534b8ab84c44db9e342990bd9fafd5afe1 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 15:13:43 +0300 Subject: [PATCH 36/96] Finished #22 Add field/params validation to /payment/request route --- index.js | 2 +- server/routes/index.js | 25 +++------------- server/validators/requiredParams.js | 44 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 server/validators/requiredParams.js diff --git a/index.js b/index.js index a24d9f9..94e2200 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ let MongoStore = require('connect-mongo')(session); let models = require('./server/models'); let routes = require('./server/routes'); let genTransactionPassword = require('./server/utils/genTransactionPassword'); -let apiVersion = 1; +let apiVersion = process.env.API_VERSION; // make the models available everywhere in the app diff --git a/server/routes/index.js b/server/routes/index.js index 3a15935..55401db 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -3,6 +3,7 @@ const uuid = require('node-uuid'); const request = require('request'); const ResponseError = require('../errors/ResponseError'); const ParseResponse = require('../utils/ParseResponse'); +const requiredParams = require('../validators/requiredParams'); const PaymentRequest = require('../controllers/PaymentRequest'); const ConfirmPayment = require('../controllers/ConfirmPayment'); const PaymentStatus = require('../controllers/PaymentStatus'); @@ -15,25 +16,7 @@ module.exports = (router) => { return res.json({ 'status': 200 }); }); - router.post('/payment/request', (req, res) => { - const requiredBodyParams = [ - 'referenceID', - 'merchantTransactionID', - 'totalAmount', - 'phoneNumber' - ]; - - const extraPayload = {}; - const bodyParamKeys = Object.keys(req.body); - - // anything that is not required should be added - // to the extraPayload object - for (const key of bodyParamKeys) { - if (requiredBodyParams.indexOf(key) == -1) { - extraPayload[key] = req.body[key]; - } - } - + router.post('/payment/request', requiredParams, (req, res) => { let paymentDetails = { // transaction reference ID referenceID: (req.body.referenceID || uuid.v4()), @@ -41,7 +24,7 @@ module.exports = (router) => { merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), amountInDoubleFloat: (req.body.totalAmount || process.env.TEST_AMOUNT), clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), - extraPayload: extraPayload, + extraPayload: req.body.extraPayload, timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword }; @@ -126,7 +109,7 @@ module.exports = (router) => { }; // make a request to the merchant's endpoint - request(requestParams, (error, response, body) => { + request(requestParams, (error) => { if (error) { res.sendStatus(500); return; diff --git a/server/validators/requiredParams.js b/server/validators/requiredParams.js new file mode 100644 index 0000000..2e9230c --- /dev/null +++ b/server/validators/requiredParams.js @@ -0,0 +1,44 @@ +module.exports = (req, res, next) => { + const requiredBodyParams = [ + 'referenceID', + 'merchantTransactionID', + 'totalAmount', + 'phoneNumber' + ]; + + if ('phoneNumber' in req.body) { + // validate the phone number + if (!/\+?(254)[0-9]{9}/g.test(req.body.phoneNumber)) { + return res.status(400).send('Invalid [phoneNumber]'); + } + } else { + return res.status(400).send('No [phoneNumber] parameter was found'); + } + + // validate total amount + if ('totalAmount' in req.body) { + if (!/^[\d]+(\.[\d]{2})?$/g.test(req.body.totalAmount)) { + return res.status(400).send('Invalid [totalAmount]'); + } + + if (/^[\d]+$/g.test(req.body.totalAmount)) { + req.body.totalAmount = (parseInt(req.body.totalAmount)).toFixed(2) + } + } else { + return res.status(400).send('No [totalAmount] parameter was found'); + } + + const bodyParamKeys = Object.keys(req.body); + req.body.extraPayload = {}; + + // anything that is not a required param + // should be added to the extraPayload object + for (const key of bodyParamKeys) { + if (requiredBodyParams.indexOf(key) == -1) { + req.body.extraPayload[key] = req.body[key]; + delete req.body[key]; + } + } + + next(); +}; From a7c679d8b456d56f2ab9cd5fd165f4738374a458 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 16:26:48 +0300 Subject: [PATCH 37/96] Finished #17 Add installations steps to README --- README.md | 146 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e0abe43..5b98e05 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,34 @@ # Project Mulla -__What MPESA G2 API should have been in the 21st century.__ +**What MPESA G2 API should have been in the 21st century.** -__MPESA API RESTful mediator__. Basically converts all merchant requests to the dreaded ancient SOAP/XML +**MPESA API RESTful mediator**. Basically converts all merchant requests to the dreaded ancient SOAP/XML requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. In short, it'll deal with all of the SOAP shenanigans while you REST. 😄 -The aim of __Project Mulla__, is to create a REST API that interfaces with the __ugly MPESA G2 API.__ +The aim of **Project Mulla**, is to create a REST API that interfaces with the **ugly MPESA G2 API.** ### Yes We Know! SOAP! Yuck! -Developers should not go through the __trauma__ involved with dealing with SOAP/XML in the 21st century. +Developers should not go through the **trauma** involved with dealing with SOAP/XML in the 21st century. # Example of how it works -Once __Project Mulla__ is set up, up and running in whichever clould platform you prefer(we recommend `Heroku.com`). Your 1st request once your customer/client has consumed your services or purchasing products from you is to innitiate a payment request. +Once **Project Mulla** is set up, up and running in whichever clould platform you prefer(we recommend `Heroku.com`). Your 1st request once your customer/client has consumed your services or purchasing products from you is to innitiate a payment request. ##### Initiate Payment Request: -_Method_: __`POST`__ +_Method_: **`POST`** -_Endpoint_: __`https://awesome-service.com/api/v1/payment/request`__ +_Endpoint_: **`https://awesome-service.com/api/v1/payment/request`** _Parameters_: -- __`phoneNumber`__ - The phone number of your client -- __`totalAmount`__ - The total amount you are charging the client -- __`referenceID`__ - The reference ID of the order or service __[optional; one is provided for you if missing]__ -- __`merchantTransactionID`__ - This specific order's or service's transaction ID __[optional; one is provided for you if missing]__ +- **`phoneNumber`** - The phone number of your client +- **`totalAmount`** - The total amount you are charging the client +- **`referenceID`** - The reference ID of the order or service **[optional; one is generated for you if missing]** +- **`merchantTransactionID`** - This specific order's or service's transaction ID **[optional; one is generated for you if missing]** _Response:_ @@ -59,9 +59,129 @@ set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7 } ``` +# Installation + +## Dependencies + +You will need to install some stuff, if they are not yet in your machine: + +Majors: + +* **Node.js (v4.4.4 LTS)** - [Click here](http://nodejs.org) to install + +Secondaries(click for further information): + +* NPM (bundled with node.js installation package) + +You may need to update it to the latest version: + +``` +$ npm update -g npm +``` + +## Getting Started + +Once you have **Node.js** installed, run _(type or copy & paste; pick your poison)_: + +**To download the boilerplate** + +```bash +$ git clone https://github.com/kn9ts/project-mulla +``` + +After cloning, get into your project mulla's directory/folder: + +```bash +$ cd project-mulla +``` + +**Install all of the projects dependecies with:** + +```bash +$ npm install +``` + +**Create .env configurations file** + +The last but not least step is creating a `.env` file with your configurations in the root directory of `project mulla`. + +Should be in the same location as `index.js` + +It should look like the example below, only with your specific config values: + +```js +API_VERSION = 1 +HOST = localhost +PORT = 3000 +DATABASE = 'localhost:27017/project-mulla' +EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' +WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' +PAYBILL_NUMBER = '898998' +PASSKEY = 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' +ENDPOINT = 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' +CALLBACK_URL = 'http://awesome-service.com/mpesa/confirm-checkout.php' +CALLBACK_METHOD = 'POST' +TEST_PHONENUMBER = '0720000000' +TEST_AMOUNT = '10.00' +``` + +#### It's now ready to launch + +```bash +$ npm start + +> project-mulla@0.1.1 start ../project-mulla +> node index.js + +Express server listening on 3000, in development mode +Mongoose has connected to the database specified. +``` + +#### Test run + +If you have [httpie](https://github.com/jkbrzt/httpie) installed, too lazy to give a CURL example, you give it make test run by invoking: + +```bash +$ http POST localhost:3000/api/v1/payment/request \ +phoneNumber=254723001575 \ +totalAmount=450.00 \ +clientName='Eugene Mutai' \ +clientLocation='Kilimani' +``` + +And immediately get back a response as follows: + +```http +HTTP/1.1 200 OK +Connection: keep-alive +Content-Length: 534 +Content-Type: application/json; charset=utf-8 +Date: Sun, 22 May 2016 13:12:09 GMT +ETag: W/"216-NgmF2VWb0PIkUOKfya6WlA" +X-Powered-By: Express +set-cookie: connect.sid=s%3AiWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly + +{ + "response": { + "amount_in_double_float": "450.00", + "client_phone_number": "254723001575", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "description": "success", + "extra_payload": {}, + "http_code": 200, + "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", + "message": "Transaction carried successfully", + "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", + "return_code": "00", + "time_stamp": "20160522161208", + "trx_id": "45a3f4b64cde9d88440211187f73944b" + } +} +``` + # This project uses GPL3 LICENSE -__*TL;DR*__ Here's what the license entails: +**_TL;DR_*** Here's what the license entails: ```markdown 1. Anyone can copy, modify and distrubute this software. @@ -77,4 +197,4 @@ __*TL;DR*__ Here's what the license entails: More information on about the [LICENSE can be found here](http://choosealicense.com/licenses/gpl-3.0/) -*__PLEASE NOTE:__ All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with.* +**_PLEASE NOTE:_** All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with.* From 2572fa179f60b175ab454c33081c2825e06c5d07 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 17:57:21 +0300 Subject: [PATCH 38/96] Adding project banner --- docs/banner.png | Bin 0 -> 50517 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/banner.png diff --git a/docs/banner.png b/docs/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..862e1da1ada61b5f3acfa274162bf7c8d895ae5f GIT binary patch literal 50517 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 literal 0 HcmV?d00001 From f35c6fd34e205c1d86e76cbad828fe8625a0d2d2 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 18:04:04 +0300 Subject: [PATCH 39/96] Added banner image to readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b98e05..317050d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Project Mulla +![](https://raw.githubusercontent.com/kn9ts/project-mulla/develop/docs/banner.png) -**What MPESA G2 API should have been in the 21st century.** +> **What MPESA G2 API should have been in the 21st century.** **MPESA API RESTful mediator**. Basically converts all merchant requests to the dreaded ancient SOAP/XML requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. From 3d4250eaee277869ee59d170806a5b720e282334 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 18:27:51 +0300 Subject: [PATCH 40/96] Updated banner --- docs/banner.png | Bin 50517 -> 50516 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/banner.png b/docs/banner.png index 862e1da1ada61b5f3acfa274162bf7c8d895ae5f..1fdaea7b5254049214cc797c9e87d4883a542b80 100644 GIT binary patch delta 15 WcmccG#eAiUnWZzp&wV3nz)=7)L delta 16 Xcmcc8#eB7knYA;(&z*N8OTbY8HopcA From b88c48dad9ecd7f575c324be786c4b1fdf9f583a Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 19:22:58 +0300 Subject: [PATCH 41/96] Replaced image with one delivered from js.co.ke --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 317050d..e2f6363 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![](https://raw.githubusercontent.com/kn9ts/project-mulla/develop/docs/banner.png) +![](http://cdn.javascript.co.ke/images/banner.png) > **What MPESA G2 API should have been in the 21st century.** From 9c30f5ed2253291b67870e1b1a08e4f645fad551 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 22 May 2016 19:39:32 +0300 Subject: [PATCH 42/96] Renamed http_code to status_code --- README.md | 4 +-- server/config/statusCodes.js | 44 +++++++++++++++---------------- server/controllers/SOAPRequest.js | 2 +- server/errors/ResponseError.js | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e2f6363..711c83b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7 { "response": { "return_code": "00", - "http_code": 200, + "status_code": 200, "message": "Transaction carried successfully", "description": "success", "trx_id": "b3f28c05ae72ff3cb23fb70b2b33ad4d", @@ -168,7 +168,7 @@ set-cookie: connect.sid=s%3AiWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", "description": "success", "extra_payload": {}, - "http_code": 200, + "status_code": 200, "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", "message": "Transaction carried successfully", "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index b9c47a5..c582d9a 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -2,90 +2,90 @@ module.exports = [{ return_code: 0, - http_code: 200, + status_code: 200, message: 'Transaction carried successfully' }, { return_code: 9, - http_code: 400, + status_code: 400, message: 'The merchant ID provided does not exist in our systems' }, { return_code: 10, - http_code: 400, + status_code: 400, message: 'The phone number(MSISDN) provided isn’t registered on M-PESA' }, { return_code: 30, - http_code: 400, + status_code: 400, message: 'Missing reference ID' }, { return_code: 31, - http_code: 400, + status_code: 400, message: 'The request amount is invalid or blank' }, { return_code: 36, - http_code: 400, + status_code: 400, message: 'Incorrect credentials are provided in the request' }, { return_code: 40, - http_code: 400, + status_code: 400, message: 'Missing required parameters' }, { return_code: 41, - http_code: 400, + status_code: 400, message: 'MSISDN(phone number) is in incorrect format' }, { return_code: 32, - http_code: 401, + status_code: 401, message: 'The merchant/paybill account in the request hasn’t been activated' }, { return_code: 33, - http_code: 401, + status_code: 401, message: 'The merchant/paybill account hasn’t been approved to transact' }, { return_code: 1, - http_code: 402, + status_code: 402, message: 'Client has insufficient funds to complete the transaction' }, { return_code: 3, - http_code: 402, + status_code: 402, message: 'The amount to be transacted is less than the minimum single transfer amount allowed' }, { return_code: 4, - http_code: 402, + status_code: 402, message: 'The amount to be transacted is more than the maximum single transfer amount allowed' }, { return_code: 8, - http_code: 402, + status_code: 402, message: 'The client has reached his/her maximum transaction limit for the day' }, { return_code: 35, - http_code: 409, + status_code: 409, message: 'A duplicate request has been detected' }, { return_code: 43, - http_code: 409, + status_code: 409, message: "Duplicate merchant transaction ID detected", }, { return_code: 12, - http_code: 409, + status_code: 409, message: 'The transaction details are different from original captured request details' }, { return_code: 6, - http_code: 503, + status_code: 503, message: 'Transaction could not be confirmed possibly due to the operation failing' }, { return_code: 11, - http_code: 503, + status_code: 503, message: 'The system is unable to complete the transaction' }, { return_code: 34, - http_code: 503, + status_code: 503, message: 'A delay is being experienced while processing requests' }, { return_code: 29, - http_code: 503, + status_code: 503, message: 'The system is inaccessible; The system may be down' }, { return_code: 5, - http_code: 504, + status_code: 504, message: 'Duration provided to complete the transaction has expired' }]; diff --git a/server/controllers/SOAPRequest.js b/server/controllers/SOAPRequest.js index e58b5be..8c60a33 100644 --- a/server/controllers/SOAPRequest.js +++ b/server/controllers/SOAPRequest.js @@ -30,7 +30,7 @@ module.exports = class SOAPRequest { // Anything that is not "00" as the // SOAP response code is a Failure - if (json.http_code !== 200) { + if (json.status_code !== 200) { reject(json); return; } diff --git a/server/errors/ResponseError.js b/server/errors/ResponseError.js index 5f2bc84..3ca65f2 100644 --- a/server/errors/ResponseError.js +++ b/server/errors/ResponseError.js @@ -2,6 +2,6 @@ module.exports = function ResponseError(error, res) { let err = new Error('description' in error ? error.description : error); - err.status = 'http_code' in error ? error.http_code : 500; + err.status = 'status_code' in error ? error.status_code : 500; return res.status(err.status).json({ response: error }); } From 8ab74abae5fd03f5639065a3af1e4feffe493c47 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 23 May 2016 14:15:44 +0300 Subject: [PATCH 43/96] Added missing mongodb installation steps --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 711c83b..fc3b9c1 100644 --- a/README.md +++ b/README.md @@ -65,13 +65,21 @@ set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7 You will need to install some stuff, if they are not yet in your machine: -Majors: +##### Majors: * **Node.js (v4.4.4 LTS)** - [Click here](http://nodejs.org) to install +* **MongoDB (v3+)** - [Click here](https://docs.mongodb.com/manual/installation/) to install on your specific OS distro -Secondaries(click for further information): +Alternatively to the **MongoDB** installation you can create an account on [mLab.com](https://mlab.com/) +(previously mongolab.com) create a DB once logged in and copy the URL to your `.env` _DATABASE_ config variable. -* NPM (bundled with node.js installation package) +```js +DATABASE = "mongodb://8cp52rbucbhdnd@ds033285.mongolab.com:33285" +``` + +##### Secondaries(click for further information): + +* **NPM (v3.5+; bundled with node.js installation package)** You may need to update it to the latest version: @@ -113,7 +121,7 @@ It should look like the example below, only with your specific config values: API_VERSION = 1 HOST = localhost PORT = 3000 -DATABASE = 'localhost:27017/project-mulla' +DATABASE = 'localhost:27017/project-mulla' // Your mongodb database path EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' PAYBILL_NUMBER = '898998' From ee0365ea45b5efc803b791479b7b2c98252e4cac Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 23 May 2016 14:23:48 +0300 Subject: [PATCH 44/96] Added CURL example to README --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fc3b9c1..488de8a 100644 --- a/README.md +++ b/README.md @@ -145,19 +145,30 @@ Express server listening on 3000, in development mode Mongoose has connected to the database specified. ``` -#### Test run +#### Do a test run -If you have [httpie](https://github.com/jkbrzt/httpie) installed, too lazy to give a CURL example, you give it make test run by invoking: +You can make a test run using **CURL**: + +```bash +$ curl -i -X POST \ + --url http://localhost:3000/api/v1/payment/request \ + --data 'phoneNumber=254723001575' \ + --data 'totalAmount=450.00' \ + --data 'clientName="Eugene Mutai"' \ + --data 'clientLocation=Kilimani' \ +``` + +Or if you have [httpie](https://github.com/jkbrzt/httpie) installed: ```bash $ http POST localhost:3000/api/v1/payment/request \ -phoneNumber=254723001575 \ -totalAmount=450.00 \ -clientName='Eugene Mutai' \ -clientLocation='Kilimani' + phoneNumber=254723001575 \ + totalAmount=450.00 \ + clientName='Eugene Mutai' \ + clientLocation='Kilimani' ``` -And immediately get back a response as follows: +You should expect back a similar structured **response** as follows: ```http HTTP/1.1 200 OK From a7f82ec1afcd850b99e9d92d0801ec906e4242df Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 24 May 2016 01:30:03 +0300 Subject: [PATCH 45/96] Removed an dependencies and usage of mongodb in the app, not really required at the moment --- index.js | 16 +++------------- package.json | 2 -- server/config/database.js | 29 ----------------------------- server/config/index.js | 21 ++++++++++----------- server/models/index.js | 24 ------------------------ 5 files changed, 13 insertions(+), 79 deletions(-) delete mode 100644 server/config/database.js delete mode 100644 server/models/index.js diff --git a/index.js b/index.js index 94e2200..5d8ea5c 100644 --- a/index.js +++ b/index.js @@ -10,17 +10,11 @@ let morgan = require('morgan'); let cookieParser = require('cookie-parser'); let bodyParser = require('body-parser'); let session = require('express-session'); -let MongoStore = require('connect-mongo')(session); -let models = require('./server/models'); let routes = require('./server/routes'); let genTransactionPassword = require('./server/utils/genTransactionPassword'); let apiVersion = process.env.API_VERSION; -// make the models available everywhere in the app -app.set('models', models); -app.set('webTokenSecret', config.webTokenSecret); - // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); @@ -38,16 +32,12 @@ app.use(cookieParser()); // not using express less // app.use(require('less-middleware')(path.join(__dirname, 'server/public'))); app.use(express.static(path.join(__dirname, './server/public'))); + +// memory based session app.use(session({ secret: config.expressSessionKey, - maxAge: new Date(Date.now() + 3600000), - proxy: true, - resave: true, + resave: false, saveUninitialized: true, - store: new MongoStore({ - mongooseConnection: models.mongoose.connection, - collection: 'session' - }) })); // on payment transaction requests, diff --git a/package.json b/package.json index 59e6d72..631c9f9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "dependencies": { "body-parser": "~1.13.2", "cheerio": "^0.20.0", - "connect-mongo": "^1.2.0", "cookie-parser": "~1.3.5", "debug": "~2.2.0", "dotenv": "^2.0.0", @@ -32,7 +31,6 @@ "jade": "~1.11.0", "lodash": "^4.12.0", "moment": "^2.13.0", - "mongoose": "^4.4.16", "morgan": "~1.6.1", "node-uuid": "^1.4.7", "request": "^2.72.0", diff --git a/server/config/database.js b/server/config/database.js deleted file mode 100644 index 85c030f..0000000 --- a/server/config/database.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const mongoose = require('mongoose'); -mongoose.connect(process.env.DATABASE); - -// When successfully connected -mongoose.connection.on('connected', () => { - console.log('Mongoose has connected to the database specified.'); -}); - -// If the connection throws an error -mongoose.connection.on('error', err => { - console.log('Mongoose default connection error: ' + err); -}); - -// When the connection is disconnected -mongoose.connection.on('disconnected', () => { - console.log('Mongoose default connection disconnected'); -}); - -// If the Node process ends, close the mongoose connection -process.on('SIGINT', () => { - mongoose.connection.close(() => { - console.log('Mongoose disconnected on application exit'); - process.exit(0); - }); -}); - -module.exports = mongoose; diff --git a/server/config/index.js b/server/config/index.js index c90a448..291d930 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -1,16 +1,15 @@ 'use strict'; module.exports = (value) => { - var envVariables = { - host: process.env.HOST, - database: process.env.DATABASE, - expressSessionKey: process.env.EXPRESS_SESSION_KEY, - webTokenSecret: process.env.WEB_TOKEN_SECRET - }, - environments = { - development: envVariables, - staging: envVariables, - production: envVariables - }; + const envVariables = { + host: process.env.HOST, + expressSessionKey: process.env.EXPRESS_SESSION_KEY, + }; + + const environments = { + development: envVariables, + staging: envVariables, + production: envVariables + }; return environments[value] ? environments[value] : environments.development; } diff --git a/server/models/index.js b/server/models/index.js deleted file mode 100644 index ab1cac1..0000000 --- a/server/models/index.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -const mongoose = require('../config/database'); -const path = require('path'); -const fs = require('fs'); -const ucFirst = require('../utils/ucfirst'); -const Schema = mongoose.Schema; - - -// load models -fs.readdirSync(__dirname) - .filter(function(file) { - return (file.indexOf('.') !== 0) && - (file !== path.basename(module.filename)); - }) - .forEach(function(file) { - if (file.slice(-3) !== '.js') return; - let modelName = file.replace('.js', ''); - let modelPath = path.join(__dirname, modelName) - module.exports[ucFirst(modelName)] = require(modelPath)(mongoose, Schema); - }); - -// export connection -module.exports = { mongoose, Schema }; From a42d46fe2b0635f9b1c267a5eae6511632e0c011 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 24 May 2016 01:32:17 +0300 Subject: [PATCH 46/96] Updated README --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 488de8a..ec1021e 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,6 @@ You will need to install some stuff, if they are not yet in your machine: ##### Majors: * **Node.js (v4.4.4 LTS)** - [Click here](http://nodejs.org) to install -* **MongoDB (v3+)** - [Click here](https://docs.mongodb.com/manual/installation/) to install on your specific OS distro - -Alternatively to the **MongoDB** installation you can create an account on [mLab.com](https://mlab.com/) -(previously mongolab.com) create a DB once logged in and copy the URL to your `.env` _DATABASE_ config variable. - -```js -DATABASE = "mongodb://8cp52rbucbhdnd@ds033285.mongolab.com:33285" -``` ##### Secondaries(click for further information): @@ -121,7 +113,6 @@ It should look like the example below, only with your specific config values: API_VERSION = 1 HOST = localhost PORT = 3000 -DATABASE = 'localhost:27017/project-mulla' // Your mongodb database path EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' PAYBILL_NUMBER = '898998' From 01ac4952ca236b37637ef19bdcaa27359190609f Mon Sep 17 00:00:00 2001 From: gangachris Date: Tue, 24 May 2016 12:49:33 +0300 Subject: [PATCH 47/96] [Fixed #23] Fix all eslint syntax issues --- .eslintrc | 2 +- .gitignore | 1 + .hound.yml | 2 + .jshintignore | 2 - .jshintrc | 48 ----------------- server/config/index.js | 4 +- server/config/statusCodes.js | 44 ++++++++-------- server/controllers/ConfirmPayment.js | 4 +- server/controllers/PaymentRequest.js | 6 ++- server/controllers/PaymentStatus.js | 4 +- server/controllers/SOAPRequest.js | 20 +++---- server/errors/ResponseError.js | 4 +- server/routes/index.js | 78 +++++++++++++--------------- server/utils/GenEncryptedPassword.js | 10 ++-- server/utils/ParseResponse.js | 16 +++--- server/utils/ucFirst.js | 18 +++---- server/validators/requiredParams.js | 8 +-- 17 files changed, 113 insertions(+), 158 deletions(-) delete mode 100644 .jshintignore delete mode 100644 .jshintrc diff --git a/.eslintrc b/.eslintrc index 4235824..db28d59 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,7 @@ "node": true, "mocha": true }, - "extends": "airbnb", + "extends": "airbnb/base", "rules": { "prefer-template": 0, "prefer-rest-params": 0, 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/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(); }; From c1f9c10148e89b7b905277326e6587704370a623 Mon Sep 17 00:00:00 2001 From: gangachris Date: Tue, 24 May 2016 13:26:02 +0300 Subject: [PATCH 48/96] Extend airbnb --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index db28d59..4235824 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,7 @@ "node": true, "mocha": true }, - "extends": "airbnb/base", + "extends": "airbnb", "rules": { "prefer-template": 0, "prefer-rest-params": 0, From 51575e8dc5ebd67ea7b0c3c054cb75ce0e39409d Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 24 May 2016 14:38:51 +0300 Subject: [PATCH 49/96] Updated readme --- README.md | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ec1021e..7f7192e 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,18 @@ set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7 { "response": { - "return_code": "00", - "status_code": 200, - "message": "Transaction carried successfully", - "description": "success", - "trx_id": "b3f28c05ae72ff3cb23fb70b2b33ad4d", - "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", - "referenceID": "f765b1ef-6890-44f2-bc7a-9be23013da1c", - "timeStamp": "20160522000459", - "clientPhoneNumber": "254723001575", - "merchantTransactionID": "4938a780-1f3b-11e6-acc6-5dabc98661b9", - "amountInDoubleFloat": "450.00" + "amount_in_double_float": "450.00", + "client_phone_number": "254723001575", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "description": "success", + "extra_payload": {}, + "status_code": 200, + "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", + "message": "Transaction carried successfully", + "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", + "return_code": "00", + "time_stamp": "20160522161208", + "trx_id": "45a3f4b64cde9d88440211187f73944b" } } ``` @@ -124,6 +125,10 @@ TEST_PHONENUMBER = '0720000000' TEST_AMOUNT = '10.00' ``` +__The `PAYBILL_NUMBER` and `PASSKEY` are provided by Safaricom once you have registered for the MPESA G2 API.__ + +*__PLEASE NOTE__: The details above only serve as examples* + #### It's now ready to launch ```bash @@ -133,7 +138,6 @@ $ npm start > node index.js Express server listening on 3000, in development mode -Mongoose has connected to the database specified. ``` #### Do a test run @@ -173,17 +177,17 @@ set-cookie: connect.sid=s%3AiWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly { "response": { - "amount_in_double_float": "450.00", - "client_phone_number": "254723001575", - "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", - "description": "success", - "extra_payload": {}, - "status_code": 200, - "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", - "message": "Transaction carried successfully", - "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", - "return_code": "00", - "time_stamp": "20160522161208", + "amount_in_double_float": "450.00", + "client_phone_number": "254723001575", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "description": "success", + "extra_payload": {}, + "status_code": 200, + "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", + "message": "Transaction carried successfully", + "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", + "return_code": "00", + "time_stamp": "20160522161208", "trx_id": "45a3f4b64cde9d88440211187f73944b" } } From e5f39ce14b3af95ac58f7f64533a43c59a006a67 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 24 May 2016 15:10:53 +0300 Subject: [PATCH 50/96] Fixed grammer issues in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7f7192e..a0556c7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. -In short, it'll deal with all of the SOAP shenanigans while you REST. 😄 +In short, it'll deal with all of the SOAP shenanigans while you REST. The aim of **Project Mulla**, is to create a REST API that interfaces with the **ugly MPESA G2 API.** @@ -40,7 +40,7 @@ Content-Type: application/json; charset=utf-8 Date: Sat, 21 May 2016 10:03:37 GMT ETag: W/"1fe-jy66YehfhiFHWoyTNHpSnA" X-Powered-By: Express -set-cookie: connect.sid=s%3Anc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly +set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly { "response": { @@ -173,7 +173,7 @@ Content-Type: application/json; charset=utf-8 Date: Sun, 22 May 2016 13:12:09 GMT ETag: W/"216-NgmF2VWb0PIkUOKfya6WlA" X-Powered-By: Express -set-cookie: connect.sid=s%3AiWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly +set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly { "response": { From 2e65cb28c18097904fe219bfa717b076968eae6a Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 24 May 2016 15:15:47 +0300 Subject: [PATCH 51/96] Updated readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0556c7..04e7389 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,11 @@ _Endpoint_: **`https://awesome-service.com/api/v1/payment/request`** _Parameters_: - **`phoneNumber`** - The phone number of your client - **`totalAmount`** - The total amount you are charging the client -- **`referenceID`** - The reference ID of the order or service **[optional; one is generated for you if missing]** -- **`merchantTransactionID`** - This specific order's or service's transaction ID **[optional; one is generated for you if missing]** +- **`referenceID`** - The reference ID of the order or service **[optional]** +- **`merchantTransactionID`** - This specific order's or service's transaction ID **[optional]** + +__NOTE:__ If `merchantTransactionID` or `referenceID` are not provided a time-based and random +UUID is generated for each respectively. _Response:_ From a940a1c02cb1eee3227b996b6b505f129dc8984c Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 24 May 2016 22:34:43 +0300 Subject: [PATCH 52/96] Refactored responseError function syntax style --- server/errors/ResponseError.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/errors/ResponseError.js b/server/errors/ResponseError.js index ee578af..c0d75ab 100644 --- a/server/errors/ResponseError.js +++ b/server/errors/ResponseError.js @@ -1,7 +1,9 @@ 'use strict'; -module.exports = function ResponseError(error, res) { +let responseError = (error, res) => { 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 }); }; + +module.exports = responseError; From e01ceadf7410a9862426feda7bdc93255b57ee57 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Wed, 25 May 2016 16:10:40 +0300 Subject: [PATCH 53/96] [Finished #21] Added utils/ script tests --- .coveralls.yml | 1 + README.md | 2 + docs/requests/1-process-checkout.xml | 2 - docs/responses/4-transaction-confirmed.xml | 2 +- docs/responses/5-transaction-completed.xml | 8 -- gulpfile.js | 55 +++++++++++++ index.js | 44 +++++------ package.json | 16 +++- server/errors/ResponseError.js | 2 +- server/routes/index.js | 2 +- test/utils/ParseResponse.js | 90 ++++++++++++++++++++++ test/utils/genTransactionPassword.js | 30 ++++++++ test/utils/ucFirst.js | 18 +++++ 13 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 .coveralls.yml create mode 100644 gulpfile.js create mode 100644 test/utils/ParseResponse.js create mode 100644 test/utils/genTransactionPassword.js create mode 100644 test/utils/ucFirst.js diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..10ad240 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: ryNIFOAZ1HF1firLwwslUDfvsjy3RofHM diff --git a/README.md b/README.md index 04e7389..b784e21 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Coverage Status](https://coveralls.io/repos/github/kn9ts/project-mulla/badge.svg?branch=master)](https://coveralls.io/github/kn9ts/project-mulla?branch=master) + ![](http://cdn.javascript.co.ke/images/banner.png) > **What MPESA G2 API should have been in the 21st century.** diff --git a/docs/requests/1-process-checkout.xml b/docs/requests/1-process-checkout.xml index 313def9..75ced85 100644 --- a/docs/requests/1-process-checkout.xml +++ b/docs/requests/1-process-checkout.xml @@ -2,7 +2,6 @@ 898945 - MmRmNTliMjIzNjJhNmI5ODVhZGU5OTAxYWQ4NDJkZmI2MWE4ODg1ODFhMTQ3ZmZmNTFjMjg4M2UyYWQ5NTU3Yw== 20141128174717 @@ -13,7 +12,6 @@ 1112254500 54 2547204871865 - http://172.21.20.215:8080/test xml diff --git a/docs/responses/4-transaction-confirmed.xml b/docs/responses/4-transaction-confirmed.xml index 6d57f78..7e7c7fb 100644 --- a/docs/responses/4-transaction-confirmed.xml +++ b/docs/responses/4-transaction-confirmed.xml @@ -3,7 +3,7 @@ 00 Success - + 5f6af12be0800c4ffabb4cf2608f0808 diff --git a/docs/responses/5-transaction-completed.xml b/docs/responses/5-transaction-completed.xml index 02decc9..6f6976c 100644 --- a/docs/responses/5-transaction-completed.xml +++ b/docs/responses/5-transaction-completed.xml @@ -14,11 +14,3 @@ - - diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..e0b2b6e --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,55 @@ +'use strict'; + +require('./environment'); +const gulp = require('gulp'); +const mocha = require('gulp-mocha'); +const istanbul = require('gulp-istanbul'); +const coveralls = require('gulp-coveralls'); +const eslint = require('gulp-eslint'); +const runSequence = require('run-sequence').use(gulp); + + +const paths = { + serverTests: './tests' +} + +gulp.task('lint', () => { + return gulp.src(['index.js', './server/**/*.js', '!node_modules/**']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('istanbul', ['lint'], () => { + return gulp.src(['./server/**/*.js', '!node_modules/**']) + .pipe(istanbul({ includeUntested: true })) + .pipe(istanbul.hookRequire()); +}); + +gulp.task('test:bend', ['istanbul'], () => { + return gulp.src(['test/**/*.js']) + .pipe(mocha({ reporter: 'spec' })) + .pipe(istanbul.writeReports({ + dir: './coverage', + reporters: ['html', 'lcov', 'text', 'json'] + })) + .once('error', (err) => { + console.log(err); + process.exit(1); + }) + .once('end', () => { + process.exit(); + }) + // .pipe(istanbul.enforceThresholds({ + // thresholds: { + // global: 80, + // each: -10 + // } + // })); +}); + +gulp.task('coveralls', () => gulp.src('coverage/lcov.info').pipe(coveralls())); + +gulp.task('test', function(callback) { + runSequence('test:bend', 'coveralls', callback); +}); diff --git a/index.js b/index.js index 5d8ea5c..29d9316 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,18 @@ 'use strict'; require('./environment'); -let express = require('express'); -let app = express(); -let path = require('path'); -let config = require('./server/config')(process.env.NODE_ENV); -// let favicon = require('serve-favicon'); -let morgan = require('morgan'); -let cookieParser = require('cookie-parser'); -let bodyParser = require('body-parser'); -let session = require('express-session'); -let routes = require('./server/routes'); -let genTransactionPassword = require('./server/utils/genTransactionPassword'); -let apiVersion = process.env.API_VERSION; +const express = require('express'); +const app = express(); +const path = require('path'); +const config = require('./server/config')(process.env.NODE_ENV); +// const favicon = require('serve-favicon'); +const morgan = require('morgan'); +const cookieParser = require('cookie-parser'); +const bodyParser = require('body-parser'); +const session = require('express-session'); +const routes = require('./server/routes'); +const genTransactionPassword = require('./server/utils/genTransactionPassword'); +const apiVersion = process.env.API_VERSION; // view engine setup @@ -23,9 +23,7 @@ app.set('view engine', 'jade'); // For now, they churned out via a JSON response app.use(morgan('dev')); app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ - extended: true -})); +app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser()); // uncomment after placing your favicon in /public // app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); @@ -45,11 +43,12 @@ app.use(session({ app.use(`/api/v${apiVersion}/payment*`, genTransactionPassword); // get an instance of the router for api routes -app.use(`/api/v${apiVersion}`, routes(express.Router())); +const apiRouter = express.Router; +app.use(`/api/v${apiVersion}`, routes(apiRouter())); // catch 404 and forward to error handler app.use((req, res, next) => { - let err = new Error('Not Found'); + const err = new Error('Not Found'); err.request = req.originalUrl; err.status = 404; next(err); @@ -59,20 +58,19 @@ app.use((req, res, next) => { app.use((err, req, res) => { res.status(err.status || 500); // get the error stack - let stack = err.stack.split(/\n/).map((err) => { - return err.replace(/\s{2,}/g, ' ').trim(); - }); - console.log('ERROR PASSING THROUGH', err.message); + const stack = err.stack.split(/\n/) + .map(error => error.replace(/\s{2,}/g, ' ').trim()); + // console.log('ERROR PASSING THROUGH', err.message); // send out the error as json res.json({ api: err, url: req.originalUrl, error: err.message, - stack: stack + stack, }); }); -var server = app.listen(process.env.PORT || 3000, () => { +const server = app.listen(process.env.PORT || 3000, () => { console.log('Express server listening on %d, in %s' + ' mode', server.address().port, app.get('env')); }); diff --git a/package.json b/package.json index 1e4c0a2..4b4f86a 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "private": true, "main": "index.js", "scripts": { + "test": "gulp test", "develop": "nodemon -w ./server --exec npm start", - "lint": "eslint ./server", - "babel-compile": "./node_modules/.bin/babel ./server ./*.js --out-dir dist", + "lint": "eslint ./server ./test", "start": "node index.js" }, "repository": { @@ -41,12 +41,22 @@ "babel-eslint": "^6.0.4", "babel-preset-es2015": "^6.6.0", "babel-preset-stage-0": "^6.5.0", + "chai": "^3.5.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" + "gulp": "^3.9.1", + "gulp-coveralls": "^0.1.4", + "gulp-eslint": "^2.0.0", + "gulp-istanbul": "^0.10.4", + "gulp-mocha": "^2.2.0", + "istanbul": "^0.4.3", + "mocha": "^2.5.2", + "nodemon": "^1.9.2", + "run-sequence": "^1.2.1", + "sinon": "^1.17.4" }, "engines": { "node": ">=4.2.0" diff --git a/server/errors/ResponseError.js b/server/errors/ResponseError.js index c0d75ab..806193a 100644 --- a/server/errors/ResponseError.js +++ b/server/errors/ResponseError.js @@ -1,6 +1,6 @@ 'use strict'; -let responseError = (error, res) => { +const responseError = (error, res) => { 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 5b85592..a706977 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'); diff --git a/test/utils/ParseResponse.js b/test/utils/ParseResponse.js new file mode 100644 index 0000000..6ba5ace --- /dev/null +++ b/test/utils/ParseResponse.js @@ -0,0 +1,90 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const chai = require('chai'); +const assert = chai.assert; + +const ParseResponse = require('../../server/utils/ParseResponse'); +const XMLFile = { + processCheckoutResponse: '../../docs/responses/2-process-checkout-response.xml', + transactionConfirmResponse: '../../docs/responses/4-transaction-confirmed.xml', + transactionCompleteResponse: '../../docs/responses/5-transaction-completed.xml', +}; + +describe('ParseResponse', () => { + it('when class is instantiated bodyTagName is defined', () => { + const parser = new ParseResponse('bodyTagName'); + assert.equal(parser.bodyTagName, 'bodyTagName'); + }); + + it('parses a processCheckoutResponse XML response', () => { + const processCheckoutResponse = fs.readFileSync( + path.join(__dirname, XMLFile.processCheckoutResponse), + 'utf-8' + ).replace(/\n\s+/gmi, ''); + + const parser = new ParseResponse('processcheckoutresponse'); + const parsedResponse = parser.parse(processCheckoutResponse); + parsedResponse.toJSON(); + + assert.isString(parser.response, 'XML was parsed'); + assert.notMatch(parser.response, /[A-Z]/gm, 'all character are in lowercase'); + + assert.isObject(parser.json, 'JSON was extracted'); + assert.includeMembers(Object.keys(parser.json), [ + 'return_code', + 'description', + 'trx_id', + 'cust_msg', + ], 'parsed json has the following properties'); + }); + + it('parses a transactionConfirmResponse XML response', () => { + const transactionConfirmResponse = fs.readFileSync( + path.join(__dirname, XMLFile.transactionConfirmResponse), + 'utf-8' + ).replace(/\n\s+/gmi, ''); + + const parser = new ParseResponse('transactionconfirmresponse'); + const parsedResponse = parser.parse(transactionConfirmResponse); + parsedResponse.toJSON(); + + assert.isString(parser.response, 'XML was parsed'); + assert.notMatch(parser.response, /[A-Z]/gm, 'all character are in lowercase'); + + assert.isObject(parser.json, 'JSON was extracted'); + assert.includeMembers(Object.keys(parser.json), [ + 'return_code', + 'description', + 'trx_id', + ], 'parsed json has the following properties'); + }); + + it('parses a transactionCompleteResponse XML response', () => { + const transactionCompleteResponse = fs.readFileSync( + path.join(__dirname, XMLFile.transactionCompleteResponse), + 'utf-8' + ).replace(/\n\s+/gmi, ''); + + const parser = new ParseResponse('resultmsg'); + const parsedResponse = parser.parse(transactionCompleteResponse); + parsedResponse.toJSON(); + + assert.isString(parser.response, 'XML was parsed'); + assert.notMatch(parser.response, /[A-Z]/gm, 'all character are in lowercase'); + + assert.isObject(parser.json, 'JSON was extracted'); + assert.sameMembers(Object.keys(parser.json), [ + 'msisdn', + 'amount', + 'm-pesa_trx_date', + 'm-pesa_trx_id', + 'trx_status', + 'return_code', + 'description', + 'merchant_transaction_id', + 'trx_id', + ], 'parsed json has the following properties'); + }); +}); diff --git a/test/utils/genTransactionPassword.js b/test/utils/genTransactionPassword.js new file mode 100644 index 0000000..ef86e96 --- /dev/null +++ b/test/utils/genTransactionPassword.js @@ -0,0 +1,30 @@ +'use strict'; + +const chai = require('chai'); +const assert = chai.assert; +const expect = chai.expect; +const sinon = require('sinon'); + +const genTransactionPassword = require('../../server/utils/genTransactionPassword'); + +describe('genTransactionPassword', () => { + const req = {}; + const next = sinon.spy(); + + before(() => { + genTransactionPassword(req, null, next); + }); + + it('attaches an encryptedPassword property in request object', () => { + assert.isDefined(req.encryptedPassword, 'encryptedPassword generated'); + }); + + it('attaches a timeStamp property in request object', () => { + assert.isNumber(parseInt(req.timeStamp, 10), 'is numerical'); + assert.lengthOf(req.timeStamp, 14, 'is 14 numbers long'); + }); + + it('expect next to have been called', () => { + expect(next).to.have.been.calledOnce; + }); +}); diff --git a/test/utils/ucFirst.js b/test/utils/ucFirst.js new file mode 100644 index 0000000..4464525 --- /dev/null +++ b/test/utils/ucFirst.js @@ -0,0 +1,18 @@ +'use strict'; + +const chai = require('chai'); +const assert = chai.assert; + +const ucFirst = require('../../server/utils/ucFirst'); + +describe('ucFirst', () => { + it('uppercases the 1st letter in the string', () => { + const string = 'Projectmulla'; + assert.equal(ucFirst(string), 'Projectmulla'); + }); + + it('lowercases all the letters in the string after the 1st letter', () => { + const string = 'proJECTMulLA'; + assert.equal(ucFirst(string), 'Projectmulla'); + }); +}); From dea67fdb81b25c8944e21549c424815067fd0ed9 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 26 May 2016 01:39:10 +0300 Subject: [PATCH 54/96] Tests added for utils/ scripts --- README.md | 1 + gulpfile.js | 73 ++++++++++++---------------- package.json | 5 +- server/errors/ResponseError.js | 9 ---- server/routes/index.js | 2 +- server/utils/errors/responseError.js | 12 +++++ test/utils/errors/reponseError.js | 44 +++++++++++++++++ 7 files changed, 89 insertions(+), 57 deletions(-) delete mode 100644 server/errors/ResponseError.js create mode 100644 server/utils/errors/responseError.js create mode 100644 test/utils/errors/reponseError.js diff --git a/README.md b/README.md index b784e21..75cde6f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Coverage Status](https://coveralls.io/repos/github/kn9ts/project-mulla/badge.svg?branch=master)](https://coveralls.io/github/kn9ts/project-mulla?branch=master) +[![Build Status](https://semaphoreci.com/api/v1/kn9ts/project-mulla/branches/develop/badge.svg)](https://semaphoreci.com/kn9ts/project-mulla) ![](http://cdn.javascript.co.ke/images/banner.png) diff --git a/gulpfile.js b/gulpfile.js index e0b2b6e..c1398da 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,50 +6,37 @@ const mocha = require('gulp-mocha'); const istanbul = require('gulp-istanbul'); const coveralls = require('gulp-coveralls'); const eslint = require('gulp-eslint'); -const runSequence = require('run-sequence').use(gulp); - - -const paths = { - serverTests: './tests' -} - -gulp.task('lint', () => { - return gulp.src(['index.js', './server/**/*.js', '!node_modules/**']) - .pipe(eslint()) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()); -}); - -gulp.task('istanbul', ['lint'], () => { - return gulp.src(['./server/**/*.js', '!node_modules/**']) - .pipe(istanbul({ includeUntested: true })) - .pipe(istanbul.hookRequire()); -}); - -gulp.task('test:bend', ['istanbul'], () => { - return gulp.src(['test/**/*.js']) - .pipe(mocha({ reporter: 'spec' })) - .pipe(istanbul.writeReports({ - dir: './coverage', - reporters: ['html', 'lcov', 'text', 'json'] - })) - .once('error', (err) => { - console.log(err); - process.exit(1); - }) - .once('end', () => { - process.exit(); - }) - // .pipe(istanbul.enforceThresholds({ - // thresholds: { - // global: 80, - // each: -10 - // } - // })); -}); +const runSequence = require('run-sequence'); + +const filesToLint = [ + 'gulpfile.js', + 'index.js', + './server/**/*.js', + '!node_modules/**', +]; + +gulp.task('lint', () => gulp.src(filesToLint) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError())); + +gulp.task('coverage', () => gulp + .src(['!node_modules/**', '!server/routes/**', './server/**/*.js']) + .pipe(istanbul({ includeUntested: true })) + .pipe(istanbul.hookRequire())); + +gulp.task('test:backend', () => gulp.src(['test/**/*.js']) + .pipe(mocha({ reporter: 'spec' })) + .once('error', err => { + throw err; + }) + .pipe(istanbul.writeReports({ + dir: './coverage', + reporters: ['html', 'lcov', 'text', 'json'], + }))); gulp.task('coveralls', () => gulp.src('coverage/lcov.info').pipe(coveralls())); -gulp.task('test', function(callback) { - runSequence('test:bend', 'coveralls', callback); +gulp.task('test', callback => { + runSequence('lint', 'coverage', 'test:backend', callback); }); diff --git a/package.json b/package.json index 4b4f86a..a870e02 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,8 @@ "serve-favicon": "~2.3.0" }, "devDependencies": { - "babel-cli": "^6.7.7", - "babel-eslint": "^6.0.4", - "babel-preset-es2015": "^6.6.0", - "babel-preset-stage-0": "^6.5.0", "chai": "^3.5.0", + "coveralls": "^2.11.9", "eslint": "^2.10.2", "eslint-config-airbnb": "^9.0.1", "eslint-plugin-import": "^1.8.0", diff --git a/server/errors/ResponseError.js b/server/errors/ResponseError.js deleted file mode 100644 index 806193a..0000000 --- a/server/errors/ResponseError.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const responseError = (error, res) => { - 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 }); -}; - -module.exports = responseError; diff --git a/server/routes/index.js b/server/routes/index.js index a706977..90fd26c 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('../utils/errors/responseError'); const ParseResponse = require('../utils/ParseResponse'); const requiredParams = require('../validators/requiredParams'); const PaymentRequest = require('../controllers/PaymentRequest'); diff --git a/server/utils/errors/responseError.js b/server/utils/errors/responseError.js new file mode 100644 index 0000000..818fdd1 --- /dev/null +++ b/server/utils/errors/responseError.js @@ -0,0 +1,12 @@ +'use strict'; + +const responseError = (error, res) => { + const descriptionExists = (typeof error === 'object' && 'description' in error); + const statusCodeExists = (typeof error === 'object' && 'status_code' in error); + + const err = new Error(descriptionExists ? error.description : error); + err.status = statusCodeExists ? error.status_code : 500; + return res.status(err.status).json({ response: error }); +}; + +module.exports = responseError; diff --git a/test/utils/errors/reponseError.js b/test/utils/errors/reponseError.js new file mode 100644 index 0000000..73ab934 --- /dev/null +++ b/test/utils/errors/reponseError.js @@ -0,0 +1,44 @@ +'use strict'; + +const chai = require('chai'); +const assert = chai.assert; +const expect = chai.expect; +const sinon = require('sinon'); + +const responseError = require('../../../server/utils/errors/responseError'); + +let spyCall; +const res = {}; +let error = 'An error message'; + +describe('responseError', () => { + beforeEach(() => { + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); + responseError(error, res); + }); + + it('Calls response method with default(500) error code', () => { + spyCall = res.status.getCall(0); + assert.isTrue(res.status.calledOnce); + assert.isTrue(spyCall.calledWithExactly(500)); + }); + + it('Returns error wrapped in json response', () => { + spyCall = res.json.getCall(0); + assert.isTrue(res.json.calledOnce); + assert.isObject(spyCall.args[0]); + assert.property(spyCall.args[0], 'response', 'status'); + }); + + it('Calls response method with custom error code', () => { + error = { + description: 'Bad request', + status_code: 400 + }; + responseError(error, res); + spyCall = res.status.getCall(0); + assert.isTrue(res.status.called); + assert.isTrue(res.status.calledWithExactly(400)); + }); +}); From ee8dcc3092497eab3e3a909c3903197908441639 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 26 May 2016 01:41:05 +0300 Subject: [PATCH 55/96] Ignoring untracted jekyll build folders --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4114993..fa08a36 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,10 @@ node_modules public dist -./server/config/local.env.js +server/config/local.env.js npm-debug.log +.sass-cache +_site ### Added by loopback framework *.csv From cb96f8569cb53a91b5c4a0b7d80c3c9379a54184 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 26 May 2016 01:48:21 +0300 Subject: [PATCH 56/96] Add missing status request and response SOAP/XML examples --- docs/statuses/transaction-status-query.xml | 15 +++++++++++++++ docs/statuses/transaction-status-response.xml | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/statuses/transaction-status-query.xml create mode 100644 docs/statuses/transaction-status-response.xml diff --git a/docs/statuses/transaction-status-query.xml b/docs/statuses/transaction-status-query.xml new file mode 100644 index 0000000..055b5aa --- /dev/null +++ b/docs/statuses/transaction-status-query.xml @@ -0,0 +1,15 @@ + + + + 898945 + MmRmNTliMjIzNjJhNmI5ODVhZGU5OTAxYWQ4NDJkZmI2MWE4ODg1ODFhMTQ3ZmZmNTFjMjg4M2UyYWQ5NTU3Yw== + 20141128174717 + + + + + ddd396509b168297141a747cd2dc1748 + 911-100 + + + diff --git a/docs/statuses/transaction-status-response.xml b/docs/statuses/transaction-status-response.xml new file mode 100644 index 0000000..bdd67f7 --- /dev/null +++ b/docs/statuses/transaction-status-response.xml @@ -0,0 +1,16 @@ + + + + 254720471865 + 54000 + 2014-12-01 16:59:07 + ddd396509b168297141a747cd2dc1748 + Failed + 01 + InsufficientFunds + + + ddd396509b168297141a747cd2dc1748 + + + From f6ddcebd72354ff0af3aa91ed6aeb7c81f9bd32d Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 26 May 2016 01:49:04 +0300 Subject: [PATCH 57/96] Add missing processCheckoutResponse SOAP/XML sample --- docs/responses/2-process-checkout-response.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/responses/2-process-checkout-response.xml diff --git a/docs/responses/2-process-checkout-response.xml b/docs/responses/2-process-checkout-response.xml new file mode 100644 index 0000000..a42ac24 --- /dev/null +++ b/docs/responses/2-process-checkout-response.xml @@ -0,0 +1,11 @@ + + + + 00 + Success + cce3d32e0159c1e62a9ec45b67676200 + + To complete this transaction, enter your Bonga PIN on your handset. if you don't have one dial *126*5# for instructions + + + From 4970beaa20402392fb4d2f2aecc85f024c9f2ece Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 27 May 2016 14:38:02 +0300 Subject: [PATCH 58/96] Move the route handlers to their respective controller classes --- server/controllers/ConfirmPayment.js | 19 +++ server/controllers/PaymentRequest.js | 43 ++++++- server/controllers/PaymentStatus.js | 20 +++ server/controllers/PaymentSuccess.js | 38 ++++++ server/routes/index.js | 118 ++---------------- server/utils/ParseResponse.js | 8 +- ...redParams.js => checkForRequiredParams.js} | 11 +- 7 files changed, 139 insertions(+), 118 deletions(-) create mode 100644 server/controllers/PaymentSuccess.js rename server/validators/{requiredParams.js => checkForRequiredParams.js} (85%) diff --git a/server/controllers/ConfirmPayment.js b/server/controllers/ConfirmPayment.js index 225c05a..f251091 100644 --- a/server/controllers/ConfirmPayment.js +++ b/server/controllers/ConfirmPayment.js @@ -1,5 +1,9 @@ 'use strict'; +const ParseResponse = require('../utils/ParseResponse'); +const SOAPRequest = require('../controllers/SOAPRequest'); +const responseError = require('../utils/errors/responseError'); + module.exports = class ConfirmPayment { constructor(data) { const transactionConfirmRequest = typeof data.transactionID !== undefined ? @@ -25,4 +29,19 @@ module.exports = class ConfirmPayment { requestBody() { return this.body; } + + static handler(req, res) { + const payment = new ConfirmPayment({ + transactionID: req.params.id, // eg. '99d0b1c0237b70f3dc63f36232b9984c' + timeStamp: req.timeStamp, + encryptedPassword: req.encryptedPassword, + }); + const parser = new ParseResponse('transactionconfirmresponse'); + const confirm = new SOAPRequest(payment, parser); + + // process ConfirmPayment response + confirm.post() + .then(response => res.status(200).json({ response })) + .catch(error => responseError(error, res)); + } }; diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index 8049215..5ec9ff3 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -1,5 +1,10 @@ 'use strict'; +const uuid = require('node-uuid'); +const ParseResponse = require('../utils/ParseResponse'); +const SOAPRequest = require('../controllers/SOAPRequest'); +const responseError = require('../utils/errors/responseError'); + module.exports = class PaymentRequest { constructor(data) { this.body = ` @@ -17,7 +22,7 @@ module.exports = class PaymentRequest { ${data.amountInDoubleFloat} ${data.clientPhoneNumber} - ${data.extraMerchantPayload ? JSON.stringify(data.extraMerchantPayload) : ''} + ${data.extraPayload ? JSON.stringify(data.extraPayload) : ''} ${process.env.CALLBACK_URL} ${process.env.CALLBACK_METHOD} @@ -30,4 +35,40 @@ module.exports = class PaymentRequest { requestBody() { return this.body; } + + static handler(req, res) { + const paymentDetails = { + // transaction reference ID + referenceID: (req.body.referenceID || uuid.v4()), + // product, service or order ID + merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), + amountInDoubleFloat: (req.body.totalAmount || process.env.TEST_AMOUNT), + clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), + extraPayload: req.body.extraPayload, + timeStamp: req.timeStamp, + encryptedPassword: req.encryptedPassword, + }; + + const payment = new PaymentRequest(paymentDetails); + const parser = new ParseResponse('processcheckoutresponse'); + const soapRequest = new SOAPRequest(payment, parser); + + // remove encryptedPassword + delete paymentDetails.encryptedPassword; + + // convert paymentDetails properties to underscore notation + const returnThesePaymentDetails = {}; + for (const key of Object.keys(paymentDetails)) { + const newkey = key.replace(/[A-Z]{1,}/g, match => '_' + match.toLowerCase()); + returnThesePaymentDetails[newkey] = paymentDetails[key]; + delete paymentDetails[key]; + } + + // make the payment requets and process response + soapRequest.post() + .then(response => res.status(200).json({ + response: Object.assign({}, response, returnThesePaymentDetails), + })) + .catch(error => responseError(error, res)); + } }; diff --git a/server/controllers/PaymentStatus.js b/server/controllers/PaymentStatus.js index af8f939..7363acf 100644 --- a/server/controllers/PaymentStatus.js +++ b/server/controllers/PaymentStatus.js @@ -1,4 +1,9 @@ 'use strict'; + +const ParseResponse = require('../utils/ParseResponse'); +const SOAPRequest = require('../controllers/SOAPRequest'); +const responseError = require('../utils/errors/responseError'); + module.exports = class PaymentStatus { constructor(data) { const transactionStatusRequest = typeof data.transactionID !== undefined ? @@ -24,4 +29,19 @@ module.exports = class PaymentStatus { requestBody() { return this.body; } + + static handler(req, res) { + const payment = new PaymentStatus({ + transactionID: req.params.id, + timeStamp: req.timeStamp, + encryptedPassword: req.encryptedPassword, + }); + const parser = new ParseResponse('transactionstatusresponse'); + const status = new SOAPRequest(payment, parser); + + // process PaymentStatus response + return status.post() + .then(response => res.status(200).json({ response })) + .catch(error => responseError(error, res)); + } }; diff --git a/server/controllers/PaymentSuccess.js b/server/controllers/PaymentSuccess.js new file mode 100644 index 0000000..1056930 --- /dev/null +++ b/server/controllers/PaymentSuccess.js @@ -0,0 +1,38 @@ +'use strict'; + +const request = require('request'); + +module.exports = class PaymentSuccess { + static handler(req, res) { + const keys = Object.keys(req.body); + 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) { + 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', + }, + }; + + // make a request to the merchant's endpoint + request(requestParams, (error) => { + if (error) { + res.sendStatus(500); + return; + } + res.sendStatus(200); + }); + } +} diff --git a/server/routes/index.js b/server/routes/index.js index 90fd26c..e2ba6b2 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,120 +1,22 @@ 'use strict'; -const uuid = require('node-uuid'); -const request = require('request'); -const responseError = require('../utils/errors/responseError'); -const ParseResponse = require('../utils/ParseResponse'); -const requiredParams = require('../validators/requiredParams'); + const PaymentRequest = require('../controllers/PaymentRequest'); const ConfirmPayment = require('../controllers/ConfirmPayment'); const PaymentStatus = require('../controllers/PaymentStatus'); +const PaymentSuccess = require('../controllers/PaymentSuccess'); + +const requiredParams = require('../validators/checkForRequiredParams'); const SOAPRequest = require('../controllers/SOAPRequest'); module.exports = (router) => { - /* Check the status of the API system */ - router.get('/', (req, res) => res.json({ status: 200 })); - - router.post('/payment/request', requiredParams, (req, res) => { - const paymentDetails = { - // transaction reference ID - referenceID: (req.body.referenceID || uuid.v4()), - // product, service or order ID - merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), - amountInDoubleFloat: (req.body.totalAmount || process.env.TEST_AMOUNT), - clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), - extraPayload: req.body.extraPayload, - timeStamp: req.timeStamp, - encryptedPassword: req.encryptedPassword, - }; - - 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 - delete paymentDetails.encryptedPassword; - - // convert paymentDetails properties to underscore notation - // to match the SAG JSON response - for (const key of Object.keys(paymentDetails)) { - 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 - soapRequest.post() - .then(response => res.json({ - response: Object.assign({}, response, paymentDetails), - })) - .catch(error => responseError(error, res)); - }); - - router.get('/payment/confirm/:id', (req, res) => { - const payment = new ConfirmPayment({ - transactionID: req.params.id, // eg. '99d0b1c0237b70f3dc63f36232b9984c' - timeStamp: req.timeStamp, - encryptedPassword: req.encryptedPassword, - }); - const parser = new ParseResponse('transactionconfirmresponse'); - const confirm = new SOAPRequest(payment, parser); - - // process ConfirmPayment response - confirm.post() - .then(response => res.json({ response })) - .catch(error => responseError(error, res)); - }); - - router.get('/payment/status/:id', (req, res) => { - const payment = new PaymentStatus({ - transactionID: req.params.id, - timeStamp: req.timeStamp, - encryptedPassword: req.encryptedPassword, - }); - const parser = new ParseResponse('transactionstatusresponse'); - const status = new SOAPRequest(payment, parser); - - // process PaymentStatus response - status.post() - .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); - 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) { - 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', - }, - }; + // check the status of the API system + router.get('/status', (req, res) => res.json({ status: 200 })); - // make a request to the merchant's endpoint - request(requestParams, (error) => { - if (error) { - res.sendStatus(500); - return; - } - res.sendStatus(200); - }); - }); + router.post('/payment/request', requiredParams, PaymentRequest.handler); + router.get('/payment/confirm/:id', ConfirmPayment.handler); + router.get('/payment/status/:id', PaymentStatus.handler); + router.all('/payment/success', PaymentSuccess.handler); // for testing last POST response // if MERCHANT_ENDPOINT has not been provided diff --git a/server/utils/ParseResponse.js b/server/utils/ParseResponse.js index 5ee960f..925a80a 100644 --- a/server/utils/ParseResponse.js +++ b/server/utils/ParseResponse.js @@ -50,10 +50,8 @@ module.exports = class ParseResponse { } }); - // Unserialise the ENC_PARAMS value - if ('enc_params' in this.json) { - this.json.enc_params = JSON.parse(this.json.enc_params); - } + // delete the enc_params value + delete this.json.enc_params; // Get the equivalent HTTP CODE to respond with this.json = _.assignIn(this.extractCode(), this.json); @@ -61,6 +59,6 @@ module.exports = class ParseResponse { } extractCode() { - return _.find(statusCodes, (o) => o.return_code === this.json.return_code); + return _.find(statusCodes, (o) => o.return_code === parseInt(this.json.return_code, 10)); } }; diff --git a/server/validators/requiredParams.js b/server/validators/checkForRequiredParams.js similarity index 85% rename from server/validators/requiredParams.js rename to server/validators/checkForRequiredParams.js index 95f6d38..e06613c 100644 --- a/server/validators/requiredParams.js +++ b/server/validators/checkForRequiredParams.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (req, res, next) => { const requiredBodyParams = [ 'referenceID', @@ -29,16 +31,17 @@ module.exports = (req, res, next) => { } const bodyParamKeys = Object.keys(req.body); - req.body.extraPayload = {}; + let extraPayload = {}; // anything that is not a required param // should be added to the extraPayload object for (const key of bodyParamKeys) { if (requiredBodyParams.indexOf(key) === -1) { - req.body.extraPayload[key] = req.body[key]; + extraPayload[key] = req.body[key]; delete req.body[key]; } } - - return next(); + req.body.extraPayload = extraPayload; + // console.log('extraPayload', req.body.extraPayload); + next(); }; From 7d909285e6755d42693e9b6d9757c54ab36eca33 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 27 May 2016 14:38:42 +0300 Subject: [PATCH 59/96] Fix eslint issues and add fix capabaility to eslint npm task --- index.js | 6 +++--- package.json | 4 ++-- server/controllers/PaymentSuccess.js | 2 +- server/routes/index.js | 2 -- server/validators/checkForRequiredParams.js | 4 ++-- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 29d9316..7301df9 100644 --- a/index.js +++ b/index.js @@ -56,13 +56,13 @@ app.use((req, res, next) => { // error handlers app.use((err, req, res) => { - res.status(err.status || 500); + console.log('ERROR PASSING THROUGH', err.message); // get the error stack const stack = err.stack.split(/\n/) .map(error => error.replace(/\s{2,}/g, ' ').trim()); - // console.log('ERROR PASSING THROUGH', err.message); + // send out the error as json - res.json({ + res.status(err.status || 500).json({ api: err, url: req.originalUrl, error: err.message, diff --git a/package.json b/package.json index a870e02..64c88e4 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "main": "index.js", "scripts": { "test": "gulp test", - "develop": "nodemon -w ./server --exec npm start", - "lint": "eslint ./server ./test", + "develop": "nodemon -w ./server/** --exec npm start", + "lint": "eslint --fix ./server ./test", "start": "node index.js" }, "repository": { diff --git a/server/controllers/PaymentSuccess.js b/server/controllers/PaymentSuccess.js index 1056930..08d3803 100644 --- a/server/controllers/PaymentSuccess.js +++ b/server/controllers/PaymentSuccess.js @@ -35,4 +35,4 @@ module.exports = class PaymentSuccess { res.sendStatus(200); }); } -} +}; diff --git a/server/routes/index.js b/server/routes/index.js index e2ba6b2..833e71f 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -4,9 +4,7 @@ const PaymentRequest = require('../controllers/PaymentRequest'); const ConfirmPayment = require('../controllers/ConfirmPayment'); const PaymentStatus = require('../controllers/PaymentStatus'); const PaymentSuccess = require('../controllers/PaymentSuccess'); - const requiredParams = require('../validators/checkForRequiredParams'); -const SOAPRequest = require('../controllers/SOAPRequest'); module.exports = (router) => { diff --git a/server/validators/checkForRequiredParams.js b/server/validators/checkForRequiredParams.js index e06613c..0d5a5e6 100644 --- a/server/validators/checkForRequiredParams.js +++ b/server/validators/checkForRequiredParams.js @@ -31,7 +31,7 @@ module.exports = (req, res, next) => { } const bodyParamKeys = Object.keys(req.body); - let extraPayload = {}; + const extraPayload = {}; // anything that is not a required param // should be added to the extraPayload object @@ -43,5 +43,5 @@ module.exports = (req, res, next) => { } req.body.extraPayload = extraPayload; // console.log('extraPayload', req.body.extraPayload); - next(); + return next(); }; From dccd6a3893f29c4c0bb57952a305562f5f722c3e Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 13:45:32 +0300 Subject: [PATCH 60/96] Finished #26 Software QA: Write unit tests for all the scripts --- package.json | 2 +- server/controllers/ConfirmPayment.js | 34 ++++--- server/controllers/PaymentRequest.js | 29 +++--- server/controllers/PaymentStatus.js | 32 +++--- server/controllers/PaymentSuccess.js | 14 ++- server/controllers/SOAPRequest.js | 12 ++- server/validators/checkForRequiredParams.js | 4 +- test/config/index.js | 22 +++++ test/controllers/ConfirmPayment.js | 93 ++++++++++++++++++ test/controllers/PaymentRequest.js | 92 +++++++++++++++++ test/controllers/PaymentStatus.js | 93 ++++++++++++++++++ test/controllers/PaymentSuccess.js | 52 ++++++++++ test/controllers/SOAPRequest.js | 86 ++++++++++++++++ test/validators/checkForRequiredParams.js | 103 ++++++++++++++++++++ 14 files changed, 617 insertions(+), 51 deletions(-) create mode 100644 test/config/index.js create mode 100644 test/controllers/ConfirmPayment.js create mode 100644 test/controllers/PaymentRequest.js create mode 100644 test/controllers/PaymentStatus.js create mode 100644 test/controllers/PaymentSuccess.js create mode 100644 test/controllers/SOAPRequest.js create mode 100644 test/validators/checkForRequiredParams.js diff --git a/package.json b/package.json index 64c88e4..09a2044 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "test": "gulp test", - "develop": "nodemon -w ./server/** --exec npm start", + "develop": "nodemon -w ./server --exec npm start", "lint": "eslint --fix ./server ./test", "start": "node index.js" }, diff --git a/server/controllers/ConfirmPayment.js b/server/controllers/ConfirmPayment.js index f251091..de53b87 100644 --- a/server/controllers/ConfirmPayment.js +++ b/server/controllers/ConfirmPayment.js @@ -4,9 +4,17 @@ const ParseResponse = require('../utils/ParseResponse'); const SOAPRequest = require('../controllers/SOAPRequest'); const responseError = require('../utils/errors/responseError'); -module.exports = class ConfirmPayment { - constructor(data) { - const transactionConfirmRequest = typeof data.transactionID !== undefined ? +const parseResponse = new ParseResponse('transactionconfirmresponse'); +const soapRequest = new SOAPRequest(); + +class ConfirmPayment { + constructor(request, parser) { + this.parser = parser; + this.soapRequest = request; + } + + buildSoapBody(data) { + const transactionConfirmRequest = typeof data.transactionID !== 'undefined' ? '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; @@ -26,22 +34,20 @@ module.exports = class ConfirmPayment { `; } - requestBody() { - return this.body; - } - - static handler(req, res) { - const payment = new ConfirmPayment({ + handler(req, res) { + const paymentDetails = { transactionID: req.params.id, // eg. '99d0b1c0237b70f3dc63f36232b9984c' timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword, - }); - const parser = new ParseResponse('transactionconfirmresponse'); - const confirm = new SOAPRequest(payment, parser); + }; + const payment = this.buildSoapBody(paymentDetails); + const confirm = this.soapRequest.construct(payment, this.parser); // process ConfirmPayment response - confirm.post() + return confirm.post() .then(response => res.status(200).json({ response })) .catch(error => responseError(error, res)); } -}; +} + +module.exports = new ConfirmPayment(soapRequest, parseResponse); diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index 5ec9ff3..c06fdce 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -5,8 +5,16 @@ const ParseResponse = require('../utils/ParseResponse'); const SOAPRequest = require('../controllers/SOAPRequest'); const responseError = require('../utils/errors/responseError'); -module.exports = class PaymentRequest { - constructor(data) { +const parseResponse = new ParseResponse('processcheckoutresponse'); +const soapRequest = new SOAPRequest(); + +class PaymentRequest { + constructor(request, parser) { + this.parser = parser; + this.soapRequest = request; + } + + buildSoapBody(data) { this.body = ` @@ -32,11 +40,7 @@ module.exports = class PaymentRequest { `; } - requestBody() { - return this.body; - } - - static handler(req, res) { + handler(req, res) { const paymentDetails = { // transaction reference ID referenceID: (req.body.referenceID || uuid.v4()), @@ -49,9 +53,8 @@ module.exports = class PaymentRequest { encryptedPassword: req.encryptedPassword, }; - const payment = new PaymentRequest(paymentDetails); - const parser = new ParseResponse('processcheckoutresponse'); - const soapRequest = new SOAPRequest(payment, parser); + const payment = this.buildSoapBody(paymentDetails); + const request = this.soapRequest.construct(payment, this.parser); // remove encryptedPassword delete paymentDetails.encryptedPassword; @@ -65,10 +68,12 @@ module.exports = class PaymentRequest { } // make the payment requets and process response - soapRequest.post() + return request.post() .then(response => res.status(200).json({ response: Object.assign({}, response, returnThesePaymentDetails), })) .catch(error => responseError(error, res)); } -}; +} + +module.exports = new PaymentRequest(soapRequest, parseResponse); diff --git a/server/controllers/PaymentStatus.js b/server/controllers/PaymentStatus.js index 7363acf..e8eb330 100644 --- a/server/controllers/PaymentStatus.js +++ b/server/controllers/PaymentStatus.js @@ -4,9 +4,17 @@ const ParseResponse = require('../utils/ParseResponse'); const SOAPRequest = require('../controllers/SOAPRequest'); const responseError = require('../utils/errors/responseError'); -module.exports = class PaymentStatus { - constructor(data) { - const transactionStatusRequest = typeof data.transactionID !== undefined ? +const parseResponse = new ParseResponse('transactionstatusresponse'); +const soapRequest = new SOAPRequest(); + +class PaymentStatus { + constructor(request, parser) { + this.parser = parser; + this.soapRequest = request; + } + + buildSoapBody(data) { + const transactionStatusRequest = typeof data.transactionID !== 'undefined' ? '' + data.transactionID + '' : '' + data.merchantTransactionID + ''; @@ -26,22 +34,20 @@ module.exports = class PaymentStatus { `; } - requestBody() { - return this.body; - } - - static handler(req, res) { - const payment = new PaymentStatus({ + handler(req, res) { + const paymentDetails = { transactionID: req.params.id, timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword, - }); - const parser = new ParseResponse('transactionstatusresponse'); - const status = new SOAPRequest(payment, parser); + }; + const payment = this.buildSoapBody(paymentDetails); + const status = this.soapRequest.construct(payment, this.parser); // process PaymentStatus response return status.post() .then(response => res.status(200).json({ response })) .catch(error => responseError(error, res)); } -}; +} + +module.exports = new PaymentStatus(soapRequest, parseResponse); diff --git a/server/controllers/PaymentSuccess.js b/server/controllers/PaymentSuccess.js index 08d3803..a338714 100644 --- a/server/controllers/PaymentSuccess.js +++ b/server/controllers/PaymentSuccess.js @@ -2,8 +2,12 @@ const request = require('request'); -module.exports = class PaymentSuccess { - static handler(req, res) { +class PaymentSuccess { + constructor() { + this.request = request; + } + + handler(req, res) { const keys = Object.keys(req.body); const response = {}; const baseURL = `${req.protocol}://${req.hostname}:${process.env.PORT}`; @@ -27,7 +31,7 @@ module.exports = class PaymentSuccess { }; // make a request to the merchant's endpoint - request(requestParams, (error) => { + this.request(requestParams, (error) => { if (error) { res.sendStatus(500); return; @@ -35,4 +39,6 @@ module.exports = class PaymentSuccess { res.sendStatus(200); }); } -}; +} + +module.exports = new PaymentSuccess(); diff --git a/server/controllers/SOAPRequest.js b/server/controllers/SOAPRequest.js index d9f02c8..2a89f3c 100644 --- a/server/controllers/SOAPRequest.js +++ b/server/controllers/SOAPRequest.js @@ -3,25 +3,27 @@ const request = require('request'); module.exports = class SOAPRequest { - constructor(payment, parser) { + construct(payment, parser) { + this.request = request; this.parser = parser; this.requestOptions = { method: 'POST', uri: process.env.ENDPOINT, rejectUnauthorized: false, - body: payment.requestBody(), + body: payment.body, headers: { 'content-type': 'application/xml; charset=utf-8', }, }; + return this; } post() { return new Promise((resolve, reject) => { // Make the soap request to the SAG URI - request(this.requestOptions, (error, response, body) => { + this.request(this.requestOptions, (error, response, body) => { if (error) { - reject(error); + reject({ description: error.message }); return; } @@ -30,7 +32,7 @@ module.exports = class SOAPRequest { // Anything that is not "00" as the // SOAP response code is a Failure - if (json.status_code !== 200) { + if (json && json.status_code !== 200) { reject(json); return; } diff --git a/server/validators/checkForRequiredParams.js b/server/validators/checkForRequiredParams.js index 0d5a5e6..70493c6 100644 --- a/server/validators/checkForRequiredParams.js +++ b/server/validators/checkForRequiredParams.js @@ -8,7 +8,7 @@ module.exports = (req, res, next) => { 'phoneNumber', ]; - if ('phoneNumber' in req.body) { + if (req.body && 'phoneNumber' in req.body) { // validate the phone number if (!/\+?(254)[0-9]{9}/g.test(req.body.phoneNumber)) { return res.status(400).send('Invalid [phoneNumber]'); @@ -18,7 +18,7 @@ module.exports = (req, res, next) => { } // validate total amount - if ('totalAmount' in req.body) { + if (req.body && 'totalAmount' in req.body) { if (!/^[\d]+(\.[\d]{2})?$/g.test(req.body.totalAmount)) { return res.status(400).send('Invalid [totalAmount]'); } diff --git a/test/config/index.js b/test/config/index.js new file mode 100644 index 0000000..ad90738 --- /dev/null +++ b/test/config/index.js @@ -0,0 +1,22 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); + +const configSetup = require('../../server/config'); + +describe('Config: index.js', () => { + it('returns a default config object if one is provided', () => { + const config = configSetup('staging'); + assert.isObject(config); + assert.sameMembers(Object.keys(config), ['host', 'expressSessionKey']); + }); + + it('returns a configuration object if it exists', () => { + const config = configSetup(process.env.NODE_ENV); + assert.isObject(config); + assert.sameMembers(Object.keys(config), ['host', 'expressSessionKey']); + }); +}); diff --git a/test/controllers/ConfirmPayment.js b/test/controllers/ConfirmPayment.js new file mode 100644 index 0000000..fce579d --- /dev/null +++ b/test/controllers/ConfirmPayment.js @@ -0,0 +1,93 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); +const moment = require('moment'); +const uuid = require('node-uuid'); + +const confirmPayment = require('../../server/controllers/ConfirmPayment'); +const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); + +let timeStamp = moment().format('YYYYMMDDHHmmss'); +let encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +let params = { + transactionID: uuid.v1(), + timeStamp, + encryptedPassword, +}; + +let req = {}; +const res = {}; +let response = { status_code: 200 }; +let promise = new Promise((resolve, reject) => { + resolve(response); +}); + +sinon.stub(promise, 'then', (callback) => { + callback(response); + return promise; +}); + +sinon.stub(promise, 'catch', (callback) => { + callback(new Error('threw an error')); + return promise; +}); + +describe('confirmPayment', () => { + beforeEach(() => { + req.timeStamp = timeStamp; + req.encryptedPassword = encryptedPassword; + req.params = { + id: uuid.v1(), + }; + + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); + + confirmPayment.parser = sinon.stub().returnsThis(); + confirmPayment.soapRequest.construct = sinon.stub().returnsThis(); + confirmPayment.soapRequest.post = sinon.stub().returns(promise); + }); + + it('BuildSoapBody builds the soap body string with transactionID', () => { + confirmPayment.buildSoapBody(params); + + assert.isString(confirmPayment.body); + assert.match(confirmPayment.body, /(TRX_ID)/); + assert.notMatch(confirmPayment.body, /(MERCHANT_TRANSACTION_ID)/); + assert.match(confirmPayment.body, /(soapenv:Envelope)/gi); + }); + + it('if transactionID is not provide soap body built with merchantTransactionID', () => { + delete params.transactionID; + params.merchantTransactionID = uuid.v4(); + confirmPayment.buildSoapBody(params); + + assert.isString(confirmPayment.body); + assert.match(confirmPayment.body, /(MERCHANT_TRANSACTION_ID)/); + assert.notMatch(confirmPayment.body, /(TRX_ID)/); + assert.match(confirmPayment.body, /(soapenv:Envelope)/gi); + }); + + it('Makes a SOAP request and returns a promise', () => { + confirmPayment.buildSoapBody = sinon.stub(); + confirmPayment.handler(req, res); + + assert.isTrue(confirmPayment.buildSoapBody.called); + assert.isTrue(confirmPayment.soapRequest.construct.called); + assert.isTrue(confirmPayment.soapRequest.post.called); + + assert.isTrue(promise.then.called); + assert.isTrue(promise.catch.called); + assert.isTrue(res.status.calledWithExactly(200)); + assert.isTrue(res.json.called); + + const spyCall = res.json.getCall(0); + assert.isObject(spyCall.args[0]); + assert.sameMembers(Object.keys(spyCall.args[0].response), [ + 'status_code', + ]); + }); +}); diff --git a/test/controllers/PaymentRequest.js b/test/controllers/PaymentRequest.js new file mode 100644 index 0000000..605c900 --- /dev/null +++ b/test/controllers/PaymentRequest.js @@ -0,0 +1,92 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); +const moment = require('moment'); +const uuid = require('node-uuid'); + +const paymentRequest = require('../../server/controllers/PaymentRequest'); +const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); + +let timeStamp = moment().format('YYYYMMDDHHmmss'); +let encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +let params = { + referenceID: uuid.v4(), + merchantTransactionID: uuid.v1(), + amountInDoubleFloat: '100.00', + clientPhoneNumber: '254723001575', + extraPayload: {}, + timeStamp, + encryptedPassword, +}; + +let req = {}; +const res = {}; +let response = { status_code: 200 }; +let promise = new Promise((resolve, reject) => { + resolve(response); +}); + +sinon.stub(promise, 'then', (callback) => { + callback(response); + return promise; +}); + +sinon.stub(promise, 'catch', (callback) => { + callback(new Error('threw an error')); + return promise; +}); + +describe('paymentRequest', () => { + beforeEach(() => { + req.timeStamp = timeStamp; + req.encryptedPassword = encryptedPassword; + req.body = { + totalAmount: '100.00', + phoneNumber: '254723001575', + extraPayload: {}, + }; + + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); + + paymentRequest.parser = sinon.stub().returnsThis(); + paymentRequest.soapRequest.construct = sinon.stub().returnsThis(); + paymentRequest.soapRequest.post = sinon.stub().returns(promise); + }); + + it('BuildSoapBody builds the soap body string', () => { + paymentRequest.buildSoapBody(params); + + assert.isString(paymentRequest.body); + assert.match(paymentRequest.body, /(soapenv:Envelope)/gi); + }); + + it('Makes a SOAP request and returns a promise', () => { + paymentRequest.buildSoapBody = sinon.stub(); + paymentRequest.handler(req, res); + + assert.isTrue(paymentRequest.buildSoapBody.called); + assert.isTrue(paymentRequest.soapRequest.construct.called); + assert.isTrue(paymentRequest.soapRequest.post.called); + + assert.isTrue(promise.then.called); + assert.isTrue(promise.catch.called); + assert.isTrue(res.status.calledWithExactly(200)); + assert.isTrue(res.json.called); + + const spyCall = res.json.getCall(0); + assert.isObject(spyCall.args[0]); + assert.sameMembers(Object.keys(spyCall.args[0].response), [ + 'status_code', + 'reference_id', + 'merchant_transaction_id', + 'amount_in_double_float', + 'client_phone_number', + 'extra_payload', + 'time_stamp', + ]); + }); +}); diff --git a/test/controllers/PaymentStatus.js b/test/controllers/PaymentStatus.js new file mode 100644 index 0000000..38c6f98 --- /dev/null +++ b/test/controllers/PaymentStatus.js @@ -0,0 +1,93 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); +const moment = require('moment'); +const uuid = require('node-uuid'); + +const paymentStatus = require('../../server/controllers/PaymentStatus'); +const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); + +let timeStamp = moment().format('YYYYMMDDHHmmss'); +let encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +let params = { + transactionID: uuid.v1(), + timeStamp, + encryptedPassword, +}; + +let req = {}; +const res = {}; +let response = { status_code: 200 }; +let promise = new Promise((resolve, reject) => { + resolve(response); +}); + +sinon.stub(promise, 'then', (callback) => { + callback(response); + return promise; +}); + +sinon.stub(promise, 'catch', (callback) => { + callback(new Error('threw an error')); + return promise; +}); + +describe('paymentStatus', () => { + beforeEach(() => { + req.timeStamp = timeStamp; + req.encryptedPassword = encryptedPassword; + req.params = { + id: uuid.v1(), + }; + + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); + + paymentStatus.parser = sinon.stub().returnsThis(); + paymentStatus.soapRequest.construct = sinon.stub().returnsThis(); + paymentStatus.soapRequest.post = sinon.stub().returns(promise); + }); + + it('BuildSoapBody builds the soap body string with transactionID', () => { + paymentStatus.buildSoapBody(params); + + assert.isString(paymentStatus.body); + assert.match(paymentStatus.body, /(TRX_ID)/); + assert.notMatch(paymentStatus.body, /(MERCHANT_TRANSACTION_ID)/); + assert.match(paymentStatus.body, /(soapenv:Envelope)/gi); + }); + + it('if transactionID is not provide soap body built with merchantTransactionID', () => { + delete params.transactionID; + params.merchantTransactionID = uuid.v4(); + paymentStatus.buildSoapBody(params); + + assert.isString(paymentStatus.body); + assert.match(paymentStatus.body, /(MERCHANT_TRANSACTION_ID)/); + assert.notMatch(paymentStatus.body, /(TRX_ID)/); + assert.match(paymentStatus.body, /(soapenv:Envelope)/gi); + }); + + it('Makes a SOAP request and returns a promise', () => { + paymentStatus.buildSoapBody = sinon.stub(); + paymentStatus.handler(req, res); + + assert.isTrue(paymentStatus.buildSoapBody.called); + assert.isTrue(paymentStatus.soapRequest.construct.called); + assert.isTrue(paymentStatus.soapRequest.post.called); + + assert.isTrue(promise.then.called); + assert.isTrue(promise.catch.called); + assert.isTrue(res.status.calledWithExactly(200)); + assert.isTrue(res.json.called); + + const spyCall = res.json.getCall(0); + assert.isObject(spyCall.args[0]); + assert.sameMembers(Object.keys(spyCall.args[0].response), [ + 'status_code', + ]); + }); +}); diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js new file mode 100644 index 0000000..04c5055 --- /dev/null +++ b/test/controllers/PaymentSuccess.js @@ -0,0 +1,52 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); +const moment = require('moment'); +const uuid = require('node-uuid'); + +const paymentSuccess = require('../../server/controllers/PaymentSuccess'); + +const req = {}; +req.protocol = 'https'; +req.hostname = 'localhost'; +req.body = { + 'MSISDN': '254723001575', + 'MERCHANT_TRANSACTION_ID': 'FG232FT0', + 'USERNAME': '', + 'PASSWORD': '', + 'AMOUNT': '100', + 'TRX_STATUS': 'Success', + 'RETURN_CODE': '00', + 'DESCRIPTION': 'Transaction successful', + 'M-PESA_TRX_DATE': '2014-08-01 15:30:00', + 'M-PESA_TRX_ID': 'FG232FT0', + 'TRX_ID': '1448', + 'ENC_PARAMS': '{}', +}; +const res = {}; +res.sendStatus = sinon.stub(); + +describe('paymentSuccess', () => { + it('Make a request to MERCHANT_ENDPOINT and respond to SAG with OK', (done) => { + process.env.MERCHANT_ENDPOINT = process.env.ENDPOINT; + paymentSuccess.handler(req, res); + + setTimeout(() => { + assert.isTrue(res.sendStatus.calledWithExactly(200)); + done(); + }, 1500); + }); + + it('If ENDPOINT is not reachable, an error reponse is sent back', (done) => { + delete process.env.MERCHANT_ENDPOINT; + paymentSuccess.handler(req, res); + + setTimeout(() => { + assert.isTrue(res.sendStatus.calledWithExactly(500)); + done(); + }, 1000); + }); +}); diff --git a/test/controllers/SOAPRequest.js b/test/controllers/SOAPRequest.js new file mode 100644 index 0000000..bf38f09 --- /dev/null +++ b/test/controllers/SOAPRequest.js @@ -0,0 +1,86 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); +const moment = require('moment'); +const uuid = require('node-uuid'); + +const SOAPRequest = require('../../server/controllers/SOAPRequest'); +const ParseResponse = require('../../server/utils/ParseResponse'); +const paymentRequest = require('../../server/controllers/PaymentRequest'); +const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); + +const timeStamp = moment().format('YYYYMMDDHHmmss'); +const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +const paymentDetails = { + referenceID: uuid.v4(), + merchantTransactionID: uuid.v1(), + amountInDoubleFloat: '100.00', + clientPhoneNumber: '254723001575', + extraPayload: {}, + timeStamp, + encryptedPassword, +}; + +const parser = new ParseResponse('processcheckoutresponse'); +parser.parse = sinon.stub().returns(parser); +parser.toJSON = sinon.stub(); +parser.toJSON.onFirstCall().returns({ status_code: 200 }); +parser.toJSON.onSecondCall().returns({ status_code: 400 }); + +const soapRequest = new SOAPRequest(); +paymentRequest.buildSoapBody(paymentDetails); +soapRequest.construct(paymentRequest, parser); + + +describe('SOAPRequest', () => { + it('SOAPRequest is contructed', () => { + assert.instanceOf(soapRequest.parser, ParseResponse); + assert.sameMembers(Object.keys(soapRequest.requestOptions), [ + 'method', + 'uri', + 'rejectUnauthorized', + 'body', + 'headers', + ]); + }); + + it('Invokes then method from a successful response', (done) => { + const request = soapRequest.post().then((response) => { + assert.instanceOf(request, Promise); + assert.isObject(response); + assert.sameMembers(Object.keys(response), ['status_code']); + assert.isTrue(soapRequest.parser.parse.called); + assert.isTrue(soapRequest.parser.toJSON.called); + done(); + }); + }); + + it('Invokes catch method from an unsuccessful response', (done) => { + const request = soapRequest.post().catch((error) => { + assert.instanceOf(request, Promise); + assert.isObject(error); + assert.sameMembers(Object.keys(error), ['status_code']); + assert.isTrue(soapRequest.parser.parse.called); + assert.isTrue(soapRequest.parser.toJSON.called); + done(); + }); + }); + + + it('Invokes catch method if an error is returned on invalid request', (done) => { + process.env.ENDPOINT = 'undefined'; + soapRequest.construct(paymentRequest, parser); + + const request = soapRequest.post().catch((error) => { + assert.instanceOf(request, Promise); + assert.isObject(error); + assert.sameMembers(Object.keys(error), ['description']); + assert.isTrue(soapRequest.parser.parse.called); + assert.isTrue(soapRequest.parser.toJSON.called); + done(); + }); + }); +}); diff --git a/test/validators/checkForRequiredParams.js b/test/validators/checkForRequiredParams.js new file mode 100644 index 0000000..dfd7c78 --- /dev/null +++ b/test/validators/checkForRequiredParams.js @@ -0,0 +1,103 @@ +'use strict'; + +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); + +const checkForRequiredParams = require('../../server/validators/checkForRequiredParams'); + +const res = {}; +const req = {}; +let next = sinon.stub(); + +describe('checkForRequiredParams', () => { + beforeEach(() => { + res.status = sinon.stub().returns(res); + res.send = sinon.stub(); + next = sinon.stub(); + }); + + it('Throws an error if phone number is not provided', () => { + req.body = {}; + checkForRequiredParams(req, res, next); + + assert.isTrue(res.status.calledWithExactly(400)); + assert.isTrue(res.send.calledOnce); + }); + + it('Throws an error if phone number is not valid', () => { + req.body = { + phoneNumber: '0723001575', + }; + checkForRequiredParams(req, res, next); + const spyCall = res.send.getCall(0); + + assert.isTrue(res.status.calledWithExactly(400)); + assert.isTrue(res.send.calledOnce); + assert.isString(spyCall.args[0], 'called with string'); + }); + + it('Throws an error if total amount is not provided', () => { + req.body = { + phoneNumber: '254723001575', + }; + checkForRequiredParams(req, res, next); + const spyCall = res.send.getCall(0); + + assert.isTrue(res.status.calledWithExactly(400)); + assert.isTrue(res.send.calledOnce); + assert.isString(spyCall.args[0], 'called with string'); + }); + + it('Throws an error if total amount is not provided', () => { + req.body = { + phoneNumber: '254723001575', + totalAmount: 'a hundred bob', + }; + checkForRequiredParams(req, res, next); + const spyCall = res.send.getCall(0); + + assert.isTrue(res.status.calledWithExactly(400)); + assert.isTrue(res.send.calledOnce); + assert.isString(spyCall.args[0], 'called with string'); + }); + + it('Converts a whole number into a number with double floating points', () => { + req.body = { + phoneNumber: '254723001575', + totalAmount: '100', + }; + checkForRequiredParams(req, res, next); + const spyCall = res.send.getCall(0); + + assert.equal(res.status.callCount, 0); + assert.equal(res.send.callCount, 0); + assert.isNumber(parseInt(req.body.totalAmount, 100), 'should be 100.00'); + }); + + it('Next is returned if everything is valid', () => { + req.body = { + phoneNumber: '254723001575', + totalAmount: '100.00', + }; + checkForRequiredParams(req, res, next); + + assert.equal(res.status.callCount, 0); + assert.equal(res.send.callCount, 0); + assert.isTrue(next.calledOnce); + assert.isDefined(req.body.extraPayload); + }); + + it('Other params are moved into extraPayload property', () => { + req.body = { + phoneNumber: '254723001575', + totalAmount: '100.00', + userID: 1515, + location: 'Kilimani', + }; + checkForRequiredParams(req, res, next); + + assert.isDefined(req.body.extraPayload); + assert.sameMembers(Object.keys(req.body.extraPayload), ['userID', 'location']); + }); +}); From 69f43f82b621483834326f7cf31d567eeec9be20 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 14:17:19 +0300 Subject: [PATCH 61/96] Fixed all eslint issues that popped up after tests --- server/controllers/PaymentRequest.js | 2 ++ server/routes/index.js | 14 +++++++++----- test/config/index.js | 1 - test/controllers/ConfirmPayment.js | 12 ++++++------ test/controllers/PaymentRequest.js | 12 ++++++------ test/controllers/PaymentStatus.js | 12 ++++++------ test/controllers/PaymentSuccess.js | 22 ++++++++++------------ test/utils/errors/reponseError.js | 3 +-- test/validators/checkForRequiredParams.js | 1 - 9 files changed, 40 insertions(+), 39 deletions(-) diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index c06fdce..000fb54 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -38,6 +38,8 @@ class PaymentRequest { `; + + return this; } handler(req, res) { diff --git a/server/routes/index.js b/server/routes/index.js index 833e71f..bac16f6 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -4,17 +4,21 @@ const PaymentRequest = require('../controllers/PaymentRequest'); const ConfirmPayment = require('../controllers/ConfirmPayment'); const PaymentStatus = require('../controllers/PaymentStatus'); const PaymentSuccess = require('../controllers/PaymentSuccess'); -const requiredParams = require('../validators/checkForRequiredParams'); +const checkForRequiredParams = require('../validators/checkForRequiredParams'); module.exports = (router) => { // check the status of the API system router.get('/status', (req, res) => res.json({ status: 200 })); - router.post('/payment/request', requiredParams, PaymentRequest.handler); - router.get('/payment/confirm/:id', ConfirmPayment.handler); - router.get('/payment/status/:id', PaymentStatus.handler); - router.all('/payment/success', PaymentSuccess.handler); + router.post( + '/payment/request', + checkForRequiredParams, + (req, res) => PaymentRequest.handler(req, res) + ); + router.get('/payment/confirm/:id', (req, res) => ConfirmPayment.handler(req, res)); + router.get('/payment/status/:id', (req, res) => PaymentStatus.handler(req, res)); + router.all('/payment/success', (req, res) => PaymentSuccess.handler(req, res)); // for testing last POST response // if MERCHANT_ENDPOINT has not been provided diff --git a/test/config/index.js b/test/config/index.js index ad90738..6a6dcbd 100644 --- a/test/config/index.js +++ b/test/config/index.js @@ -3,7 +3,6 @@ require('../../environment'); const chai = require('chai'); const assert = chai.assert; -const sinon = require('sinon'); const configSetup = require('../../server/config'); diff --git a/test/controllers/ConfirmPayment.js b/test/controllers/ConfirmPayment.js index fce579d..eb19d43 100644 --- a/test/controllers/ConfirmPayment.js +++ b/test/controllers/ConfirmPayment.js @@ -10,18 +10,18 @@ const uuid = require('node-uuid'); const confirmPayment = require('../../server/controllers/ConfirmPayment'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -let timeStamp = moment().format('YYYYMMDDHHmmss'); -let encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -let params = { +const timeStamp = moment().format('YYYYMMDDHHmmss'); +const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +const params = { transactionID: uuid.v1(), timeStamp, encryptedPassword, }; -let req = {}; +const req = {}; const res = {}; -let response = { status_code: 200 }; -let promise = new Promise((resolve, reject) => { +const response = { status_code: 200 }; +const promise = new Promise((resolve) => { resolve(response); }); diff --git a/test/controllers/PaymentRequest.js b/test/controllers/PaymentRequest.js index 605c900..de70123 100644 --- a/test/controllers/PaymentRequest.js +++ b/test/controllers/PaymentRequest.js @@ -10,9 +10,9 @@ const uuid = require('node-uuid'); const paymentRequest = require('../../server/controllers/PaymentRequest'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -let timeStamp = moment().format('YYYYMMDDHHmmss'); -let encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -let params = { +const timeStamp = moment().format('YYYYMMDDHHmmss'); +const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +const params = { referenceID: uuid.v4(), merchantTransactionID: uuid.v1(), amountInDoubleFloat: '100.00', @@ -22,10 +22,10 @@ let params = { encryptedPassword, }; -let req = {}; +const req = {}; const res = {}; -let response = { status_code: 200 }; -let promise = new Promise((resolve, reject) => { +const response = { status_code: 200 }; +const promise = new Promise((resolve) => { resolve(response); }); diff --git a/test/controllers/PaymentStatus.js b/test/controllers/PaymentStatus.js index 38c6f98..2a3c98f 100644 --- a/test/controllers/PaymentStatus.js +++ b/test/controllers/PaymentStatus.js @@ -10,18 +10,18 @@ const uuid = require('node-uuid'); const paymentStatus = require('../../server/controllers/PaymentStatus'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -let timeStamp = moment().format('YYYYMMDDHHmmss'); -let encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -let params = { +const timeStamp = moment().format('YYYYMMDDHHmmss'); +const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; +const params = { transactionID: uuid.v1(), timeStamp, encryptedPassword, }; -let req = {}; +const req = {}; const res = {}; -let response = { status_code: 200 }; -let promise = new Promise((resolve, reject) => { +const response = { status_code: 200 }; +const promise = new Promise((resolve) => { resolve(response); }); diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js index 04c5055..fc6bdc2 100644 --- a/test/controllers/PaymentSuccess.js +++ b/test/controllers/PaymentSuccess.js @@ -4,8 +4,6 @@ require('../../environment'); const chai = require('chai'); const assert = chai.assert; const sinon = require('sinon'); -const moment = require('moment'); -const uuid = require('node-uuid'); const paymentSuccess = require('../../server/controllers/PaymentSuccess'); @@ -13,18 +11,18 @@ const req = {}; req.protocol = 'https'; req.hostname = 'localhost'; req.body = { - 'MSISDN': '254723001575', - 'MERCHANT_TRANSACTION_ID': 'FG232FT0', - 'USERNAME': '', - 'PASSWORD': '', - 'AMOUNT': '100', - 'TRX_STATUS': 'Success', - 'RETURN_CODE': '00', - 'DESCRIPTION': 'Transaction successful', + MSISDN: '254723001575', + MERCHANT_TRANSACTION_ID: 'FG232FT0', + USERNAME: '', + PASSWORD: '', + AMOUNT: '100', + TRX_STATUS: 'Success', + RETURN_CODE: '00', + DESCRIPTION: 'Transaction successful', 'M-PESA_TRX_DATE': '2014-08-01 15:30:00', 'M-PESA_TRX_ID': 'FG232FT0', - 'TRX_ID': '1448', - 'ENC_PARAMS': '{}', + TRX_ID: '1448', + ENC_PARAMS: '{}', }; const res = {}; res.sendStatus = sinon.stub(); diff --git a/test/utils/errors/reponseError.js b/test/utils/errors/reponseError.js index 73ab934..7aed0c2 100644 --- a/test/utils/errors/reponseError.js +++ b/test/utils/errors/reponseError.js @@ -2,7 +2,6 @@ const chai = require('chai'); const assert = chai.assert; -const expect = chai.expect; const sinon = require('sinon'); const responseError = require('../../../server/utils/errors/responseError'); @@ -34,7 +33,7 @@ describe('responseError', () => { it('Calls response method with custom error code', () => { error = { description: 'Bad request', - status_code: 400 + status_code: 400, }; responseError(error, res); spyCall = res.status.getCall(0); diff --git a/test/validators/checkForRequiredParams.js b/test/validators/checkForRequiredParams.js index dfd7c78..ed8b9f7 100644 --- a/test/validators/checkForRequiredParams.js +++ b/test/validators/checkForRequiredParams.js @@ -68,7 +68,6 @@ describe('checkForRequiredParams', () => { totalAmount: '100', }; checkForRequiredParams(req, res, next); - const spyCall = res.send.getCall(0); assert.equal(res.status.callCount, 0); assert.equal(res.send.callCount, 0); From 1dfcbc3f1f748506684781d7e90cf758fd82e5a0 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 14:37:23 +0300 Subject: [PATCH 62/96] Fixed missing properties from the responses after the 1st request --- server/controllers/ConfirmPayment.js | 2 ++ server/controllers/PaymentStatus.js | 2 ++ server/utils/ParseResponse.js | 5 ++--- test/utils/ParseResponse.js | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/controllers/ConfirmPayment.js b/server/controllers/ConfirmPayment.js index de53b87..56cdff4 100644 --- a/server/controllers/ConfirmPayment.js +++ b/server/controllers/ConfirmPayment.js @@ -32,6 +32,8 @@ class ConfirmPayment { `; + + return this; } handler(req, res) { diff --git a/server/controllers/PaymentStatus.js b/server/controllers/PaymentStatus.js index e8eb330..496acb8 100644 --- a/server/controllers/PaymentStatus.js +++ b/server/controllers/PaymentStatus.js @@ -32,6 +32,8 @@ class PaymentStatus { `; + + return this; } handler(req, res) { diff --git a/server/utils/ParseResponse.js b/server/utils/ParseResponse.js index 925a80a..86cebe5 100644 --- a/server/utils/ParseResponse.js +++ b/server/utils/ParseResponse.js @@ -1,6 +1,5 @@ 'use strict'; const cheerio = require('cheerio'); -const _ = require('lodash'); const statusCodes = require('../config/statusCodes'); @@ -54,11 +53,11 @@ module.exports = class ParseResponse { delete this.json.enc_params; // Get the equivalent HTTP CODE to respond with - this.json = _.assignIn(this.extractCode(), this.json); + this.json = Object.assign({}, this.extractCode(), this.json); return this.json; } extractCode() { - return _.find(statusCodes, (o) => o.return_code === parseInt(this.json.return_code, 10)); + return statusCodes.find(sts => sts.return_code === parseInt(this.json.return_code, 10)); } }; diff --git a/test/utils/ParseResponse.js b/test/utils/ParseResponse.js index 6ba5ace..a2c7e01 100644 --- a/test/utils/ParseResponse.js +++ b/test/utils/ParseResponse.js @@ -76,12 +76,14 @@ describe('ParseResponse', () => { assert.isObject(parser.json, 'JSON was extracted'); assert.sameMembers(Object.keys(parser.json), [ + 'return_code', + 'status_code', + 'message', 'msisdn', 'amount', 'm-pesa_trx_date', 'm-pesa_trx_id', 'trx_status', - 'return_code', 'description', 'merchant_transaction_id', 'trx_id', From 6ed0515d2a82a76440e0f054c6c85396ee4fb117 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 22:12:03 +0300 Subject: [PATCH 63/96] A couple of optimisations done - Finished issue #24 - removed unrequired dependencies - mocked ProcessSuccess and SOAPRequest controllers post requests - removed option of using default environment provided TEST phonenumber and amount, use POSTMAN or CURL now - Updated the node required to version to v4.3.2 or higher --- package.json | 3 +-- server/controllers/PaymentRequest.js | 11 +++++------ test/controllers/PaymentSuccess.js | 10 ++++++++-- test/controllers/SOAPRequest.js | 16 +++++++++++++--- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 09a2044..04652b6 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "express": "^4.13.4", "express-session": "^1.13.0", "jade": "~1.11.0", - "lodash": "^4.12.0", "moment": "^2.13.0", "morgan": "~1.6.1", "node-uuid": "^1.4.7", @@ -56,6 +55,6 @@ "sinon": "^1.17.4" }, "engines": { - "node": ">=4.2.0" + "node": ">=4.3.2" } } diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index 000fb54..5c4f8af 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -25,13 +25,12 @@ class PaymentRequest { - ${data.merchantTransactionID} + ${String(data.merchantTransactionID).slice(0, 8)} + ${data.referenceID} ${data.amountInDoubleFloat} ${data.clientPhoneNumber} - - ${data.extraPayload ? JSON.stringify(data.extraPayload) : ''} - + ${JSON.stringify(data.extraPayload)} ${process.env.CALLBACK_URL} ${process.env.CALLBACK_METHOD} ${data.timeStamp} @@ -48,8 +47,8 @@ class PaymentRequest { referenceID: (req.body.referenceID || uuid.v4()), // product, service or order ID merchantTransactionID: (req.body.merchantTransactionID || uuid.v1()), - amountInDoubleFloat: (req.body.totalAmount || process.env.TEST_AMOUNT), - clientPhoneNumber: (req.body.phoneNumber || process.env.TEST_PHONENUMBER), + amountInDoubleFloat: req.body.totalAmount, + clientPhoneNumber: req.body.phoneNumber, extraPayload: req.body.extraPayload, timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword, diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js index fc6bdc2..f07e706 100644 --- a/test/controllers/PaymentSuccess.js +++ b/test/controllers/PaymentSuccess.js @@ -27,6 +27,11 @@ req.body = { const res = {}; res.sendStatus = sinon.stub(); +let error = false; +sinon.stub(paymentSuccess, 'request', (params, callback) => { + callback(error); +}); + describe('paymentSuccess', () => { it('Make a request to MERCHANT_ENDPOINT and respond to SAG with OK', (done) => { process.env.MERCHANT_ENDPOINT = process.env.ENDPOINT; @@ -35,16 +40,17 @@ describe('paymentSuccess', () => { setTimeout(() => { assert.isTrue(res.sendStatus.calledWithExactly(200)); done(); - }, 1500); + }, 20); }); it('If ENDPOINT is not reachable, an error reponse is sent back', (done) => { delete process.env.MERCHANT_ENDPOINT; + error = new Error('ENDPOINT not reachable'); paymentSuccess.handler(req, res); setTimeout(() => { assert.isTrue(res.sendStatus.calledWithExactly(500)); done(); - }, 1000); + }, 20); }); }); diff --git a/test/controllers/SOAPRequest.js b/test/controllers/SOAPRequest.js index bf38f09..a951408 100644 --- a/test/controllers/SOAPRequest.js +++ b/test/controllers/SOAPRequest.js @@ -24,7 +24,7 @@ const paymentDetails = { encryptedPassword, }; -const parser = new ParseResponse('processcheckoutresponse'); +const parser = new ParseResponse('bodyTagName'); parser.parse = sinon.stub().returns(parser); parser.toJSON = sinon.stub(); parser.toJSON.onFirstCall().returns({ status_code: 200 }); @@ -34,8 +34,16 @@ const soapRequest = new SOAPRequest(); paymentRequest.buildSoapBody(paymentDetails); soapRequest.construct(paymentRequest, parser); +let requestError = undefined; +sinon.stub(soapRequest, 'request', (params, callback) => { + callback(requestError, null, 'a soap dom tree string'); +}); describe('SOAPRequest', () => { + afterEach(() => { + requestError = undefined; + }); + it('SOAPRequest is contructed', () => { assert.instanceOf(soapRequest.parser, ParseResponse); assert.sameMembers(Object.keys(soapRequest.requestOptions), [ @@ -54,6 +62,7 @@ describe('SOAPRequest', () => { assert.sameMembers(Object.keys(response), ['status_code']); assert.isTrue(soapRequest.parser.parse.called); assert.isTrue(soapRequest.parser.toJSON.called); + assert.isTrue(soapRequest.request.called); done(); }); }); @@ -65,14 +74,14 @@ describe('SOAPRequest', () => { assert.sameMembers(Object.keys(error), ['status_code']); assert.isTrue(soapRequest.parser.parse.called); assert.isTrue(soapRequest.parser.toJSON.called); + assert.isTrue(soapRequest.request.called); done(); }); }); it('Invokes catch method if an error is returned on invalid request', (done) => { - process.env.ENDPOINT = 'undefined'; - soapRequest.construct(paymentRequest, parser); + requestError = new Error('invalid URI provided'); const request = soapRequest.post().catch((error) => { assert.instanceOf(request, Promise); @@ -80,6 +89,7 @@ describe('SOAPRequest', () => { assert.sameMembers(Object.keys(error), ['description']); assert.isTrue(soapRequest.parser.parse.called); assert.isTrue(soapRequest.parser.toJSON.called); + assert.isTrue(soapRequest.request.called); done(); }); }); From a3efe0a5d09bdb9dac593d5037b471d1d82b1a38 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 23:18:15 +0300 Subject: [PATCH 64/96] Fixed #24 Should be referenceID not merchantTransactionID --- server/controllers/PaymentRequest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index 5c4f8af..e4f852c 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -25,9 +25,9 @@ class PaymentRequest { - ${String(data.merchantTransactionID).slice(0, 8)} + ${data.merchantTransactionID} - ${data.referenceID} + ${String(data.referenceID).slice(0, 8)} ${data.amountInDoubleFloat} ${data.clientPhoneNumber} ${JSON.stringify(data.extraPayload)} From 7444ccb14c566f09bbe265c0e4bbc7dc5cda9353 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 23:47:00 +0300 Subject: [PATCH 65/96] Updated and fixed grammatical issues on readme --- README.md | 64 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 75cde6f..a44f754 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ [![Coverage Status](https://coveralls.io/repos/github/kn9ts/project-mulla/badge.svg?branch=master)](https://coveralls.io/github/kn9ts/project-mulla?branch=master) -[![Build Status](https://semaphoreci.com/api/v1/kn9ts/project-mulla/branches/develop/badge.svg)](https://semaphoreci.com/kn9ts/project-mulla) +[![Build Status](https://semaphoreci.com/api/v1/kn9ts/project-mulla/branches/master/badge.svg)](https://semaphoreci.com/kn9ts/project-mulla) ![](http://cdn.javascript.co.ke/images/banner.png) > **What MPESA G2 API should have been in the 21st century.** +> **PLEASE NOTE: RESTifys C2B portion only for now.** + **MPESA API RESTful mediator**. Basically converts all merchant requests to the dreaded ancient SOAP/XML requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. In short, it'll deal with all of the SOAP shenanigans while you REST. -The aim of **Project Mulla**, is to create a REST API that interfaces with the **ugly MPESA G2 API.** +The aim of **Project Mulla** is to create a REST API that interfaces with the **ugly MPESA G2 API.** ### Yes We Know! SOAP! Yuck! @@ -19,7 +21,11 @@ Developers should not go through the **trauma** involved with dealing with SOAP/ # Example of how it works -Once **Project Mulla** is set up, up and running in whichever clould platform you prefer(we recommend `Heroku.com`). Your 1st request once your customer/client has consumed your services or purchasing products from you is to innitiate a payment request. +Once **Project Mulla** is set up in whichever cloud platform you prefer(we recommend [Heroku.com](https://heroku.com) or [Google App Engine](https://cloud.google.com/appengine/)) it is ready to mediate your MPESA G2 API requests. + +Let's go on ahead and make the 1st call, **ProcessCheckoutRequest**. Basically, this is telling the SAG to initialise a payment request you want to transact. After initialisation, you can on ahead and tell SAG to go on and do the actual transaction. + +Ok! Now you just have to make a **POST request to Project Mulla**. _Not Safaricom_. Project Mulla's mission, remember!!! See below of the request hueristics: ##### Initiate Payment Request: @@ -27,7 +33,7 @@ _Method_: **`POST`** _Endpoint_: **`https://awesome-service.com/api/v1/payment/request`** -_Parameters_: +_Body Parameters_: - **`phoneNumber`** - The phone number of your client - **`totalAmount`** - The total amount you are charging the client - **`referenceID`** - The reference ID of the order or service **[optional]** @@ -36,7 +42,7 @@ _Parameters_: __NOTE:__ If `merchantTransactionID` or `referenceID` are not provided a time-based and random UUID is generated for each respectively. -_Response:_ +_The response you get:_ ```http HTTP/1.1 200 OK @@ -49,20 +55,20 @@ X-Powered-By: Express set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly { - "response": { - "amount_in_double_float": "450.00", - "client_phone_number": "254723001575", - "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", - "description": "success", - "extra_payload": {}, - "status_code": 200, - "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", - "message": "Transaction carried successfully", - "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", - "return_code": "00", - "time_stamp": "20160522161208", - "trx_id": "45a3f4b64cde9d88440211187f73944b" - } + "response": { + "return_code": "00", + "status_code": 200, + "message": "Transaction carried successfully", + "trx_id": "453c70c4b2434bd94bcbafb17518dc8e", + "description": "success", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "reference_id": "3e3beff0-fc05-417a-bbf2-190ee19a5e58", + "merchant_transaction_id": "95d64500-2514-11e6-bcb8-a7f8e1c786c4", + "amount_in_double_float": "450.00", + "client_phone_number": "254723001575", + "extra_payload": {}, + "time_stamp": "20160528234142" + } } ``` @@ -74,7 +80,7 @@ You will need to install some stuff, if they are not yet in your machine: ##### Majors: -* **Node.js (v4.4.4 LTS)** - [Click here](http://nodejs.org) to install +* **Node.js (v4.3.2 or higher; LTS)** - [Click here](http://nodejs.org) to install ##### Secondaries(click for further information): @@ -120,15 +126,13 @@ It should look like the example below, only with your specific config values: API_VERSION = 1 HOST = localhost PORT = 3000 -EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' -WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' -PAYBILL_NUMBER = '898998' -PASSKEY = 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' +EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' // for security purposes +WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' // this too +PAYBILL_NUMBER = '123456' +PASSKEY = 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' // this a pure giberish string, don't bother ENDPOINT = 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' CALLBACK_URL = 'http://awesome-service.com/mpesa/confirm-checkout.php' CALLBACK_METHOD = 'POST' -TEST_PHONENUMBER = '0720000000' -TEST_AMOUNT = '10.00' ``` __The `PAYBILL_NUMBER` and `PASSKEY` are provided by Safaricom once you have registered for the MPESA G2 API.__ @@ -204,14 +208,14 @@ set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly **_TL;DR_*** Here's what the license entails: ```markdown -1. Anyone can copy, modify and distrubute this software. +1. Anyone can copy, modify and distribute this software. 2. You have to include the license and copyright notice with each and every distribution. 3. You can use this software privately. -4. You can use this sofware for commercial purposes. +4. You can use this software for commercial purposes. 5. If you dare build your business solely from this code, you risk open-sourcing the whole code base. -6. If you modifiy it, you have to indicate changes made to the code. +6. If you modify it, you have to indicate changes made to the code. 7. Any modifications of this code base MUST be distributed with the same license, GPLv3. -8. This sofware is provided without warranty. +8. This software is provided without warranty. 9. The software author or license can not be held liable for any damages inflicted by the software. ``` From d23c3a532444832b3ffd088c71ed0d241561e67b Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 28 May 2016 23:47:00 +0300 Subject: [PATCH 66/96] Updated and fixed grammatical issues on readme --- README.md | 64 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 75cde6f..a44f754 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ [![Coverage Status](https://coveralls.io/repos/github/kn9ts/project-mulla/badge.svg?branch=master)](https://coveralls.io/github/kn9ts/project-mulla?branch=master) -[![Build Status](https://semaphoreci.com/api/v1/kn9ts/project-mulla/branches/develop/badge.svg)](https://semaphoreci.com/kn9ts/project-mulla) +[![Build Status](https://semaphoreci.com/api/v1/kn9ts/project-mulla/branches/master/badge.svg)](https://semaphoreci.com/kn9ts/project-mulla) ![](http://cdn.javascript.co.ke/images/banner.png) > **What MPESA G2 API should have been in the 21st century.** +> **PLEASE NOTE: RESTifys C2B portion only for now.** + **MPESA API RESTful mediator**. Basically converts all merchant requests to the dreaded ancient SOAP/XML requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. Responding to the merchant via a beautiful and soothing 21st century REST API. In short, it'll deal with all of the SOAP shenanigans while you REST. -The aim of **Project Mulla**, is to create a REST API that interfaces with the **ugly MPESA G2 API.** +The aim of **Project Mulla** is to create a REST API that interfaces with the **ugly MPESA G2 API.** ### Yes We Know! SOAP! Yuck! @@ -19,7 +21,11 @@ Developers should not go through the **trauma** involved with dealing with SOAP/ # Example of how it works -Once **Project Mulla** is set up, up and running in whichever clould platform you prefer(we recommend `Heroku.com`). Your 1st request once your customer/client has consumed your services or purchasing products from you is to innitiate a payment request. +Once **Project Mulla** is set up in whichever cloud platform you prefer(we recommend [Heroku.com](https://heroku.com) or [Google App Engine](https://cloud.google.com/appengine/)) it is ready to mediate your MPESA G2 API requests. + +Let's go on ahead and make the 1st call, **ProcessCheckoutRequest**. Basically, this is telling the SAG to initialise a payment request you want to transact. After initialisation, you can on ahead and tell SAG to go on and do the actual transaction. + +Ok! Now you just have to make a **POST request to Project Mulla**. _Not Safaricom_. Project Mulla's mission, remember!!! See below of the request hueristics: ##### Initiate Payment Request: @@ -27,7 +33,7 @@ _Method_: **`POST`** _Endpoint_: **`https://awesome-service.com/api/v1/payment/request`** -_Parameters_: +_Body Parameters_: - **`phoneNumber`** - The phone number of your client - **`totalAmount`** - The total amount you are charging the client - **`referenceID`** - The reference ID of the order or service **[optional]** @@ -36,7 +42,7 @@ _Parameters_: __NOTE:__ If `merchantTransactionID` or `referenceID` are not provided a time-based and random UUID is generated for each respectively. -_Response:_ +_The response you get:_ ```http HTTP/1.1 200 OK @@ -49,20 +55,20 @@ X-Powered-By: Express set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly { - "response": { - "amount_in_double_float": "450.00", - "client_phone_number": "254723001575", - "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", - "description": "success", - "extra_payload": {}, - "status_code": 200, - "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", - "message": "Transaction carried successfully", - "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", - "return_code": "00", - "time_stamp": "20160522161208", - "trx_id": "45a3f4b64cde9d88440211187f73944b" - } + "response": { + "return_code": "00", + "status_code": 200, + "message": "Transaction carried successfully", + "trx_id": "453c70c4b2434bd94bcbafb17518dc8e", + "description": "success", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "reference_id": "3e3beff0-fc05-417a-bbf2-190ee19a5e58", + "merchant_transaction_id": "95d64500-2514-11e6-bcb8-a7f8e1c786c4", + "amount_in_double_float": "450.00", + "client_phone_number": "254723001575", + "extra_payload": {}, + "time_stamp": "20160528234142" + } } ``` @@ -74,7 +80,7 @@ You will need to install some stuff, if they are not yet in your machine: ##### Majors: -* **Node.js (v4.4.4 LTS)** - [Click here](http://nodejs.org) to install +* **Node.js (v4.3.2 or higher; LTS)** - [Click here](http://nodejs.org) to install ##### Secondaries(click for further information): @@ -120,15 +126,13 @@ It should look like the example below, only with your specific config values: API_VERSION = 1 HOST = localhost PORT = 3000 -EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' -WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' -PAYBILL_NUMBER = '898998' -PASSKEY = 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' +EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' // for security purposes +WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' // this too +PAYBILL_NUMBER = '123456' +PASSKEY = 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' // this a pure giberish string, don't bother ENDPOINT = 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' CALLBACK_URL = 'http://awesome-service.com/mpesa/confirm-checkout.php' CALLBACK_METHOD = 'POST' -TEST_PHONENUMBER = '0720000000' -TEST_AMOUNT = '10.00' ``` __The `PAYBILL_NUMBER` and `PASSKEY` are provided by Safaricom once you have registered for the MPESA G2 API.__ @@ -204,14 +208,14 @@ set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly **_TL;DR_*** Here's what the license entails: ```markdown -1. Anyone can copy, modify and distrubute this software. +1. Anyone can copy, modify and distribute this software. 2. You have to include the license and copyright notice with each and every distribution. 3. You can use this software privately. -4. You can use this sofware for commercial purposes. +4. You can use this software for commercial purposes. 5. If you dare build your business solely from this code, you risk open-sourcing the whole code base. -6. If you modifiy it, you have to indicate changes made to the code. +6. If you modify it, you have to indicate changes made to the code. 7. Any modifications of this code base MUST be distributed with the same license, GPLv3. -8. This sofware is provided without warranty. +8. This software is provided without warranty. 9. The software author or license can not be held liable for any damages inflicted by the software. ``` From c57cc1dc7656239b5d8047cefccac4fbf20e753f Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 29 May 2016 07:25:01 +0300 Subject: [PATCH 67/96] Done minor refactors --- .coveralls.yml | 2 +- .hound.yml | 2 -- README.md | 53 +++++++++++++++++++++++++++----------------------- environment.js | 1 + 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/.coveralls.yml b/.coveralls.yml index 10ad240..28f81bd 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -repo_token: ryNIFOAZ1HF1firLwwslUDfvsjy3RofHM +repo_token: EI2vRz1HRhJ3pGi7g3J6sMxI4dsnrWxtb diff --git a/.hound.yml b/.hound.yml index bbeeb8b..3be1f25 100644 --- a/.hound.yml +++ b/.hound.yml @@ -1,5 +1,3 @@ -javascript: - enabled: false eslint: enabled: true config_file: .eslintrc diff --git a/README.md b/README.md index a44f754..71da810 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,20 @@ Developers should not go through the **trauma** involved with dealing with SOAP/ Once **Project Mulla** is set up in whichever cloud platform you prefer(we recommend [Heroku.com](https://heroku.com) or [Google App Engine](https://cloud.google.com/appengine/)) it is ready to mediate your MPESA G2 API requests. -Let's go on ahead and make the 1st call, **ProcessCheckoutRequest**. Basically, this is telling the SAG to initialise a payment request you want to transact. After initialisation, you can on ahead and tell SAG to go on and do the actual transaction. +Let's go ahead and make the 1st call, **ProcessCheckoutRequest**. Basically, this is telling the SAG to initialise a payment request you want to transact. After initialisation, you then confirm with SAG via another POST request to do the actual payment/transaction. -Ok! Now you just have to make a **POST request to Project Mulla**. _Not Safaricom_. Project Mulla's mission, remember!!! See below of the request hueristics: +Make the initialisation request by making a **POST request to Project Mulla**. _Not Safaricom_. Project Mulla's mission, remember!!! + +See below on how you'd make the 1st request: ##### Initiate Payment Request: _Method_: **`POST`** -_Endpoint_: **`https://awesome-service.com/api/v1/payment/request`** +_Endpoint_: **`https://your-project-mulla-endpoint.herokuapp.com/api/v1/payment/request`** _Body Parameters_: + - **`phoneNumber`** - The phone number of your client - **`totalAmount`** - The total amount you are charging the client - **`referenceID`** - The reference ID of the order or service **[optional]** @@ -53,7 +56,9 @@ Date: Sat, 21 May 2016 10:03:37 GMT ETag: W/"1fe-jy66YehfhiFHWoyTNHpSnA" X-Powered-By: Express set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly +``` +```json { "response": { "return_code": "00", @@ -76,7 +81,7 @@ set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7Gd ## Dependencies -You will need to install some stuff, if they are not yet in your machine: +You will need to install some stuff, if they are not yet installed in your machine: ##### Majors: @@ -88,7 +93,7 @@ You will need to install some stuff, if they are not yet in your machine: You may need to update it to the latest version: -``` +```bash $ npm update -g npm ``` @@ -152,7 +157,7 @@ Express server listening on 3000, in development mode #### Do a test run -You can make a test run using **CURL**: +Now make a test run using **CURL**: ```bash $ curl -i -X POST \ @@ -173,7 +178,7 @@ $ http POST localhost:3000/api/v1/payment/request \ clientLocation='Kilimani' ``` -You should expect back a similar structured **response** as follows: +Once the request is executed, your console should print a similar structured **response** as below: ```http HTTP/1.1 200 OK @@ -186,24 +191,24 @@ X-Powered-By: Express set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly { - "response": { - "amount_in_double_float": "450.00", - "client_phone_number": "254723001575", - "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", - "description": "success", - "extra_payload": {}, - "status_code": 200, - "merchant_transaction_id": "c9bcf350-201e-11e6-a676-5984a015f2fd", - "message": "Transaction carried successfully", - "reference_id": "7d2c8f65-1228-4e6c-9b67-bb3b825c8441", - "return_code": "00", - "time_stamp": "20160522161208", - "trx_id": "45a3f4b64cde9d88440211187f73944b" - } + "response": { + "return_code": "00", + "status_code": 200, + "message": "Transaction carried successfully", + "trx_id": "453c70c4b2434bd94bcbafb17518dc8e", + "description": "success", + "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", + "reference_id": "3e3beff0-fc05-417a-bbf2-190ee19a5e58", + "merchant_transaction_id": "95d64500-2514-11e6-bcb8-a7f8e1c786c4", + "amount_in_double_float": "450.00", + "client_phone_number": "254723001575", + "extra_payload": {}, + "time_stamp": "20160528234142" + } } ``` -# This project uses GPL3 LICENSE +# This project uses GPLv3 LICENSE **_TL;DR_*** Here's what the license entails: @@ -219,6 +224,6 @@ set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly 9. The software author or license can not be held liable for any damages inflicted by the software. ``` -More information on about the [LICENSE can be found here](http://choosealicense.com/licenses/gpl-3.0/) +More information on the [LICENSE can be found here](http://choosealicense.com/licenses/gpl-3.0/) -**_PLEASE NOTE:_** All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with.* +**_DISCLAIMER:_** _All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with._ diff --git a/environment.js b/environment.js index 96f2c73..82fac9d 100644 --- a/environment.js +++ b/environment.js @@ -1,4 +1,5 @@ 'use strict'; + const dotenv = require('dotenv'); if ((process.env.NODE_ENV || 'development') === 'development') { From 0062b4c23fb7a22fd4cad02798a050231a0e0803 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 30 May 2016 07:26:24 +0300 Subject: [PATCH 68/96] Mocked the actual request in PaymentSuccess test --- test/controllers/PaymentSuccess.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js index f07e706..bf77826 100644 --- a/test/controllers/PaymentSuccess.js +++ b/test/controllers/PaymentSuccess.js @@ -3,6 +3,7 @@ require('../../environment'); const chai = require('chai'); const assert = chai.assert; +const expect = chai.expect; const sinon = require('sinon'); const paymentSuccess = require('../../server/controllers/PaymentSuccess'); @@ -27,30 +28,35 @@ req.body = { const res = {}; res.sendStatus = sinon.stub(); +const response = {}; +for (const x of Object.keys(req.body)) { + const prop = x.toLowerCase().replace(/\-/g, ''); + response[prop] = req.body[x]; +} + let error = false; sinon.stub(paymentSuccess, 'request', (params, callback) => { callback(error); }); describe('paymentSuccess', () => { - it('Make a request to MERCHANT_ENDPOINT and respond to SAG with OK', (done) => { + it('Make a request to MERCHANT_ENDPOINT and respond to SAG with OK', () => { process.env.MERCHANT_ENDPOINT = process.env.ENDPOINT; paymentSuccess.handler(req, res); - setTimeout(() => { - assert.isTrue(res.sendStatus.calledWithExactly(200)); - done(); - }, 20); + const spyCall = paymentSuccess.request.getCall(0); + const args = spyCall.args[0]; + + assert.isTrue(res.sendStatus.calledWithExactly(200)); + assert.isTrue(paymentSuccess.request.called); + expect(response).to.deep.equal(JSON.parse(args.body)); }); - it('If ENDPOINT is not reachable, an error reponse is sent back', (done) => { + it('If ENDPOINT is not reachable, an error reponse is sent back', () => { delete process.env.MERCHANT_ENDPOINT; error = new Error('ENDPOINT not reachable'); paymentSuccess.handler(req, res); - setTimeout(() => { - assert.isTrue(res.sendStatus.calledWithExactly(500)); - done(); - }, 20); + assert.isTrue(res.sendStatus.calledWithExactly(500)); }); }); From d1c4a6dad52309a8fc7800d4d32475b6d81371f0 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 31 May 2016 07:51:58 +0300 Subject: [PATCH 69/96] Refactor tests moving initial configs inside describe to scope them --- test/controllers/ConfirmPayment.js | 72 ++++++++++----------- test/controllers/PaymentRequest.js | 78 +++++++++++------------ test/controllers/PaymentStatus.js | 72 ++++++++++----------- test/controllers/PaymentSuccess.js | 62 +++++++++--------- test/controllers/SOAPRequest.js | 48 +++++++------- test/utils/errors/reponseError.js | 9 +-- test/validators/checkForRequiredParams.js | 8 +-- 7 files changed, 172 insertions(+), 177 deletions(-) diff --git a/test/controllers/ConfirmPayment.js b/test/controllers/ConfirmPayment.js index eb19d43..eee7eb0 100644 --- a/test/controllers/ConfirmPayment.js +++ b/test/controllers/ConfirmPayment.js @@ -10,47 +10,45 @@ const uuid = require('node-uuid'); const confirmPayment = require('../../server/controllers/ConfirmPayment'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -const timeStamp = moment().format('YYYYMMDDHHmmss'); -const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -const params = { - transactionID: uuid.v1(), - timeStamp, - encryptedPassword, -}; - -const req = {}; -const res = {}; -const response = { status_code: 200 }; -const promise = new Promise((resolve) => { - resolve(response); -}); - -sinon.stub(promise, 'then', (callback) => { - callback(response); - return promise; -}); +describe('confirmPayment', () => { + const timeStamp = moment().format('YYYYMMDDHHmmss'); + const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; + const params = { + transactionID: uuid.v1(), + timeStamp, + encryptedPassword, + }; + + const req = {}; + req.timeStamp = timeStamp; + req.encryptedPassword = encryptedPassword; + req.params = { + id: uuid.v1(), + }; + + const res = {}; + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); + + const response = { status_code: 200 }; + const promise = new Promise((resolve) => { + resolve(response); + }); -sinon.stub(promise, 'catch', (callback) => { - callback(new Error('threw an error')); - return promise; -}); + sinon.stub(promise, 'then', (callback) => { + callback(response); + return promise; + }); -describe('confirmPayment', () => { - beforeEach(() => { - req.timeStamp = timeStamp; - req.encryptedPassword = encryptedPassword; - req.params = { - id: uuid.v1(), - }; - - res.status = sinon.stub().returns(res); - res.json = sinon.stub(); - - confirmPayment.parser = sinon.stub().returnsThis(); - confirmPayment.soapRequest.construct = sinon.stub().returnsThis(); - confirmPayment.soapRequest.post = sinon.stub().returns(promise); + sinon.stub(promise, 'catch', (callback) => { + callback(new Error('threw an error')); + return promise; }); + confirmPayment.parser = sinon.stub().returnsThis(); + confirmPayment.soapRequest.construct = sinon.stub().returnsThis(); + confirmPayment.soapRequest.post = sinon.stub().returns(promise); + it('BuildSoapBody builds the soap body string with transactionID', () => { confirmPayment.buildSoapBody(params); diff --git a/test/controllers/PaymentRequest.js b/test/controllers/PaymentRequest.js index de70123..8110f1b 100644 --- a/test/controllers/PaymentRequest.js +++ b/test/controllers/PaymentRequest.js @@ -10,53 +10,51 @@ const uuid = require('node-uuid'); const paymentRequest = require('../../server/controllers/PaymentRequest'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -const timeStamp = moment().format('YYYYMMDDHHmmss'); -const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -const params = { - referenceID: uuid.v4(), - merchantTransactionID: uuid.v1(), - amountInDoubleFloat: '100.00', - clientPhoneNumber: '254723001575', - extraPayload: {}, - timeStamp, - encryptedPassword, -}; +describe('paymentRequest', () => { + const timeStamp = moment().format('YYYYMMDDHHmmss'); + const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; + const params = { + referenceID: uuid.v4(), + merchantTransactionID: uuid.v1(), + amountInDoubleFloat: '100.00', + clientPhoneNumber: '254723001575', + extraPayload: {}, + timeStamp, + encryptedPassword, + }; -const req = {}; -const res = {}; -const response = { status_code: 200 }; -const promise = new Promise((resolve) => { - resolve(response); -}); + const req = {}; + req.timeStamp = timeStamp; + req.encryptedPassword = encryptedPassword; + req.body = { + totalAmount: '100.00', + phoneNumber: '254723001575', + extraPayload: {}, + }; -sinon.stub(promise, 'then', (callback) => { - callback(response); - return promise; -}); - -sinon.stub(promise, 'catch', (callback) => { - callback(new Error('threw an error')); - return promise; -}); + const res = {}; + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); -describe('paymentRequest', () => { - beforeEach(() => { - req.timeStamp = timeStamp; - req.encryptedPassword = encryptedPassword; - req.body = { - totalAmount: '100.00', - phoneNumber: '254723001575', - extraPayload: {}, - }; + const response = { status_code: 200 }; + const promise = new Promise((resolve) => { + resolve(response); + }); - res.status = sinon.stub().returns(res); - res.json = sinon.stub(); + sinon.stub(promise, 'then', (callback) => { + callback(response); + return promise; + }); - paymentRequest.parser = sinon.stub().returnsThis(); - paymentRequest.soapRequest.construct = sinon.stub().returnsThis(); - paymentRequest.soapRequest.post = sinon.stub().returns(promise); + sinon.stub(promise, 'catch', (callback) => { + callback(new Error('threw an error')); + return promise; }); + paymentRequest.parser = sinon.stub().returnsThis(); + paymentRequest.soapRequest.construct = sinon.stub().returnsThis(); + paymentRequest.soapRequest.post = sinon.stub().returns(promise); + it('BuildSoapBody builds the soap body string', () => { paymentRequest.buildSoapBody(params); diff --git a/test/controllers/PaymentStatus.js b/test/controllers/PaymentStatus.js index 2a3c98f..7f2a35f 100644 --- a/test/controllers/PaymentStatus.js +++ b/test/controllers/PaymentStatus.js @@ -10,47 +10,45 @@ const uuid = require('node-uuid'); const paymentStatus = require('../../server/controllers/PaymentStatus'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -const timeStamp = moment().format('YYYYMMDDHHmmss'); -const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -const params = { - transactionID: uuid.v1(), - timeStamp, - encryptedPassword, -}; - -const req = {}; -const res = {}; -const response = { status_code: 200 }; -const promise = new Promise((resolve) => { - resolve(response); -}); - -sinon.stub(promise, 'then', (callback) => { - callback(response); - return promise; -}); +describe('paymentStatus', () => { + const timeStamp = moment().format('YYYYMMDDHHmmss'); + const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; + const params = { + transactionID: uuid.v1(), + timeStamp, + encryptedPassword, + }; + + const req = {}; + req.timeStamp = timeStamp; + req.encryptedPassword = encryptedPassword; + req.params = { + id: uuid.v1(), + }; + + const res = {}; + res.status = sinon.stub().returns(res); + res.json = sinon.stub(); + + const response = { status_code: 200 }; + const promise = new Promise((resolve) => { + resolve(response); + }); -sinon.stub(promise, 'catch', (callback) => { - callback(new Error('threw an error')); - return promise; -}); + sinon.stub(promise, 'then', (callback) => { + callback(response); + return promise; + }); -describe('paymentStatus', () => { - beforeEach(() => { - req.timeStamp = timeStamp; - req.encryptedPassword = encryptedPassword; - req.params = { - id: uuid.v1(), - }; - - res.status = sinon.stub().returns(res); - res.json = sinon.stub(); - - paymentStatus.parser = sinon.stub().returnsThis(); - paymentStatus.soapRequest.construct = sinon.stub().returnsThis(); - paymentStatus.soapRequest.post = sinon.stub().returns(promise); + sinon.stub(promise, 'catch', (callback) => { + callback(new Error('threw an error')); + return promise; }); + paymentStatus.parser = sinon.stub().returnsThis(); + paymentStatus.soapRequest.construct = sinon.stub().returnsThis(); + paymentStatus.soapRequest.post = sinon.stub().returns(promise); + it('BuildSoapBody builds the soap body string with transactionID', () => { paymentStatus.buildSoapBody(params); diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js index bf77826..b54545e 100644 --- a/test/controllers/PaymentSuccess.js +++ b/test/controllers/PaymentSuccess.js @@ -8,38 +8,38 @@ const sinon = require('sinon'); const paymentSuccess = require('../../server/controllers/PaymentSuccess'); -const req = {}; -req.protocol = 'https'; -req.hostname = 'localhost'; -req.body = { - MSISDN: '254723001575', - MERCHANT_TRANSACTION_ID: 'FG232FT0', - USERNAME: '', - PASSWORD: '', - AMOUNT: '100', - TRX_STATUS: 'Success', - RETURN_CODE: '00', - DESCRIPTION: 'Transaction successful', - 'M-PESA_TRX_DATE': '2014-08-01 15:30:00', - 'M-PESA_TRX_ID': 'FG232FT0', - TRX_ID: '1448', - ENC_PARAMS: '{}', -}; -const res = {}; -res.sendStatus = sinon.stub(); - -const response = {}; -for (const x of Object.keys(req.body)) { - const prop = x.toLowerCase().replace(/\-/g, ''); - response[prop] = req.body[x]; -} - -let error = false; -sinon.stub(paymentSuccess, 'request', (params, callback) => { - callback(error); -}); - describe('paymentSuccess', () => { + const req = {}; + req.protocol = 'https'; + req.hostname = 'localhost'; + req.body = { + MSISDN: '254723001575', + MERCHANT_TRANSACTION_ID: 'FG232FT0', + USERNAME: '', + PASSWORD: '', + AMOUNT: '100', + TRX_STATUS: 'Success', + RETURN_CODE: '00', + DESCRIPTION: 'Transaction successful', + 'M-PESA_TRX_DATE': '2014-08-01 15:30:00', + 'M-PESA_TRX_ID': 'FG232FT0', + TRX_ID: '1448', + ENC_PARAMS: '{}', + }; + const res = {}; + res.sendStatus = sinon.stub(); + + const response = {}; + for (const x of Object.keys(req.body)) { + const prop = x.toLowerCase().replace(/\-/g, ''); + response[prop] = req.body[x]; + } + + let error = false; + sinon.stub(paymentSuccess, 'request', (params, callback) => { + callback(error); + }); + it('Make a request to MERCHANT_ENDPOINT and respond to SAG with OK', () => { process.env.MERCHANT_ENDPOINT = process.env.ENDPOINT; paymentSuccess.handler(req, res); diff --git a/test/controllers/SOAPRequest.js b/test/controllers/SOAPRequest.js index a951408..a63a53b 100644 --- a/test/controllers/SOAPRequest.js +++ b/test/controllers/SOAPRequest.js @@ -12,34 +12,34 @@ const ParseResponse = require('../../server/utils/ParseResponse'); const paymentRequest = require('../../server/controllers/PaymentRequest'); const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); -const timeStamp = moment().format('YYYYMMDDHHmmss'); -const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; -const paymentDetails = { - referenceID: uuid.v4(), - merchantTransactionID: uuid.v1(), - amountInDoubleFloat: '100.00', - clientPhoneNumber: '254723001575', - extraPayload: {}, - timeStamp, - encryptedPassword, -}; +describe('SOAPRequest', () => { + const timeStamp = moment().format('YYYYMMDDHHmmss'); + const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; + const paymentDetails = { + referenceID: uuid.v4(), + merchantTransactionID: uuid.v1(), + amountInDoubleFloat: '100.00', + clientPhoneNumber: '254723001575', + extraPayload: {}, + timeStamp, + encryptedPassword, + }; -const parser = new ParseResponse('bodyTagName'); -parser.parse = sinon.stub().returns(parser); -parser.toJSON = sinon.stub(); -parser.toJSON.onFirstCall().returns({ status_code: 200 }); -parser.toJSON.onSecondCall().returns({ status_code: 400 }); + const parser = new ParseResponse('bodyTagName'); + parser.parse = sinon.stub().returns(parser); + parser.toJSON = sinon.stub(); + parser.toJSON.onFirstCall().returns({ status_code: 200 }); + parser.toJSON.onSecondCall().returns({ status_code: 400 }); -const soapRequest = new SOAPRequest(); -paymentRequest.buildSoapBody(paymentDetails); -soapRequest.construct(paymentRequest, parser); + const soapRequest = new SOAPRequest(); + paymentRequest.buildSoapBody(paymentDetails); + soapRequest.construct(paymentRequest, parser); -let requestError = undefined; -sinon.stub(soapRequest, 'request', (params, callback) => { - callback(requestError, null, 'a soap dom tree string'); -}); + let requestError = undefined; + sinon.stub(soapRequest, 'request', (params, callback) => { + callback(requestError, null, 'a soap dom tree string'); + }); -describe('SOAPRequest', () => { afterEach(() => { requestError = undefined; }); diff --git a/test/utils/errors/reponseError.js b/test/utils/errors/reponseError.js index 7aed0c2..7df4785 100644 --- a/test/utils/errors/reponseError.js +++ b/test/utils/errors/reponseError.js @@ -6,14 +6,15 @@ const sinon = require('sinon'); const responseError = require('../../../server/utils/errors/responseError'); -let spyCall; -const res = {}; -let error = 'An error message'; - describe('responseError', () => { + let spyCall; + const res = {}; + let error = 'An error message'; + beforeEach(() => { res.status = sinon.stub().returns(res); res.json = sinon.stub(); + responseError(error, res); }); diff --git a/test/validators/checkForRequiredParams.js b/test/validators/checkForRequiredParams.js index ed8b9f7..f1af80d 100644 --- a/test/validators/checkForRequiredParams.js +++ b/test/validators/checkForRequiredParams.js @@ -6,11 +6,11 @@ const sinon = require('sinon'); const checkForRequiredParams = require('../../server/validators/checkForRequiredParams'); -const res = {}; -const req = {}; -let next = sinon.stub(); - describe('checkForRequiredParams', () => { + const res = {}; + const req = {}; + let next = sinon.stub(); + beforeEach(() => { res.status = sinon.stub().returns(res); res.send = sinon.stub(); From 03fa245b15dd7c80dc45c7af96fd89fc49b5095d Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Tue, 31 May 2016 08:42:13 +0300 Subject: [PATCH 70/96] Move Checkout.wsdl to docs/ --- Checkout.wsdl => docs/Checkout.wsdl | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Checkout.wsdl => docs/Checkout.wsdl (100%) diff --git a/Checkout.wsdl b/docs/Checkout.wsdl similarity index 100% rename from Checkout.wsdl rename to docs/Checkout.wsdl From cab4ca26510fd741c73b3e710e301b17118f92fe Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Wed, 1 Jun 2016 11:07:06 +0300 Subject: [PATCH 71/96] Ensure npm version is 3.5 or higher --- .babelrc | 3 --- .bowerrc | 3 --- package.json | 3 ++- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .babelrc delete mode 100755 .bowerrc diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 4897f9d..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["stage-0", "es2015"] -} diff --git a/.bowerrc b/.bowerrc deleted file mode 100755 index 7c609d3..0000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "public/vendor" -} diff --git a/package.json b/package.json index 04652b6..532171d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "sinon": "^1.17.4" }, "engines": { - "node": ">=4.3.2" + "node": ">=4.3.2", + "npm": ">=3.5" } } From 7f62209dcd9702ffbc22211fd05fe676b4ce1dbe Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Thu, 2 Jun 2016 11:10:11 +0300 Subject: [PATCH 72/96] Serve app on port 8080 and not 3000 --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 7301df9..84ac93f 100644 --- a/index.js +++ b/index.js @@ -70,7 +70,7 @@ app.use((err, req, res) => { }); }); -const server = app.listen(process.env.PORT || 3000, () => { +const server = app.listen(process.env.PORT || 8080, () => { console.log('Express server listening on %d, in %s' + ' mode', server.address().port, app.get('env')); }); From 6969ead8509d4283f969777647591b1dc49150b1 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 3 Jun 2016 11:24:47 +0300 Subject: [PATCH 73/96] Remove 'host' from application config vars --- server/config/index.js | 1 - test/config/index.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/config/index.js b/server/config/index.js index 7aa6be0..89e2c22 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -2,7 +2,6 @@ module.exports = (value) => { const envVariables = { - host: process.env.HOST, expressSessionKey: process.env.EXPRESS_SESSION_KEY, }; diff --git a/test/config/index.js b/test/config/index.js index 6a6dcbd..2cd8e33 100644 --- a/test/config/index.js +++ b/test/config/index.js @@ -10,12 +10,12 @@ describe('Config: index.js', () => { it('returns a default config object if one is provided', () => { const config = configSetup('staging'); assert.isObject(config); - assert.sameMembers(Object.keys(config), ['host', 'expressSessionKey']); + assert.sameMembers(Object.keys(config), ['expressSessionKey']); }); it('returns a configuration object if it exists', () => { const config = configSetup(process.env.NODE_ENV); assert.isObject(config); - assert.sameMembers(Object.keys(config), ['host', 'expressSessionKey']); + assert.sameMembers(Object.keys(config), ['expressSessionKey']); }); }); From a40e57d27799f1e50a5c57fc5250c6793316525f Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Fri, 3 Jun 2016 11:35:45 +0300 Subject: [PATCH 74/96] Move coveralls env vars to gulp task file --- gulpfile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index c1398da..cf12bdb 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,6 +1,7 @@ 'use strict'; require('./environment'); +const os = require('os'); const gulp = require('gulp'); const mocha = require('gulp-mocha'); const istanbul = require('gulp-istanbul'); @@ -8,6 +9,9 @@ const coveralls = require('gulp-coveralls'); const eslint = require('gulp-eslint'); const runSequence = require('run-sequence'); +process.env.COVERALLS_SERVICE_NAME = `${os.hostname()}.${os.platform()}-${os.release()}`; +process.env.COVERALLS_REPO_TOKEN = 'EI2vRz1HRhJ3pGi7g3J6sMxI4dsnrWxtb'; + const filesToLint = [ 'gulpfile.js', 'index.js', From 0ff1877f033f97b4db45d5699f5120bdbeb3c864 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 4 Jun 2016 11:36:56 +0300 Subject: [PATCH 75/96] Enable proxy support when deployed to GAE --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 84ac93f..c7781eb 100644 --- a/index.js +++ b/index.js @@ -14,11 +14,13 @@ const routes = require('./server/routes'); const genTransactionPassword = require('./server/utils/genTransactionPassword'); const apiVersion = process.env.API_VERSION; - // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); +// trust proxy if it's being served in GoogleAppEngine +if ('GAE_APPENGINE_HOSTNAME' in process.env) app.set('trust_proxy', 1); + // Uncomment this for Morgan to intercept all Error instantiations // For now, they churned out via a JSON response app.use(morgan('dev')); From 74663fa1b374a6ca8896f2cbc010e624dfc2ca9e Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 4 Jun 2016 15:29:15 +0300 Subject: [PATCH 76/96] Refactors and optimisation: - Replaced dotenv loader with js-yaml loader - Added support to easily deploy to Google app engine (needs documentation) - YAML file should now be used instead of .env file to load environment vars - Add environment.js to linting task - Deleted/removed unrequired/unused ENV VARS and their references - config/index.js --- .gitignore | 1 + environment.js | 18 ++++++++++++++---- gulpfile.js | 1 + index.js | 4 +--- package.json | 3 ++- server/config/index.js | 14 -------------- test/config/index.js | 21 --------------------- test/environment.js | 20 ++++++++++++++++++++ 8 files changed, 39 insertions(+), 43 deletions(-) delete mode 100644 server/config/index.js delete mode 100644 test/config/index.js create mode 100644 test/environment.js diff --git a/.gitignore b/.gitignore index fa08a36..956d3c7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ server/config/local.env.js npm-debug.log .sass-cache _site +app.yaml ### Added by loopback framework *.csv diff --git a/environment.js b/environment.js index 82fac9d..76586ca 100644 --- a/environment.js +++ b/environment.js @@ -1,8 +1,18 @@ 'use strict'; -const dotenv = require('dotenv'); +const yaml = require('js-yaml'); +const fs = require('fs'); -if ((process.env.NODE_ENV || 'development') === 'development') { - // load the applications environment - dotenv.load(); +// default configuration +process.env.API_VERSION = 1; + +// if an env has not been provided, default to development +if (!('NODE_ENV' in process.env)) process.env.NODE_ENV = 'development'; + +if (process.env.NODE_ENV === 'development') { + // Get the rest of the config from app.yaml config file + const config = yaml.safeLoad(fs.readFileSync('app.yaml', 'utf8')); + Object.keys(config.env_variables).forEach(key => { + process.env[key] = config.env_variables[key]; + }); } diff --git a/gulpfile.js b/gulpfile.js index cf12bdb..f254386 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,6 +15,7 @@ process.env.COVERALLS_REPO_TOKEN = 'EI2vRz1HRhJ3pGi7g3J6sMxI4dsnrWxtb'; const filesToLint = [ 'gulpfile.js', 'index.js', + 'environment.js', './server/**/*.js', '!node_modules/**', ]; diff --git a/index.js b/index.js index c7781eb..759cca6 100644 --- a/index.js +++ b/index.js @@ -4,8 +4,6 @@ require('./environment'); const express = require('express'); const app = express(); const path = require('path'); -const config = require('./server/config')(process.env.NODE_ENV); -// const favicon = require('serve-favicon'); const morgan = require('morgan'); const cookieParser = require('cookie-parser'); const bodyParser = require('body-parser'); @@ -35,7 +33,7 @@ app.use(express.static(path.join(__dirname, './server/public'))); // memory based session app.use(session({ - secret: config.expressSessionKey, + secret: process.env.SESSION_SECRET_KEY, resave: false, saveUninitialized: true, })); diff --git a/package.json b/package.json index 532171d..3023465 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "gulp test", "develop": "nodemon -w ./server --exec npm start", "lint": "eslint --fix ./server ./test", + "monitor": "nodemon app.js", "start": "node index.js" }, "repository": { @@ -25,10 +26,10 @@ "cheerio": "^0.20.0", "cookie-parser": "~1.3.5", "debug": "~2.2.0", - "dotenv": "^2.0.0", "express": "^4.13.4", "express-session": "^1.13.0", "jade": "~1.11.0", + "js-yaml": "^3.6.1", "moment": "^2.13.0", "morgan": "~1.6.1", "node-uuid": "^1.4.7", diff --git a/server/config/index.js b/server/config/index.js deleted file mode 100644 index 89e2c22..0000000 --- a/server/config/index.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -module.exports = (value) => { - const envVariables = { - expressSessionKey: process.env.EXPRESS_SESSION_KEY, - }; - - const environments = { - development: envVariables, - staging: envVariables, - production: envVariables, - }; - return environments[value] ? environments[value] : environments.development; -}; diff --git a/test/config/index.js b/test/config/index.js deleted file mode 100644 index 2cd8e33..0000000 --- a/test/config/index.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -require('../../environment'); -const chai = require('chai'); -const assert = chai.assert; - -const configSetup = require('../../server/config'); - -describe('Config: index.js', () => { - it('returns a default config object if one is provided', () => { - const config = configSetup('staging'); - assert.isObject(config); - assert.sameMembers(Object.keys(config), ['expressSessionKey']); - }); - - it('returns a configuration object if it exists', () => { - const config = configSetup(process.env.NODE_ENV); - assert.isObject(config); - assert.sameMembers(Object.keys(config), ['expressSessionKey']); - }); -}); diff --git a/test/environment.js b/test/environment.js new file mode 100644 index 0000000..0051c1b --- /dev/null +++ b/test/environment.js @@ -0,0 +1,20 @@ +'use strict'; + +require('../environment'); +const chai = require('chai'); +const expect = chai.expect; + +describe('environment.js', () => { + it('Should load default environment vars if environment stage is not defined', () => { + // console.log(Object.keys(process.env)); + expect(Object.keys(process.env)).to.include.members([ + 'API_VERSION', + 'SESSION_SECRET_KEY', + 'PAYBILL_NUMBER', + 'PASSKEY', + 'ENDPOINT', + 'CALLBACK_URL', + 'CALLBACK_METHOD', + ]); + }); +}); From 6e1546e4f99f4f4972103b844ee1c5e8ec66b962 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 4 Jun 2016 16:46:45 +0300 Subject: [PATCH 77/96] only set the COVERALLS_SERVICE_NAME env if one has not been already defined --- gulpfile.js | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index f254386..1757406 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,7 +9,9 @@ const coveralls = require('gulp-coveralls'); const eslint = require('gulp-eslint'); const runSequence = require('run-sequence'); -process.env.COVERALLS_SERVICE_NAME = `${os.hostname()}.${os.platform()}-${os.release()}`; +if (!('COVERALLS_SERVICE_NAME' in process.env)) { + process.env.COVERALLS_SERVICE_NAME = `${os.hostname()}.${os.platform()}-${os.release()}`; +} process.env.COVERALLS_REPO_TOKEN = 'EI2vRz1HRhJ3pGi7g3J6sMxI4dsnrWxtb'; const filesToLint = [ diff --git a/package.json b/package.json index 3023465..d686e1d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "gulp test", "develop": "nodemon -w ./server --exec npm start", "lint": "eslint --fix ./server ./test", - "monitor": "nodemon app.js", + "monitor": "nodemon index.js", "start": "node index.js" }, "repository": { From 846718b4573694ee1059e8ae7993e57b15d2102b Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 4 Jun 2016 18:07:27 +0300 Subject: [PATCH 78/96] updated README --- README.md | 91 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 71da810..ae103c8 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ > **What MPESA G2 API should have been in the 21st century.** -> **PLEASE NOTE: RESTifys C2B portion only for now.** +> **PLEASE NOTE: Mediates only C2B portion for now.** -**MPESA API RESTful mediator**. Basically converts all merchant requests to the dreaded ancient SOAP/XML -requests. It then mediates all communications to and from the Safaricom MPESA gateway frictionlessly. -Responding to the merchant via a beautiful and soothing 21st century REST API. +**MPESA API RESTful mediator**. It transforms all merchant REST requests to the dreaded ancient SOAP/XML +requests. And transforms Safaricom MPESA G2 API gateway SOAP responses to JSON. +Responding back to the merchant via a beautiful and soothing REST API. -In short, it'll deal with all of the SOAP shenanigans while you REST. +In short, it'll deal with all of the SOAP shenanigans while you REST. -The aim of **Project Mulla** is to create a REST API that interfaces with the **ugly MPESA G2 API.** +The aim of **Project Mulla** is to create a REST API middleman that interfaces with the **MPESA G2 API** for you. ### Yes We Know! SOAP! Yuck! @@ -21,13 +21,13 @@ Developers should not go through the **trauma** involved with dealing with SOAP/ # Example of how it works -Once **Project Mulla** is set up in whichever cloud platform you prefer(we recommend [Heroku.com](https://heroku.com) or [Google App Engine](https://cloud.google.com/appengine/)) it is ready to mediate your MPESA G2 API requests. +Let's go ahead and make the 1st call, **ProcessCheckoutRequest**. This is initial step is to tell the SAG to +initialise a payment request you want to transact. After initialisation, you then make another POST request to +the SAG as a confirmation signal to carry out the actual payment/transaction request prior. -Let's go ahead and make the 1st call, **ProcessCheckoutRequest**. Basically, this is telling the SAG to initialise a payment request you want to transact. After initialisation, you then confirm with SAG via another POST request to do the actual payment/transaction. +Assuming **Project Mulla** is now your mediator, you'd now make a **POST request to Project Mulla**. _Not Safaricom_. -Make the initialisation request by making a **POST request to Project Mulla**. _Not Safaricom_. Project Mulla's mission, remember!!! - -See below on how you'd make the 1st request: +See below how you'd make this initial request: ##### Initiate Payment Request: @@ -42,11 +42,12 @@ _Body Parameters_: - **`referenceID`** - The reference ID of the order or service **[optional]** - **`merchantTransactionID`** - This specific order's or service's transaction ID **[optional]** -__NOTE:__ If `merchantTransactionID` or `referenceID` are not provided a time-based and random -UUID is generated for each respectively. +_**NOTE:** If `merchantTransactionID` or `referenceID` are not provided a time-based and random +UUID is generated for each respectively._ _The response you get:_ +_**`HTTP HEADER META DATA`**_ ```http HTTP/1.1 200 OK Connection: keep-alive @@ -58,6 +59,7 @@ X-Powered-By: Express set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly ``` +_**`The JSON response in the BODY`**_ ```json { "response": { @@ -69,7 +71,7 @@ set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7Gd "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", "reference_id": "3e3beff0-fc05-417a-bbf2-190ee19a5e58", "merchant_transaction_id": "95d64500-2514-11e6-bcb8-a7f8e1c786c4", - "amount_in_double_float": "450.00", + "amount_in_double_float": "10.00", "client_phone_number": "254723001575", "extra_payload": {}, "time_stamp": "20160528234142" @@ -77,7 +79,7 @@ set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7Gd } ``` -# Installation +# Installation & Testing ## Dependencies @@ -91,7 +93,7 @@ You will need to install some stuff, if they are not yet installed in your machi * **NPM (v3.5+; bundled with node.js installation package)** -You may need to update it to the latest version: +If already installed you may need to only update it to the latest version: ```bash $ npm update -g npm @@ -99,9 +101,9 @@ $ npm update -g npm ## Getting Started -Once you have **Node.js** installed, run _(type or copy & paste; pick your poison)_: +Once you have **Node.js (and NPM)** installed, run _(type or copy & paste; pick your poison)_: -**To download the boilerplate** +**To clone/download the boilerplate** ```bash $ git clone https://github.com/kn9ts/project-mulla @@ -113,46 +115,53 @@ After cloning, get into your project mulla's directory/folder: $ cd project-mulla ``` -**Install all of the projects dependecies with:** +**Install all of the projects dependencies with:** ```bash $ npm install ``` -**Create .env configurations file** +**Create `app.yaml` configurations file** -The last but not least step is creating a `.env` file with your configurations in the root directory of `project mulla`. +The last but not least step is creating a `app.yaml` file with your configurations in the root directory of `project-mulla`. -Should be in the same location as `index.js` +This is the same folder estate where `index.js` can be found. It should look like the example below, only with your specific config values: -```js -API_VERSION = 1 -HOST = localhost -PORT = 3000 -EXPRESS_SESSION_KEY = '88186735405ab8d59f968ed4dab89da5515' // for security purposes -WEB_TOKEN_SECRET = 'a7f3f061-197f-4d94-bcfc-0fa72fc2d897' // this too -PAYBILL_NUMBER = '123456' -PASSKEY = 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' // this a pure giberish string, don't bother -ENDPOINT = 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' -CALLBACK_URL = 'http://awesome-service.com/mpesa/confirm-checkout.php' -CALLBACK_METHOD = 'POST' +```yaml +env_variables: + SESSION_SECRET_KEY: '88735405ab8d9f968ed4dab89da5515KadjaklJK238adnkLD32' + PAYBILL_NUMBER: '898998' + PASSKEY: 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' + ENDPOINT: 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' + CALLBACK_URL: 'http://awesome-service.com/mpesa/confirm-checkout.php' + CALLBACK_METHOD: 'POST' + +# Everything below from this point onwards are only relevant +# if you are looking to deploy Project Mulla to Google App Engine. +runtime: nodejs +vm: true + +skip_files: + - ^(.*/)?.*/node_modules/.*$ ``` -__The `PAYBILL_NUMBER` and `PASSKEY` are provided by Safaricom once you have registered for the MPESA G2 API.__ +*__PLEASE NOTE:__ The __`PAYBILL_NUMBER`__ and __`PASSKEY`__ are provided by Safaricom once you have registered for the MPESA G2 API.* -*__PLEASE NOTE__: The details above only serve as examples* +*__PLEASE NOTE:__ The details above only serve as examples* #### It's now ready to launch +1st run the command `npm test` in the console and see if everything is all good. Then run: + ```bash $ npm start > project-mulla@0.1.1 start ../project-mulla > node index.js -Express server listening on 3000, in development mode +Express server listening on 8080, in development mode ``` #### Do a test run @@ -161,9 +170,9 @@ Now make a test run using **CURL**: ```bash $ curl -i -X POST \ - --url http://localhost:3000/api/v1/payment/request \ + --url http://localhost:8080/api/v1/payment/request \ --data 'phoneNumber=254723001575' \ - --data 'totalAmount=450.00' \ + --data 'totalAmount=10.00' \ --data 'clientName="Eugene Mutai"' \ --data 'clientLocation=Kilimani' \ ``` @@ -171,9 +180,9 @@ $ curl -i -X POST \ Or if you have [httpie](https://github.com/jkbrzt/httpie) installed: ```bash -$ http POST localhost:3000/api/v1/payment/request \ +$ http POST localhost:8080/api/v1/payment/request \ phoneNumber=254723001575 \ - totalAmount=450.00 \ + totalAmount=10.00 \ clientName='Eugene Mutai' \ clientLocation='Kilimani' ``` @@ -200,7 +209,7 @@ set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", "reference_id": "3e3beff0-fc05-417a-bbf2-190ee19a5e58", "merchant_transaction_id": "95d64500-2514-11e6-bcb8-a7f8e1c786c4", - "amount_in_double_float": "450.00", + "amount_in_double_float": "10.00", "client_phone_number": "254723001575", "extra_payload": {}, "time_stamp": "20160528234142" From 6f22644de46c8d190307e7781e75a23481f16612 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 5 Jun 2016 14:20:34 +0300 Subject: [PATCH 79/96] Add index.js and env.js to nodemon watch list --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d686e1d..3919a67 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "test": "gulp test", - "develop": "nodemon -w ./server --exec npm start", + "develop": "nodemon -w ./server -w index.js -w environment.js --exec npm start", "lint": "eslint --fix ./server ./test", "monitor": "nodemon index.js", "start": "node index.js" From 965d86511d6acc0a4a7d3e54ab4b764cf8658b9b Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 5 Jun 2016 15:08:35 +0300 Subject: [PATCH 80/96] 404 error responses are now JSON --- index.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 759cca6..4234fa0 100644 --- a/index.js +++ b/index.js @@ -47,11 +47,15 @@ const apiRouter = express.Router; app.use(`/api/v${apiVersion}`, routes(apiRouter())); // catch 404 and forward to error handler -app.use((req, res, next) => { +app.use((req, res) => { const err = new Error('Not Found'); - err.request = req.originalUrl; err.status = 404; - next(err); + res.status(err.status).json({ + status: err.status, + request_url: req.originalUrl, + message: err.message, + stack_trace: err.stack.split(/\n/).map(stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim()), + }); }); // error handlers @@ -59,7 +63,7 @@ app.use((err, req, res) => { console.log('ERROR PASSING THROUGH', err.message); // get the error stack const stack = err.stack.split(/\n/) - .map(error => error.replace(/\s{2,}/g, ' ').trim()); + .map(stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim()); // send out the error as json res.status(err.status || 500).json({ @@ -76,4 +80,4 @@ const server = app.listen(process.env.PORT || 8080, () => { }); // expose app -exports.default = app; +module.exports = app; From 3ce6934aa03d79e553d705063edb683cd11b1733 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 5 Jun 2016 15:49:40 +0300 Subject: [PATCH 81/96] 404 and error handlers now send out JSON error responses --- index.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 4234fa0..4a70b5c 100644 --- a/index.js +++ b/index.js @@ -46,31 +46,34 @@ app.use(`/api/v${apiVersion}/payment*`, genTransactionPassword); const apiRouter = express.Router; app.use(`/api/v${apiVersion}`, routes(apiRouter())); +// use this prettify the error stack string into an array of stack traces +const prettifyStackTrace = stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim(); + // catch 404 and forward to error handler app.use((req, res) => { + const notFoundStatusCode = 404; const err = new Error('Not Found'); - err.status = 404; - res.status(err.status).json({ - status: err.status, + const stack = err.stack.split(/\n/).map(prettifyStackTrace); + + // send out the error as json + res.status(notFoundStatusCode).json({ + status_code: notFoundStatusCode, request_url: req.originalUrl, message: err.message, - stack_trace: err.stack.split(/\n/).map(stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim()), + stack_trace: stack, }); }); // error handlers app.use((err, req, res) => { - console.log('ERROR PASSING THROUGH', err.message); - // get the error stack - const stack = err.stack.split(/\n/) - .map(stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim()); + console.log('An error occured: ', err.message); + const stack = err.stack.split(/\n/).map(prettifyStackTrace); - // send out the error as json - res.status(err.status || 500).json({ - api: err, - url: req.originalUrl, - error: err.message, - stack, + res.status(err.statusCode || 500).json({ + status_code: err.statusCode, + request_url: req.originalUrl, + message: err.message, + stack_trace: stack, }); }); From 257b3532d82706d6af9b6c4908f449dc99a3249e Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 5 Jun 2016 16:28:00 +0300 Subject: [PATCH 82/96] Fix my custom error handler not being reached by the 404-error handler --- index.js | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 4a70b5c..9e29d6e 100644 --- a/index.js +++ b/index.js @@ -50,26 +50,19 @@ app.use(`/api/v${apiVersion}`, routes(apiRouter())); const prettifyStackTrace = stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim(); // catch 404 and forward to error handler -app.use((req, res) => { - const notFoundStatusCode = 404; +app.use((req, res, next) => { const err = new Error('Not Found'); - const stack = err.stack.split(/\n/).map(prettifyStackTrace); - - // send out the error as json - res.status(notFoundStatusCode).json({ - status_code: notFoundStatusCode, - request_url: req.originalUrl, - message: err.message, - stack_trace: stack, - }); + err.statusCode = 404; + next(err); }); // error handlers -app.use((err, req, res) => { +app.use((err, req, res, next) => { + if (typeof err === 'undefined') next(); console.log('An error occured: ', err.message); const stack = err.stack.split(/\n/).map(prettifyStackTrace); - res.status(err.statusCode || 500).json({ + return res.status(err.statusCode || 500).json({ status_code: err.statusCode, request_url: req.originalUrl, message: err.message, From 6effd3edabec0e3735f44cf22b72963c25dd891d Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 5 Jun 2016 17:19:31 +0300 Subject: [PATCH 83/96] Lock down the node and npm versions --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3919a67..656ab48 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "sinon": "^1.17.4" }, "engines": { - "node": ">=4.3.2", - "npm": ">=3.5" + "node": "^4.3.2", + "npm": "^3.9.5" } } From 5756953b9a340f5cd0a01e5def37d6ae40e39980 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 5 Jun 2016 17:20:03 +0300 Subject: [PATCH 84/96] Add heroku procfile --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1da0cd6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node index.js From 46bb9be8ac50ef7e55dc32b0f4cee4e537d18b77 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 6 Jun 2016 18:31:31 +0300 Subject: [PATCH 85/96] Finished #40 Hard code the ENPOINT and SECRET_SESSION_KEY into the app --- environment.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/environment.js b/environment.js index 76586ca..253188b 100644 --- a/environment.js +++ b/environment.js @@ -1,10 +1,13 @@ 'use strict'; - -const yaml = require('js-yaml'); const fs = require('fs'); +const yaml = require('js-yaml'); +const uuid = require('node-uuid'); // default configuration process.env.API_VERSION = 1; +process.env.ENDPOINT = 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl'; +process.env.SESSION_SECRET_KEY = uuid.v4(); +console.log('Your secret session key is: ' + process.env.SESSION_SECRET_KEY); // if an env has not been provided, default to development if (!('NODE_ENV' in process.env)) process.env.NODE_ENV = 'development'; From 41e4966381fb5ee71a4005d64d304990003b47cd Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 11 Jun 2016 21:25:01 +0300 Subject: [PATCH 86/96] Throw an error if app.yaml is not provided while on development --- environment.js | 37 +++++++++-- index.js | 1 + server/config/statusCodes.js | 4 ++ server/controllers/ConfirmPayment.js | 2 +- server/controllers/PaymentRequest.js | 2 +- server/controllers/PaymentStatus.js | 2 +- server/controllers/PaymentSuccess.js | 13 ++-- server/controllers/SOAPRequest.js | 45 ------------- test/controllers/PaymentSuccess.js | 21 ++++-- test/controllers/SOAPRequest.js | 96 ---------------------------- test/environment.js | 3 +- 11 files changed, 66 insertions(+), 160 deletions(-) delete mode 100644 server/controllers/SOAPRequest.js delete mode 100644 test/controllers/SOAPRequest.js diff --git a/environment.js b/environment.js index 253188b..2aa1835 100644 --- a/environment.js +++ b/environment.js @@ -3,19 +3,44 @@ const fs = require('fs'); const yaml = require('js-yaml'); const uuid = require('node-uuid'); +const yamlConfigFile = 'app.yaml'; + // default configuration process.env.API_VERSION = 1; process.env.ENDPOINT = 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl'; process.env.SESSION_SECRET_KEY = uuid.v4(); -console.log('Your secret session key is: ' + process.env.SESSION_SECRET_KEY); // if an env has not been provided, default to development if (!('NODE_ENV' in process.env)) process.env.NODE_ENV = 'development'; if (process.env.NODE_ENV === 'development') { - // Get the rest of the config from app.yaml config file - const config = yaml.safeLoad(fs.readFileSync('app.yaml', 'utf8')); - Object.keys(config.env_variables).forEach(key => { - process.env[key] = config.env_variables[key]; - }); + if (fs.existsSync(yamlConfigFile)) { + // Get the rest of the config from app.yaml config file + const config = yaml.safeLoad(fs.readFileSync(yamlConfigFile, 'utf8')); + Object.keys(config.env_variables).forEach(key => { + process.env[key] = config.env_variables[key]; + }); + } else { + throw new Error(` + Missing app.yaml config file used while in development mode + + It should have contents similar to the example below: + + app.yaml + ------------------------- + env_variables: + PAYBILL_NUMBER: '000000' + PASSKEY: 'a8eac82d7ac1461ba0348b0cb24d3f8140d3afb9be864e56a10d7e8026eaed66' + MERCHANT_ENDPOINT: 'http://merchant-endpoint.com/mpesa/payment/complete' + + # Everything below from this point onwards are only relevant + # if you are looking to deploy Project Mulla to Google App Engine. + runtime: nodejs + vm: true + + skip_files: + - ^(.*/)?.*/node_modules/.*$ + ------------------------- + `); + } } diff --git a/index.js b/index.js index 9e29d6e..c5af5b9 100644 --- a/index.js +++ b/index.js @@ -71,6 +71,7 @@ app.use((err, req, res, next) => { }); const server = app.listen(process.env.PORT || 8080, () => { + console.log('Your secret session key is: ' + process.env.SESSION_SECRET_KEY); console.log('Express server listening on %d, in %s' + ' mode', server.address().port, app.get('env')); }); diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index d9dfcb8..5dbd264 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -32,6 +32,10 @@ module.exports = [{ return_code: 41, status_code: 400, message: 'MSISDN(phone number) is in incorrect format', +}, { + return_code: 42, + status_code: 400, + message: 'Your PASSKEY, PAYBILL_NUMBER or environment variables may be incorrect', }, { return_code: 32, status_code: 401, diff --git a/server/controllers/ConfirmPayment.js b/server/controllers/ConfirmPayment.js index 56cdff4..27addc4 100644 --- a/server/controllers/ConfirmPayment.js +++ b/server/controllers/ConfirmPayment.js @@ -1,7 +1,7 @@ 'use strict'; const ParseResponse = require('../utils/ParseResponse'); -const SOAPRequest = require('../controllers/SOAPRequest'); +const SOAPRequest = require('../utils/SOAPRequest'); const responseError = require('../utils/errors/responseError'); const parseResponse = new ParseResponse('transactionconfirmresponse'); diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index e4f852c..de89b1a 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -2,7 +2,7 @@ const uuid = require('node-uuid'); const ParseResponse = require('../utils/ParseResponse'); -const SOAPRequest = require('../controllers/SOAPRequest'); +const SOAPRequest = require('../utils/SOAPRequest'); const responseError = require('../utils/errors/responseError'); const parseResponse = new ParseResponse('processcheckoutresponse'); diff --git a/server/controllers/PaymentStatus.js b/server/controllers/PaymentStatus.js index 496acb8..2b4e0df 100644 --- a/server/controllers/PaymentStatus.js +++ b/server/controllers/PaymentStatus.js @@ -1,7 +1,7 @@ 'use strict'; const ParseResponse = require('../utils/ParseResponse'); -const SOAPRequest = require('../controllers/SOAPRequest'); +const SOAPRequest = require('../utils/SOAPRequest'); const responseError = require('../utils/errors/responseError'); const parseResponse = new ParseResponse('transactionstatusresponse'); diff --git a/server/controllers/PaymentSuccess.js b/server/controllers/PaymentSuccess.js index a338714..2629d7f 100644 --- a/server/controllers/PaymentSuccess.js +++ b/server/controllers/PaymentSuccess.js @@ -7,13 +7,18 @@ class PaymentSuccess { this.request = request; } - handler(req, res) { + handler(req, res, next) { const keys = Object.keys(req.body); 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; + let endpoint = `${baseURL}/api/v1/thumbs/up`; + + if ('MERCHANT_ENDPOINT' in process.env) { + endpoint = process.env.MERCHANT_ENDPOINT; + } else if (process.env.NODE_ENV === 'development') { + next(new Error('MERCHANT_ENDPOINT has not been provided in environment configuration')); + return; + } for (const x of keys) { const prop = x.toLowerCase().replace(/\-/g, ''); diff --git a/server/controllers/SOAPRequest.js b/server/controllers/SOAPRequest.js deleted file mode 100644 index 2a89f3c..0000000 --- a/server/controllers/SOAPRequest.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const request = require('request'); - -module.exports = class SOAPRequest { - construct(payment, parser) { - this.request = request; - this.parser = parser; - this.requestOptions = { - method: 'POST', - uri: process.env.ENDPOINT, - rejectUnauthorized: false, - body: payment.body, - headers: { - 'content-type': 'application/xml; charset=utf-8', - }, - }; - return this; - } - - post() { - return new Promise((resolve, reject) => { - // Make the soap request to the SAG URI - this.request(this.requestOptions, (error, response, body) => { - if (error) { - reject({ description: error.message }); - return; - } - - const parsedResponse = this.parser.parse(body); - const json = parsedResponse.toJSON(); - - // Anything that is not "00" as the - // SOAP response code is a Failure - if (json && json.status_code !== 200) { - reject(json); - return; - } - - // Else everything went well - resolve(json); - }); - }); - } -}; diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js index b54545e..5aff918 100644 --- a/test/controllers/PaymentSuccess.js +++ b/test/controllers/PaymentSuccess.js @@ -28,6 +28,7 @@ describe('paymentSuccess', () => { }; const res = {}; res.sendStatus = sinon.stub(); + const next = sinon.stub(); const response = {}; for (const x of Object.keys(req.body)) { @@ -41,21 +42,33 @@ describe('paymentSuccess', () => { }); it('Make a request to MERCHANT_ENDPOINT and respond to SAG with OK', () => { - process.env.MERCHANT_ENDPOINT = process.env.ENDPOINT; - paymentSuccess.handler(req, res); + process.env.MERCHANT_ENDPOINT = 'https://awesome-service.com/mpesa/callback'; + paymentSuccess.handler(req, res, next); const spyCall = paymentSuccess.request.getCall(0); const args = spyCall.args[0]; assert.isTrue(res.sendStatus.calledWithExactly(200)); assert.isTrue(paymentSuccess.request.called); + assert.isFalse(next.called); expect(response).to.deep.equal(JSON.parse(args.body)); }); - it('If ENDPOINT is not reachable, an error reponse is sent back', () => { + it('If MERCHANT_ENDPOINT is not provided, next is passed an error', () => { delete process.env.MERCHANT_ENDPOINT; + paymentSuccess.handler(req, res, next); + + const spyCall = next.getCall(0); + const args = spyCall.args[0]; + + assert.isTrue(next.called); + assert.isTrue(args instanceof Error); + }); + + it('If ENDPOINT is not reachable, an error reponse is sent back', () => { + process.env.MERCHANT_ENDPOINT = 'https://undefined-url'; error = new Error('ENDPOINT not reachable'); - paymentSuccess.handler(req, res); + paymentSuccess.handler(req, res, next); assert.isTrue(res.sendStatus.calledWithExactly(500)); }); diff --git a/test/controllers/SOAPRequest.js b/test/controllers/SOAPRequest.js deleted file mode 100644 index a63a53b..0000000 --- a/test/controllers/SOAPRequest.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict'; - -require('../../environment'); -const chai = require('chai'); -const assert = chai.assert; -const sinon = require('sinon'); -const moment = require('moment'); -const uuid = require('node-uuid'); - -const SOAPRequest = require('../../server/controllers/SOAPRequest'); -const ParseResponse = require('../../server/utils/ParseResponse'); -const paymentRequest = require('../../server/controllers/PaymentRequest'); -const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); - -describe('SOAPRequest', () => { - const timeStamp = moment().format('YYYYMMDDHHmmss'); - const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; - const paymentDetails = { - referenceID: uuid.v4(), - merchantTransactionID: uuid.v1(), - amountInDoubleFloat: '100.00', - clientPhoneNumber: '254723001575', - extraPayload: {}, - timeStamp, - encryptedPassword, - }; - - const parser = new ParseResponse('bodyTagName'); - parser.parse = sinon.stub().returns(parser); - parser.toJSON = sinon.stub(); - parser.toJSON.onFirstCall().returns({ status_code: 200 }); - parser.toJSON.onSecondCall().returns({ status_code: 400 }); - - const soapRequest = new SOAPRequest(); - paymentRequest.buildSoapBody(paymentDetails); - soapRequest.construct(paymentRequest, parser); - - let requestError = undefined; - sinon.stub(soapRequest, 'request', (params, callback) => { - callback(requestError, null, 'a soap dom tree string'); - }); - - afterEach(() => { - requestError = undefined; - }); - - it('SOAPRequest is contructed', () => { - assert.instanceOf(soapRequest.parser, ParseResponse); - assert.sameMembers(Object.keys(soapRequest.requestOptions), [ - 'method', - 'uri', - 'rejectUnauthorized', - 'body', - 'headers', - ]); - }); - - it('Invokes then method from a successful response', (done) => { - const request = soapRequest.post().then((response) => { - assert.instanceOf(request, Promise); - assert.isObject(response); - assert.sameMembers(Object.keys(response), ['status_code']); - assert.isTrue(soapRequest.parser.parse.called); - assert.isTrue(soapRequest.parser.toJSON.called); - assert.isTrue(soapRequest.request.called); - done(); - }); - }); - - it('Invokes catch method from an unsuccessful response', (done) => { - const request = soapRequest.post().catch((error) => { - assert.instanceOf(request, Promise); - assert.isObject(error); - assert.sameMembers(Object.keys(error), ['status_code']); - assert.isTrue(soapRequest.parser.parse.called); - assert.isTrue(soapRequest.parser.toJSON.called); - assert.isTrue(soapRequest.request.called); - done(); - }); - }); - - - it('Invokes catch method if an error is returned on invalid request', (done) => { - requestError = new Error('invalid URI provided'); - - const request = soapRequest.post().catch((error) => { - assert.instanceOf(request, Promise); - assert.isObject(error); - assert.sameMembers(Object.keys(error), ['description']); - assert.isTrue(soapRequest.parser.parse.called); - assert.isTrue(soapRequest.parser.toJSON.called); - assert.isTrue(soapRequest.request.called); - done(); - }); - }); -}); diff --git a/test/environment.js b/test/environment.js index 0051c1b..8a885b6 100644 --- a/test/environment.js +++ b/test/environment.js @@ -13,8 +13,7 @@ describe('environment.js', () => { 'PAYBILL_NUMBER', 'PASSKEY', 'ENDPOINT', - 'CALLBACK_URL', - 'CALLBACK_METHOD', + 'MERCHANT_ENDPOINT', ]); }); }); From 18bb344d9da9a4b293a911d84ece4e2812341c7b Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 11 Jun 2016 21:25:58 +0300 Subject: [PATCH 87/96] Moved SOAPRequest.js to utils --- server/utils/SOAPRequest.js | 45 +++++++++++++++++ test/utils/SOAPRequest.js | 96 +++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 server/utils/SOAPRequest.js create mode 100644 test/utils/SOAPRequest.js diff --git a/server/utils/SOAPRequest.js b/server/utils/SOAPRequest.js new file mode 100644 index 0000000..2a89f3c --- /dev/null +++ b/server/utils/SOAPRequest.js @@ -0,0 +1,45 @@ +'use strict'; + +const request = require('request'); + +module.exports = class SOAPRequest { + construct(payment, parser) { + this.request = request; + this.parser = parser; + this.requestOptions = { + method: 'POST', + uri: process.env.ENDPOINT, + rejectUnauthorized: false, + body: payment.body, + headers: { + 'content-type': 'application/xml; charset=utf-8', + }, + }; + return this; + } + + post() { + return new Promise((resolve, reject) => { + // Make the soap request to the SAG URI + this.request(this.requestOptions, (error, response, body) => { + if (error) { + reject({ description: error.message }); + return; + } + + const parsedResponse = this.parser.parse(body); + const json = parsedResponse.toJSON(); + + // Anything that is not "00" as the + // SOAP response code is a Failure + if (json && json.status_code !== 200) { + reject(json); + return; + } + + // Else everything went well + resolve(json); + }); + }); + } +}; diff --git a/test/utils/SOAPRequest.js b/test/utils/SOAPRequest.js new file mode 100644 index 0000000..3e13bc6 --- /dev/null +++ b/test/utils/SOAPRequest.js @@ -0,0 +1,96 @@ +'use strict'; + +require('../../environment'); +const chai = require('chai'); +const assert = chai.assert; +const sinon = require('sinon'); +const moment = require('moment'); +const uuid = require('node-uuid'); + +const SOAPRequest = require('../../server/utils/SOAPRequest'); +const ParseResponse = require('../../server/utils/ParseResponse'); +const paymentRequest = require('../../server/controllers/PaymentRequest'); +const GenEncryptedPassword = require('../../server/utils/GenEncryptedPassword'); + +describe('SOAPRequest', () => { + const timeStamp = moment().format('YYYYMMDDHHmmss'); + const encryptedPassword = new GenEncryptedPassword(timeStamp).hashedPassword; + const paymentDetails = { + referenceID: uuid.v4(), + merchantTransactionID: uuid.v1(), + amountInDoubleFloat: '100.00', + clientPhoneNumber: '254723001575', + extraPayload: {}, + timeStamp, + encryptedPassword, + }; + + const parser = new ParseResponse('bodyTagName'); + parser.parse = sinon.stub().returns(parser); + parser.toJSON = sinon.stub(); + parser.toJSON.onFirstCall().returns({ status_code: 200 }); + parser.toJSON.onSecondCall().returns({ status_code: 400 }); + + const soapRequest = new SOAPRequest(); + paymentRequest.buildSoapBody(paymentDetails); + soapRequest.construct(paymentRequest, parser); + + let requestError = undefined; + sinon.stub(soapRequest, 'request', (params, callback) => { + callback(requestError, null, 'a soap dom tree string'); + }); + + afterEach(() => { + requestError = undefined; + }); + + it('SOAPRequest is contructed', () => { + assert.instanceOf(soapRequest.parser, ParseResponse); + assert.sameMembers(Object.keys(soapRequest.requestOptions), [ + 'method', + 'uri', + 'rejectUnauthorized', + 'body', + 'headers', + ]); + }); + + it('Invokes then method from a successful response', (done) => { + const request = soapRequest.post().then((response) => { + assert.instanceOf(request, Promise); + assert.isObject(response); + assert.sameMembers(Object.keys(response), ['status_code']); + assert.isTrue(soapRequest.parser.parse.called); + assert.isTrue(soapRequest.parser.toJSON.called); + assert.isTrue(soapRequest.request.called); + done(); + }); + }); + + it('Invokes catch method from an unsuccessful response', (done) => { + const request = soapRequest.post().catch((error) => { + assert.instanceOf(request, Promise); + assert.isObject(error); + assert.sameMembers(Object.keys(error), ['status_code']); + assert.isTrue(soapRequest.parser.parse.called); + assert.isTrue(soapRequest.parser.toJSON.called); + assert.isTrue(soapRequest.request.called); + done(); + }); + }); + + + it('Invokes catch method if an error is returned on invalid request', (done) => { + requestError = new Error('invalid URI provided'); + + const request = soapRequest.post().catch((error) => { + assert.instanceOf(request, Promise); + assert.isObject(error); + assert.sameMembers(Object.keys(error), ['description']); + assert.isTrue(soapRequest.parser.parse.called); + assert.isTrue(soapRequest.parser.toJSON.called); + assert.isTrue(soapRequest.request.called); + done(); + }); + }); +}); From 4feb3baa2641ea73b74df757d5168b0512065dc1 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sat, 11 Jun 2016 21:56:58 +0300 Subject: [PATCH 88/96] Add a new error code and error stumbled upon --- server/config/statusCodes.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index 5dbd264..a702d7d 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -36,6 +36,10 @@ module.exports = [{ return_code: 42, status_code: 400, message: 'Your PASSKEY, PAYBILL_NUMBER or environment variables may be incorrect', +}, { + return_code: 99, + status_code: 400, + message: 'There\'s no recorded transaction associated with the transaction ID provided', }, { return_code: 32, status_code: 401, @@ -92,4 +96,4 @@ module.exports = [{ return_code: 5, status_code: 504, message: 'Duration provided to complete the transaction has expired', -}]; +}] From 3225c4b23896f74ede983cd4f52da047cced4444 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 12 Jun 2016 21:58:39 +0300 Subject: [PATCH 89/96] Use internal callback URL to then forward the response as a JSON to the merchant --- server/controllers/PaymentRequest.js | 6 ++++-- test/controllers/PaymentRequest.js | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/controllers/PaymentRequest.js b/server/controllers/PaymentRequest.js index de89b1a..b1adbf1 100644 --- a/server/controllers/PaymentRequest.js +++ b/server/controllers/PaymentRequest.js @@ -12,6 +12,7 @@ class PaymentRequest { constructor(request, parser) { this.parser = parser; this.soapRequest = request; + this.callbackMethod = 'POST'; } buildSoapBody(data) { @@ -31,8 +32,8 @@ class PaymentRequest { ${data.amountInDoubleFloat} ${data.clientPhoneNumber} ${JSON.stringify(data.extraPayload)} - ${process.env.CALLBACK_URL} - ${process.env.CALLBACK_METHOD} + ${data.callbackURL} + ${this.callbackMethod} ${data.timeStamp} @@ -52,6 +53,7 @@ class PaymentRequest { extraPayload: req.body.extraPayload, timeStamp: req.timeStamp, encryptedPassword: req.encryptedPassword, + callbackURL: `${req.protocol}://${req.hostname}/api/v${process.env.API_VERSION}/payment/success`, }; const payment = this.buildSoapBody(paymentDetails); diff --git a/test/controllers/PaymentRequest.js b/test/controllers/PaymentRequest.js index 8110f1b..f9c72ff 100644 --- a/test/controllers/PaymentRequest.js +++ b/test/controllers/PaymentRequest.js @@ -85,6 +85,7 @@ describe('paymentRequest', () => { 'client_phone_number', 'extra_payload', 'time_stamp', + 'callback_url', ]); }); }); From 7dc7725a599561033a5882351439feddd6219bf2 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 12 Jun 2016 22:14:07 +0300 Subject: [PATCH 90/96] Only add the stack trace if the environment is development --- index.js | 15 ++++++++++----- server/config/statusCodes.js | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index c5af5b9..41f4dd6 100644 --- a/index.js +++ b/index.js @@ -60,14 +60,19 @@ app.use((req, res, next) => { app.use((err, req, res, next) => { if (typeof err === 'undefined') next(); console.log('An error occured: ', err.message); - const stack = err.stack.split(/\n/).map(prettifyStackTrace); - - return res.status(err.statusCode || 500).json({ + const errorResponse = { status_code: err.statusCode, request_url: req.originalUrl, message: err.message, - stack_trace: stack, - }); + }; + + // Only send back the error stack if it's on development mode + if (process.env.NODE_ENV === 'development') { + const stack = err.stack.split(/\n/).map(prettifyStackTrace); + errorResponse.stack_trace = stack; + } + + return res.status(err.statusCode || 500).json(); }); const server = app.listen(process.env.PORT || 8080, () => { diff --git a/server/config/statusCodes.js b/server/config/statusCodes.js index a702d7d..8c08c82 100644 --- a/server/config/statusCodes.js +++ b/server/config/statusCodes.js @@ -96,4 +96,4 @@ module.exports = [{ return_code: 5, status_code: 504, message: 'Duration provided to complete the transaction has expired', -}] +}]; From 04c6c2a6c112126e0f166706970b8b8714b41352 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 12 Jun 2016 22:14:35 +0300 Subject: [PATCH 91/96] Check if the required env configs have been added before trying to add them --- environment.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/environment.js b/environment.js index 2aa1835..600b84f 100644 --- a/environment.js +++ b/environment.js @@ -14,14 +14,26 @@ process.env.SESSION_SECRET_KEY = uuid.v4(); if (!('NODE_ENV' in process.env)) process.env.NODE_ENV = 'development'; if (process.env.NODE_ENV === 'development') { - if (fs.existsSync(yamlConfigFile)) { - // Get the rest of the config from app.yaml config file - const config = yaml.safeLoad(fs.readFileSync(yamlConfigFile, 'utf8')); - Object.keys(config.env_variables).forEach(key => { - process.env[key] = config.env_variables[key]; - }); - } else { - throw new Error(` + const requiredEnvVariables = [ + 'PAYBILL_NUMBER', + 'PASSKEY', + 'MERCHANT_ENDPOINT', + ]; + const envKeys = Object.keys(process.env); + const requiredEnvVariablesExist = requiredEnvVariables + .every(variable => envKeys.indexOf(variable) !== -1); + + // if the requiredEnvVariables have not been added + // maybe by GAE or Heroku ENV settings + if (!requiredEnvVariablesExist) { + if (fs.existsSync(yamlConfigFile)) { + // Get the rest of the config from app.yaml config file + const config = yaml.safeLoad(fs.readFileSync(yamlConfigFile, 'utf8')); + Object.keys(config.env_variables).forEach(key => { + process.env[key] = config.env_variables[key]; + }); + } else { + throw new Error(` Missing app.yaml config file used while in development mode It should have contents similar to the example below: @@ -42,5 +54,6 @@ if (process.env.NODE_ENV === 'development') { - ^(.*/)?.*/node_modules/.*$ ------------------------- `); + } } } From e3a8d83c83feae412bdcb0cc722ca26906d9c191 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Sun, 12 Jun 2016 22:40:35 +0300 Subject: [PATCH 92/96] Should only return an error if the MERCHAN_ENDPOINT has not been provided on other NODE_ENVs --- server/controllers/PaymentSuccess.js | 8 +++++--- test/controllers/PaymentSuccess.js | 2 ++ test/environment.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/controllers/PaymentSuccess.js b/server/controllers/PaymentSuccess.js index 2629d7f..59b6b96 100644 --- a/server/controllers/PaymentSuccess.js +++ b/server/controllers/PaymentSuccess.js @@ -15,9 +15,11 @@ class PaymentSuccess { if ('MERCHANT_ENDPOINT' in process.env) { endpoint = process.env.MERCHANT_ENDPOINT; - } else if (process.env.NODE_ENV === 'development') { - next(new Error('MERCHANT_ENDPOINT has not been provided in environment configuration')); - return; + } else { + if (process.env.NODE_ENV !== 'development') { + next(new Error('MERCHANT_ENDPOINT has not been provided in environment configuration')); + return; + } } for (const x of keys) { diff --git a/test/controllers/PaymentSuccess.js b/test/controllers/PaymentSuccess.js index 5aff918..bf3e1a6 100644 --- a/test/controllers/PaymentSuccess.js +++ b/test/controllers/PaymentSuccess.js @@ -56,8 +56,10 @@ describe('paymentSuccess', () => { it('If MERCHANT_ENDPOINT is not provided, next is passed an error', () => { delete process.env.MERCHANT_ENDPOINT; + process.env.NODE_ENV = 'production'; paymentSuccess.handler(req, res, next); + console.log(next.args); const spyCall = next.getCall(0); const args = spyCall.args[0]; diff --git a/test/environment.js b/test/environment.js index 8a885b6..810611d 100644 --- a/test/environment.js +++ b/test/environment.js @@ -9,10 +9,10 @@ describe('environment.js', () => { // console.log(Object.keys(process.env)); expect(Object.keys(process.env)).to.include.members([ 'API_VERSION', + 'ENDPOINT', 'SESSION_SECRET_KEY', 'PAYBILL_NUMBER', 'PASSKEY', - 'ENDPOINT', 'MERCHANT_ENDPOINT', ]); }); From 44c9b930799e5457b0abbd930ea8e219e0961209 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 13 Jun 2016 00:03:17 +0300 Subject: [PATCH 93/96] update readme --- README.md | 130 +++++++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index ae103c8..50bb6a9 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,12 @@ ![](http://cdn.javascript.co.ke/images/banner.png) -> **What MPESA G2 API should have been in the 21st century.** +> __What MPESA G2 API should have been in the 21st century.__ -> **PLEASE NOTE: Mediates only C2B portion for now.** +> __PLEASE NOTE: Mediates only C2B portion for now.__ -**MPESA API RESTful mediator**. It transforms all merchant REST requests to the dreaded ancient SOAP/XML -requests. And transforms Safaricom MPESA G2 API gateway SOAP responses to JSON. -Responding back to the merchant via a beautiful and soothing REST API. - -In short, it'll deal with all of the SOAP shenanigans while you REST. +

Project Mulla is a MPESA API RESTful mediator. It lets you make familiar HTTP REST requests, transforming your requests to the fiddling dreaded SOAP/XML requests that the Safaricom MPESA G2 API only understands. It then communicates with the MPESA API gateway, transforming all SOAP responses from the SAG to RESTful JSON responses that you then consume effortlessly.

+
In short, it’ll deal with all of the SOAP shenanigans while you REST. Everybody wins!
The aim of **Project Mulla** is to create a REST API middleman that interfaces with the **MPESA G2 API** for you. @@ -21,45 +18,46 @@ Developers should not go through the **trauma** involved with dealing with SOAP/ # Example of how it works -Let's go ahead and make the 1st call, **ProcessCheckoutRequest**. This is initial step is to tell the SAG to -initialise a payment request you want to transact. After initialisation, you then make another POST request to -the SAG as a confirmation signal to carry out the actual payment/transaction request prior. +## Request Payment -Assuming **Project Mulla** is now your mediator, you'd now make a **POST request to Project Mulla**. _Not Safaricom_. +This is initial step is to tell the SAG to initialise a payment you want to transact. After +initialisation, you then make another request to the SAG as a confirmation signaling the SAG to +process the payment request requested. -See below how you'd make this initial request: +Assuming __Project Mulla__ is now your mediator, you'd now make a __POST__ request to +__Project Mulla__. _Not the Safaricom Access Gateway_. -##### Initiate Payment Request: +See below how you'd make this initial request: -_Method_: **`POST`** +### Initiate Payment Request: -_Endpoint_: **`https://your-project-mulla-endpoint.herokuapp.com/api/v1/payment/request`** +__`POST`__ __`https://project-mulla-companyname.herokuapp.com/api/v1/payment/request`__ _Body Parameters_: -- **`phoneNumber`** - The phone number of your client -- **`totalAmount`** - The total amount you are charging the client -- **`referenceID`** - The reference ID of the order or service **[optional]** -- **`merchantTransactionID`** - This specific order's or service's transaction ID **[optional]** +- `phoneNumber` - The phone number of your client +- `totalAmount` - The total amount you are charging the client +- `referenceID` [optional] - The reference ID of the order or service +- `merchantTransactionID` [optional] - This specific order's or service's transaction ID -_**NOTE:** If `merchantTransactionID` or `referenceID` are not provided a time-based and random -UUID is generated for each respectively._ +> __NOTE:__ If `merchantTransactionID` or `referenceID` are not provided a time-based and random +UUID is generated for each respectively. -_The response you get:_ +### Sample request using CURL in the command line/terminal: -_**`HTTP HEADER META DATA`**_ -```http -HTTP/1.1 200 OK -Connection: keep-alive -Content-Length: 510 -Content-Type: application/json; charset=utf-8 -Date: Sat, 21 May 2016 10:03:37 GMT -ETag: W/"1fe-jy66YehfhiFHWoyTNHpSnA" -X-Powered-By: Express -set-cookie: connect.sid=s:nc8L7qNbCJRKILyn7XLYf4IIg7_QuJIV.wuWGgb3r7XdQrkOF4P7GdzAY1HRZ0utmIfC6yW8%2BMuY; Path=/; HttpOnly +```bash +$ curl -i -X POST \ +--url http://project-mulla-companyname.herokuapp.com/api/v1/payment/request \ +--data 'phoneNumber=254723001575' \ +--data 'totalAmount=45.00' \ +--data 'clientName="Eugene Mutai"' \ +--data 'clientLocation=Kilimani' \ ``` -_**`The JSON response in the BODY`**_ +### Expected Response + +If all goes well you get HTTP status code __`200`__ accompanied with the a similar structured JSON response: + ```json { "response": { @@ -71,7 +69,7 @@ _**`The JSON response in the BODY`**_ "cust_msg": "to complete this transaction, enter your bonga pin on your handset. if you don't have one dial *126*5# for instructions", "reference_id": "3e3beff0-fc05-417a-bbf2-190ee19a5e58", "merchant_transaction_id": "95d64500-2514-11e6-bcb8-a7f8e1c786c4", - "amount_in_double_float": "10.00", + "amount_in_double_float": "45.00", "client_phone_number": "254723001575", "extra_payload": {}, "time_stamp": "20160528234142" @@ -79,37 +77,47 @@ _**`The JSON response in the BODY`**_ } ``` -# Installation & Testing +## Next step: confirmation -## Dependencies +You are to use `trx_id` or `merchant_transaction_id` to make the confirmation payment +request. The confirmation request is the request the payment requested above to be processed and +triggers a pop up on the your client's mobile phone. -You will need to install some stuff, if they are not yet installed in your machine: +[Find the complete documentation here](http://kn9ts.github.io/project-mulla/docs) + +# Installation & Testing + +# Installation -##### Majors: +Installing Project Mulla is easy and straight-forward, but there are a few requirements you’ll need +to make sure your system has before you start. -* **Node.js (v4.3.2 or higher; LTS)** - [Click here](http://nodejs.org) to install +## Requirements -##### Secondaries(click for further information): +You will need to install some stuff, if they are not yet installed in your machine: -* **NPM (v3.5+; bundled with node.js installation package)** +* [Node.js (v4.3.2 or higher; LTS)](http://nodejs.org) +* [NPM (v3.5+; bundled with node.js installation package)](https://docs.npmjs.com/getting-started/installing-node#updating-npm) -If already installed you may need to only update it to the latest version: +If you've already installed the above you may need to only update **npm** to the latest version: ```bash -$ npm update -g npm +$ sudo npm update -g npm ``` -## Getting Started +--- -Once you have **Node.js (and NPM)** installed, run _(type or copy & paste; pick your poison)_: +## Install with Github + +Best way to install Project Mulla is to clone it from Github **To clone/download the boilerplate** ```bash -$ git clone https://github.com/kn9ts/project-mulla +$ git clone https://github.com/kn9ts/project-mulla.git ``` -After cloning, get into your project mulla's directory/folder: +**After cloning, get into your cloned Project Mulla's directory/folder** ```bash $ cd project-mulla @@ -121,9 +129,10 @@ $ cd project-mulla $ npm install ``` -**Create `app.yaml` configurations file** +__Create `app.yaml` configurations file__ -The last but not least step is creating a `app.yaml` file with your configurations in the root directory of `project-mulla`. +The last but not least step is creating a `app.yaml` file with your configurations in the root +directory of `project-mulla`. This is the same folder estate where `index.js` can be found. @@ -131,15 +140,12 @@ It should look like the example below, only with your specific config values: ```yaml env_variables: - SESSION_SECRET_KEY: '88735405ab8d9f968ed4dab89da5515KadjaklJK238adnkLD32' PAYBILL_NUMBER: '898998' PASSKEY: 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' - ENDPOINT: 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' - CALLBACK_URL: 'http://awesome-service.com/mpesa/confirm-checkout.php' - CALLBACK_METHOD: 'POST' + MERCHANT_ENDPOINT: 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' -# Everything below from this point onwards are only relevant -# if you are looking to deploy Project Mulla to Google App Engine. +# Everything below is only relevant if you are looking +# to deploy Project Mulla to Google App Engine. runtime: nodejs vm: true @@ -147,11 +153,11 @@ skip_files: - ^(.*/)?.*/node_modules/.*$ ``` -*__PLEASE NOTE:__ The __`PAYBILL_NUMBER`__ and __`PASSKEY`__ are provided by Safaricom once you have registered for the MPESA G2 API.* +*__NOTE:__ The `PAYBILL_NUMBER` and `PASSKEY` are provided by Safaricom once you have registered for the MPESA G2 API.* -*__PLEASE NOTE:__ The details above only serve as examples* +*__NOTE:__ The details above only serve as examples* -#### It's now ready to launch +## It's now ready to launch 1st run the command `npm test` in the console and see if everything is all good. Then run: @@ -161,10 +167,11 @@ $ npm start > project-mulla@0.1.1 start ../project-mulla > node index.js +Your secret session key is: 5f06b1f1-1bff-470d-8198-9ca2f18919c5 Express server listening on 8080, in development mode ``` -#### Do a test run +## Do a test run Now make a test run using **CURL**: @@ -217,9 +224,10 @@ set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly } ``` + # This project uses GPLv3 LICENSE -**_TL;DR_*** Here's what the license entails: +__TL;DR__ Here's what the license entails: ```markdown 1. Anyone can copy, modify and distribute this software. @@ -235,4 +243,4 @@ set-cookie: connect.sid=s:iWfXH7rbAvXz7cYgmurhGTHDn0LNBmNt; Path=/; HttpOnly More information on the [LICENSE can be found here](http://choosealicense.com/licenses/gpl-3.0/) -**_DISCLAIMER:_** _All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with._ +*__DISCLAIMER:__* _All opinions aired in this repo are ours and do not reflect any company or organisation any contributor is involved with._ From 55d6eb90b5657326e0ce6c5db8983de0e9ffb5d1 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 13 Jun 2016 00:10:58 +0300 Subject: [PATCH 94/96] set the correct merchant endpoint example --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 50bb6a9..d6e9f44 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ UUID is generated for each respectively. ```bash $ curl -i -X POST \ --url http://project-mulla-companyname.herokuapp.com/api/v1/payment/request \ ---data 'phoneNumber=254723001575' \ +--data 'phoneNumber=254723000000' \ --data 'totalAmount=45.00' \ --data 'clientName="Eugene Mutai"' \ --data 'clientLocation=Kilimani' \ @@ -141,8 +141,8 @@ It should look like the example below, only with your specific config values: ```yaml env_variables: PAYBILL_NUMBER: '898998' - PASSKEY: 'ab8d88186735405ab8d59f968ed4dab891588186735405ab8d59asku8' - MERCHANT_ENDPOINT: 'https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' + PASSKEY: 'a8eac82d7ac1461ba0348b0cb24d3f8140d3afb9be864e56a10d7e8026eaed66' + MERCHANT_ENDPOINT: 'http://merchant-endpoint.com/mpesa/payment/complete' # Everything below is only relevant if you are looking # to deploy Project Mulla to Google App Engine. @@ -178,7 +178,7 @@ Now make a test run using **CURL**: ```bash $ curl -i -X POST \ --url http://localhost:8080/api/v1/payment/request \ - --data 'phoneNumber=254723001575' \ + --data 'phoneNumber=254723000000' \ --data 'totalAmount=10.00' \ --data 'clientName="Eugene Mutai"' \ --data 'clientLocation=Kilimani' \ @@ -188,7 +188,7 @@ Or if you have [httpie](https://github.com/jkbrzt/httpie) installed: ```bash $ http POST localhost:8080/api/v1/payment/request \ - phoneNumber=254723001575 \ + phoneNumber=254723000000 \ totalAmount=10.00 \ clientName='Eugene Mutai' \ clientLocation='Kilimani' From fcfc04b1be2d221a15faf3954b274dd17a518e71 Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 13 Jun 2016 00:40:18 +0300 Subject: [PATCH 95/96] If port is not provided use default to build the POST test url: /thumbs/up --- server/controllers/PaymentSuccess.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/PaymentSuccess.js b/server/controllers/PaymentSuccess.js index 59b6b96..275142c 100644 --- a/server/controllers/PaymentSuccess.js +++ b/server/controllers/PaymentSuccess.js @@ -10,7 +10,7 @@ class PaymentSuccess { handler(req, res, next) { const keys = Object.keys(req.body); const response = {}; - const baseURL = `${req.protocol}://${req.hostname}:${process.env.PORT}`; + const baseURL = `${req.protocol}://${req.hostname}:${process.env.PORT || 8080}`; let endpoint = `${baseURL}/api/v1/thumbs/up`; if ('MERCHANT_ENDPOINT' in process.env) { From 6b9a05c8acc56b5648330c56741165e66aa75d5c Mon Sep 17 00:00:00 2001 From: Eugene Mutai Date: Mon, 13 Jun 2016 14:56:08 +0300 Subject: [PATCH 96/96] Add homepage --- .gitignore | 2 +- index.js | 6 +++++- server/public/css/style.css | 1 + server/public/css/style.less | 41 ++++++++++++++++++++++++++++++++++++ server/views/index.jade | 10 +++++++-- server/views/layout.jade | 2 +- 6 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 server/public/css/style.css create mode 100644 server/public/css/style.less diff --git a/.gitignore b/.gitignore index 956d3c7..96ccb56 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ .idea .vscode node_modules -public +./public dist server/config/local.env.js npm-debug.log diff --git a/index.js b/index.js index 41f4dd6..2867dd3 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ const genTransactionPassword = require('./server/utils/genTransactionPassword'); const apiVersion = process.env.API_VERSION; // view engine setup -app.set('views', path.join(__dirname, 'views')); +app.set('views', path.join(__dirname, 'server/views')); app.set('view engine', 'jade'); // trust proxy if it's being served in GoogleAppEngine @@ -46,6 +46,10 @@ app.use(`/api/v${apiVersion}/payment*`, genTransactionPassword); const apiRouter = express.Router; app.use(`/api/v${apiVersion}`, routes(apiRouter())); +app.all('/*', (req, res) => { + res.render('index', { title: 'Project Mulla' }); +}); + // use this prettify the error stack string into an array of stack traces const prettifyStackTrace = stackTrace => stackTrace.replace(/\s{2,}/g, ' ').trim(); diff --git a/server/public/css/style.css b/server/public/css/style.css new file mode 100644 index 0000000..e9b9a08 --- /dev/null +++ b/server/public/css/style.css @@ -0,0 +1 @@ +@charset "utf-8";@import url(https://fonts.googleapis.com/css?family=Asap:400,700);html{cursor:default;font-family:'Asap',sans-serif;margin:0;overflow-x:hidden;padding:0}body{cursor:default;font-family:'Asap',sans-serif;font-size:1.5em;margin:0;overflow-x:hidden;padding:0;color:#3a3a3a;background:#fefefe}.homepage{margin-top:10%;text-align:center}.homepage h1{font-weight:bolder;font-size:4em;padding-bottom:0;margin-bottom:0}.homepage p{font-size:2em}.homepage footer{font-size:.25em}.homepage .red{font-weight:bold;color:red} \ No newline at end of file diff --git a/server/public/css/style.less b/server/public/css/style.less new file mode 100644 index 0000000..ddf60b6 --- /dev/null +++ b/server/public/css/style.less @@ -0,0 +1,41 @@ +@import url(https://fonts.googleapis.com/css?family=Asap:400,700); +@charset "utf-8"; +html { + cursor: default; + font-family: 'Asap', sans-serif; + margin: 0; + overflow-x: hidden; + padding: 0; +} + +body { + cursor: default; + font-family: 'Asap', sans-serif; + font-size: 1.5em; + margin: 0; + overflow-x: hidden; + padding: 0; + color: #3a3a3a; + background: #fefefe; +} + +.homepage { + margin-top: 10%; + text-align: center; + h1 { + font-weight: bolder; + font-size: 4em; + padding-bottom: 0; + margin-bottom: 0; + } + p { + font-size: 2em; + } + footer { + font-size: .25em; + } + .red { + font-weight: bold; + color: red; + } +} diff --git a/server/views/index.jade b/server/views/index.jade index 3d63b9a..576f278 100644 --- a/server/views/index.jade +++ b/server/views/index.jade @@ -1,5 +1,11 @@ extends layout block content - h1= title - p Welcome to #{title} + .homepage + a(style="color: #5FAD24" href="https://kn9ts.github.io/project-mulla" target="_blank") + h1= title + p ...because Time is Money! + footer + p.text-center.text-muted.small-padding + | © 2016, with love by + Eugene Mutai diff --git a/server/views/layout.jade b/server/views/layout.jade index 15af079..215cbc6 100644 --- a/server/views/layout.jade +++ b/server/views/layout.jade @@ -2,6 +2,6 @@ doctype html html head title= title - link(rel='stylesheet', href='/stylesheets/style.css') + link(rel='stylesheet', href='/css/style.css') body block content