├── example ├── .gitignore ├── package.json ├── static │ ├── css │ │ ├── reset.css │ │ └── style.css │ ├── license.txt │ ├── js │ │ └── index.js │ └── index.html └── app.js ├── .test.json ├── index.js ├── lib ├── services │ ├── vc │ │ └── index.js │ ├── extracts │ │ └── index.js │ └── lnmo │ │ ├── xml │ │ ├── transactionConfirmRequest.xml.js │ │ ├── transactionStatusRequest.xml.js │ │ └── processCheckoutRequest.xml.js │ │ └── index.js ├── index.js └── constants.js ├── package.json ├── LICENSE ├── .gitignore ├── .npmignore ├── test └── payments.spec.js └── README.md /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "merchant": null, 3 | "passkey": null, 4 | "debug": true 5 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pesajs-example", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "body-parser": "^1.13.3", 6 | "express": "^4.13.3", 7 | "morgan": "^1.7.0", 8 | "randomstring": "^1.0.8" 9 | }, 10 | "main": "app.js", 11 | "scripts": { 12 | "start": "node app.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : index.js 9 | * Date : 9/5/15 11:53 AM 10 | * Description : 11 | * 12 | */ 13 | 'use strict'; 14 | 15 | exports = module.exports = require("./lib/index"); -------------------------------------------------------------------------------- /lib/services/vc/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : vc 9 | * Date : 9/5/15 8:16 PM 10 | * Description : PayBill Validation and Confirmation Version 0.3 11 | * 12 | */ 13 | 'use strict'; 14 | 15 | 16 | exports = module.exports = { 17 | name: "Validation and Confirmation" 18 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : index 9 | * Date : 9/5/15 1:07 PM 10 | * Description : MPesa Services 11 | * 12 | */ 13 | 'use strict'; 14 | 15 | // Lipa Na M-Pesa Online 16 | exports.PaymentService = require("./services/lnmo"); 17 | exports.LipaNaMpesa = exports.PaymentService; 18 | 19 | // Paybill and Buygoods Validation & Confirmation 20 | exports.ValidationService = require("./services/vc"); 21 | 22 | // Transaction Extracts 23 | exports.ExtractsService = require("./services/extracts"); 24 | -------------------------------------------------------------------------------- /lib/services/extracts/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : extracts 9 | * Date : 9/5/15 8:04 PM 10 | * Description : Transaction Extracts - Interface Specification Version 0.2 11 | * 12 | * All Organizations on M-Pesa can get data extracts of all the transactions that occurred on a specific 13 | * day from the Broker SFTP server. This is the process to follow to access the data extracts. 14 | * 15 | * 16 | */ 17 | 'use strict'; 18 | 19 | exports = module.exports = { 20 | name: "Transaction Extracts" 21 | }; -------------------------------------------------------------------------------- /example/static/css/reset.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0} 2 | -------------------------------------------------------------------------------- /lib/services/lnmo/xml/transactionConfirmRequest.xml.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | 5 | return ` 6 | 7 | 8 | 9 | ${data.merchant} 10 | ${data.ref} 11 | ${data.password} 12 | ${data.timestamp} 13 | 14 | 15 | 16 | 17 | ${data.mpesa_txn} 18 | ${data.transaction} 19 | 20 | 21 | `; 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /lib/services/lnmo/xml/transactionStatusRequest.xml.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | 5 | return ` 6 | 7 | 8 | 9 | ${data.merchant} 10 | ${data.ref} 11 | ${data.password} 12 | ${data.timestamp} 13 | 14 | 15 | 16 | 17 | 18 | ${data.mpesa_txn} 19 | 20 | ${data.transaction} 21 | 22 | 23 | `; 24 | 25 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pesajs", 3 | "version": "0.0.1", 4 | "description": "M-Pesa API helper", 5 | "homepage": "https://github.com/aksalj/pesajs", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/aksalj/pesajs" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/aksalj/pesajs/issues", 12 | "email": "aksalj@aksalj.com" 13 | }, 14 | "keywords": [ 15 | "mpesa", 16 | "mobile money", 17 | "safaricom" 18 | ], 19 | "author": "Salama AB ", 20 | "license": "MIT", 21 | "engineStrict": true, 22 | "engines": { 23 | "node": ">=4.4" 24 | }, 25 | "dependencies": { 26 | "crypto-js": "^3.1.6", 27 | "request": "^2.72.0", 28 | "xml2json": "^0.9.1" 29 | }, 30 | "main": "index.js", 31 | "scripts": { 32 | "test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha", 33 | "example": "cd example && npm start" 34 | }, 35 | "devDependencies": { 36 | "chai": "^3.5.0", 37 | "istanbul": "^0.4.3", 38 | "mocha": "^2.5.3", 39 | "request-debug": "^0.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Salama AB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/static/license.txt: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules/ 31 | ### JetBrains template 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 33 | 34 | *.iml 35 | 36 | ## Directory-based project format: 37 | .idea/ 38 | 39 | # Mongo Explorer plugin: 40 | # .idea/mongoSettings.xml 41 | 42 | ## File-based project format: 43 | *.ipr 44 | *.iws 45 | 46 | ## Plugin-specific files: 47 | 48 | # IntelliJ 49 | /out/ 50 | 51 | # mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # node modules 55 | 56 | # docs, logs 57 | doc/official 58 | 59 | # 60 | .test.local.json 61 | -------------------------------------------------------------------------------- /lib/services/lnmo/xml/processCheckoutRequest.xml.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | 5 | return ` 6 | 7 | 8 | 9 | ${data.merchant} 10 | ${data.ref} 11 | ${data.password} 12 | ${data.timestamp} 13 | 14 | 15 | 16 | 17 | ${data.transaction} 18 | ${data.ref} 19 | ${data.amount} 20 | ${data.account} 21 | 22 | ${data.details} 23 | ${data.callbackUrl} 24 | ${data.callbackMethod} 25 | ${data.timestamp} 26 | 27 | 28 | `; 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | ### JetBrains template 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 33 | 34 | *.iml 35 | 36 | ## Directory-based project format: 37 | .idea/ 38 | example/ 39 | # if you remove the above rule, at least ignore the following: 40 | 41 | # User-specific stuff: 42 | # .idea/workspace.xml 43 | # .idea/tasks.xml 44 | # .idea/dictionaries 45 | 46 | # Sensitive or high-churn files: 47 | # .idea/dataSources.ids 48 | # .idea/dataSources.xml 49 | # .idea/sqlDataSources.xml 50 | # .idea/dynamic.xml 51 | # .idea/uiDesigner.xml 52 | 53 | # Gradle: 54 | # .idea/gradle.xml 55 | # .idea/libraries 56 | 57 | # Mongo Explorer plugin: 58 | # .idea/mongoSettings.xml 59 | 60 | ## File-based project format: 61 | *.ipr 62 | *.iws 63 | 64 | ## Plugin-specific files: 65 | 66 | # IntelliJ 67 | /out/ 68 | 69 | # mpeltonen/sbt-idea plugin 70 | .idea_modules/ 71 | doc/ 72 | 73 | # JIRA plugin 74 | atlassian-ide-plugin.xml 75 | 76 | # Crashlytics plugin (for Android Studio and IntelliJ) 77 | com_crashlytics_export_strings.xml 78 | crashlytics.properties 79 | crashlytics-build.properties 80 | 81 | -------------------------------------------------------------------------------- /test/payments.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by aksalj on 6/17/16. 3 | */ 4 | "use strict"; 5 | let chai = require('chai'); 6 | let expect = chai.expect; 7 | 8 | 9 | const PesaJs = require('../index'); 10 | const service = new PesaJs.LipaNaMpesa(require("../.test.local.json")); 11 | 12 | const cart = { 13 | transaction: "54fgftgh", 14 | ref: "ytrytfytf", 15 | account: "254709998888", 16 | amount: 8765, 17 | callbackUrl: "http://awesome-store.co.ke/ipn", 18 | details: "Additional transaction details if any" 19 | }; 20 | 21 | describe("PaymentService", function () { 22 | 23 | 24 | describe("request payment", function () { 25 | 26 | it("checks cart params", function (done) { 27 | service.requestPayment() 28 | .then((data) => { 29 | data.should.not.exist(); 30 | done(); 31 | }) 32 | .catch((err) => { 33 | expect(err).to.be.not.equal(null); 34 | done(); 35 | }); 36 | }); 37 | 38 | it("submits payment request", function (done) { 39 | service.requestPayment(cart) 40 | .then((data) => { 41 | data.should.exist(); 42 | done(); 43 | }) 44 | .catch((err) => { 45 | expect(err).to.be.equal(null); // grr really? 46 | done(); 47 | }); 48 | }); 49 | 50 | 51 | }); 52 | 53 | describe("confirm payment", function () { 54 | it("checks params", function (done) { 55 | service.confirmPayment() 56 | .then((data) => { 57 | data.should.exist(); 58 | done(); 59 | }) 60 | .catch((err) => { 61 | expect(err).to.be.equal(null); 62 | }); 63 | }); 64 | }); 65 | 66 | describe("check payment status", function () { 67 | it("checks params", function (done) { 68 | service.getPaymentStatus() 69 | .then((data) => { 70 | data.should.exist(); 71 | done(); 72 | }) 73 | .catch((err) => { 74 | expect(err).to.be.equal(null); 75 | }); 76 | }); 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /example/static/js/index.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | $("#reference").val("PREMIUM_ELECTRONICS_" + Math.floor((Math.random() * 78346) + 1)); 4 | 5 | $("#cart").on("click", function() { 6 | $("#payment-cart").hide(); 7 | $("#shopping-cart").fadeToggle( "fast"); 8 | }); 9 | 10 | $("#btnCheckout").click(function(){ 11 | $("#shopping-cart").slideToggle( "fast", function(){ 12 | $("#payment-cart").slideToggle("fast"); 13 | }); 14 | }); 15 | 16 | $("#btnPay").click(function(e) { 17 | if($(this).hasClass("disabled")) { 18 | return; 19 | } 20 | 21 | if($(this).html() === "OK"){ 22 | location.reload(); 23 | return; 24 | } 25 | 26 | $(this).addClass("disabled"); 27 | $(this).html("Please wait..."); 28 | $("#payment-form").submit(); 29 | 30 | e.preventDefault(); 31 | }); 32 | 33 | $('#payment-form').submit(function(event) { 34 | 35 | var formData = {}; 36 | 37 | var txn = $("#transaction").val(); 38 | var mpesa_txn = $("#mpesa_txn").val(); 39 | 40 | var action = (txn !== "" && mpesa_txn !== "") ? "confirm" : "request"; 41 | if(action === "confirm") { 42 | formData = { 43 | 'transaction' : txn, 44 | 'mpesa_transaction': mpesa_txn 45 | }; 46 | 47 | } else { // is request 48 | formData = { 49 | 'phone' : '254' + $("#phone").val(), 50 | 'amount': $("#amount").val(), 51 | 'reference': $("#reference").val() 52 | }; 53 | } 54 | 55 | $.ajax({ 56 | type: 'POST', 57 | url: '/checkout/' + action, 58 | data : formData 59 | }).done(function(data) { 60 | 61 | $("#btnPay").removeClass("disabled"); 62 | 63 | if(action === "request") { 64 | // If no error 65 | $("#btnPay").html("Confirm"); 66 | 67 | $("#transaction").val(data.transaction); 68 | $("#mpesa_txn").val(data.mpesa_txn); 69 | 70 | $("#phoneNumberInput").fadeToggle("fast", function() { 71 | $("#mpesaMsg").html(data.message); // HUH 72 | }); 73 | 74 | 75 | } else { 76 | // If no error, thank you 77 | $("#mpesaMsg").html(data.message); 78 | $("#btnPay").html("OK"); 79 | } 80 | 81 | }).error(function(err) { 82 | 83 | var data = err.responseJSON; 84 | 85 | $("#phoneNumberInput").fadeToggle("fast", function() { 86 | $("#btnPay").removeClass("disabled"); 87 | $("#mpesaMsg").html(data.description || "Oops :("); 88 | $("#btnPay").html("OK"); 89 | }); 90 | 91 | 92 | }); 93 | 94 | // stop the form from submitting the normal way and refreshing the page 95 | event.preventDefault(); 96 | }); 97 | 98 | 99 | 100 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PesaJs (UNMAINTAINED) 2 | 3 | 4 | #### Goal 5 | 6 | Simplify interactions with the official (`horribly documented`) [M-PESA API](https://www.safaricom.co.ke/personal/m-pesa/do-more-with-m-pesa/m-pesa-api). 7 | 8 | > `Important`: Get your `Merchant ID` and `Pass Key` when you register for MPESA API with Safaricom. 9 | 10 | #### Usage 11 | 12 | 13 | ```shell 14 | $ npm install pesajs 15 | 16 | ``` 17 | 18 | 19 | - **Online checkout (`Lipa Na M-Pesa`)**: According to Safaricom, this is a "*Web service for integrating the M-Pesa 20 | Checkout API to a merchant site. The overall scope of this Web service is to provide primitives for application developers 21 | to handle checkout process in a simple way*." 22 | 23 | **Initiate `Lipa Na M-Pesa` online payment** 24 | 25 | ```javascript 26 | const PesaJs = require("pesajs"); 27 | 28 | let options = { 29 | merchant: "YOUR_MERCHANT_ID", 30 | passkey: "YOUR_PASSKEY" 31 | //debug: false || true 32 | }; 33 | let paymentService = new PesaJs.LipaNaMpesa(options); 34 | 35 | 36 | let requestData = { 37 | transaction: MY_TRANSACTION_ID, 38 | ref: MY_REFERENCE, 39 | account: USER_MPESA_NUMBER, 40 | amount: AMOUNT, // in Ksh 41 | callbackUrl: MY_CALLBACK_URL, 42 | details: "Additional transaction details if any" 43 | }; 44 | paymentService.requestPayment(requestData) 45 | .then(function(data) { 46 | 47 | // Now display M-Pesa message to user 48 | let msg = data.message; 49 | 50 | // Keep mpesa transaction id somewhere, so you can use it to confirm transaction (next step). 51 | mpesa_txn = data.mpesa_txn 52 | }) 53 | .catch(function(error){ 54 | // Oops, something went wrong! 55 | }); 56 | 57 | ``` 58 | 59 | **Confirm payment request** 60 | 61 | ```javascript 62 | let params = { 63 | transaction: MY_TRANSACTION, 64 | mpesa_txn: mpesa_txn 65 | }; 66 | paymentService.confirmPayment(params) 67 | .then(function(data) { 68 | // User should see a USSD menu on their phone at this point. 69 | // Now relax and wait for M-Pesa to notify you of the payment 70 | }) 71 | .catch(function(error){ 72 | // Oops, something went wrong! 73 | }); 74 | ``` 75 | 76 | **Receive payment notification** 77 | 78 | ```javascript 79 | // Wait for payment notification (example using express) 80 | app.post("/ipn", paymentService.paymentNotification, function(req, res) { 81 | 82 | // Do whatever with payment info like confirm purchase, init shipping, send download link, etc. 83 | let paymentData = req.payment; 84 | 85 | }); 86 | 87 | ``` 88 | 89 | See the [example](example/app.js) app for a working demo. 90 | 91 | - **Paybill and Buygoods validation & confirmation**: `TODO` 92 | 93 | - **Transaction Extracts**: `TODO` 94 | 95 | 96 | #### Contributing 97 | 98 | 1. Fork this repo and make changes in your own fork. 99 | 2. Commit your changes and push to your fork `git push origin master` 100 | 3. Create a new pull request and submit it back to the project. 101 | 102 | 103 | #### Bugs & Issues 104 | 105 | To report bugs (or any other issues), use the [issues page](https://github.com/aksalj/pesajs/issues). 106 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : app 9 | * Date : 9/5/15 1:14 PM 10 | * Description : 11 | * 12 | */ 13 | 'use strict'; 14 | const express = require("express"); 15 | const bodyParser = require('body-parser'); 16 | const randomstring = require("randomstring"); 17 | const morgan = require('morgan'); 18 | 19 | const PesaJs = require("../index"); 20 | 21 | const paymentService = new PesaJs.LipaNaMpesa({ 22 | merchant: "898998", 23 | passkey: "a8eac82d7ac1461ba0348b0cb24d3f8140d3afb9be864e56a10d7e8026eaed66", 24 | debug: true 25 | }); 26 | 27 | const app = express(); 28 | 29 | app.use(morgan("dev")); 30 | app.use(bodyParser.urlencoded({ extended: false })); 31 | app.use(express.static('static')); 32 | 33 | app.post('/checkout/:action(request|confirm)', function (req, res, next) { 34 | 35 | switch(req.params.action) { 36 | case "request": 37 | 38 | let cart = { 39 | transaction: randomstring.generate(), 40 | ref: req.body.reference, 41 | account: req.body.phone, 42 | amount: req.body.amount, 43 | callbackUrl: "http://awesome-store.co.ke/ipn", 44 | details: "Additional transaction details if any" 45 | }; 46 | 47 | paymentService.requestPayment(cart).then((data) => { 48 | console.info(data); 49 | 50 | // Now if ok show message to user and allow them to confirm 51 | // ... 52 | 53 | res.send({ 54 | transaction: cart.transaction, 55 | mpesa_txn: data.mpesa_txn, 56 | message: data.message 57 | }); 58 | }).catch((function(err) { 59 | console.error(err); 60 | res.status(500).send(err); 61 | })); 62 | 63 | 64 | break; 65 | case "confirm": 66 | 67 | let args = { 68 | transaction: req.body.transaction, 69 | mpesa_txn: req.body.mpesa_transaction 70 | }; 71 | 72 | paymentService.confirmPayment(args).then(function(err, data) { 73 | if(err) { 74 | console.error(err); 75 | res.sendStatus(500); 76 | return; 77 | } 78 | console.info(data); 79 | 80 | 81 | res.send({ 82 | message: "Thank you for doing business with us. Buy some more stuff while we wait for payment to be processed!" 83 | }); 84 | }); 85 | 86 | break; 87 | default: 88 | next(); 89 | break; 90 | } 91 | 92 | }); 93 | 94 | app.post("/ipn", paymentService.paymentNotification, function(req, res) { 95 | // Do whatever with payment info like confirm purchase, init shipping, send download link, etc. 96 | let ipn = req.payment; 97 | console.log(ipn); 98 | }); 99 | 100 | app.get("/status/:transaction", function(req, res) { 101 | let args = { 102 | transaction: req.params.transaction, 103 | mpesa_txn: null 104 | }; 105 | paymentService.getPaymentStatus(args).then(function(err, data) { 106 | console.error(err); 107 | console.error(data); 108 | res.send(data); 109 | }); 110 | }); 111 | 112 | 113 | var server = app.listen(process.env.PORT || 3000, function () { 114 | let host = server.address().address; 115 | let port = server.address().port; 116 | 117 | console.log('Shopping app listening at http://%s:%s', host, port); 118 | }); -------------------------------------------------------------------------------- /example/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Lato:300,400,700); 2 | @import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); 3 | 4 | *, *:before, *:after { 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font: 14px/22px "Lato", Arial, sans-serif; 10 | background: #6eb43f; 11 | } 12 | 13 | .lighter-text { 14 | color: #ABB0BE; 15 | } 16 | 17 | .main-color-text { 18 | /*color: #6394F8;*/ 19 | color: #EC1C24; 20 | } 21 | 22 | nav { 23 | padding: 20px 0 40px 0; 24 | background: #F8F8F8; 25 | font-size: 16px; 26 | } 27 | 28 | nav .navbar-left { 29 | float: left; 30 | } 31 | 32 | nav .navbar-right { 33 | float: right; 34 | } 35 | 36 | nav ul li { 37 | display: inline; 38 | padding-left: 20px; 39 | } 40 | 41 | nav ul li a { 42 | color: #777777; 43 | text-decoration: none; 44 | } 45 | 46 | nav ul li a:hover { 47 | color: black; 48 | } 49 | 50 | .container { 51 | margin: auto; 52 | width: 80%; 53 | } 54 | 55 | .badge { 56 | background-color: #EC1C24; 57 | border-radius: 10px; 58 | color: white; 59 | display: inline-block; 60 | font-size: 12px; 61 | line-height: 1; 62 | padding: 3px 7px; 63 | text-align: center; 64 | vertical-align: middle; 65 | white-space: nowrap; 66 | } 67 | 68 | #payment-cart { 69 | display: none; 70 | } 71 | 72 | #mpesaMsg { 73 | padding: 5px; 74 | text-align: center; 75 | } 76 | 77 | .shopping-cart { 78 | margin: 20px 0; 79 | float: right; 80 | background: white; 81 | width: 320px; 82 | position: relative; 83 | border-radius: 3px; 84 | padding: 20px; 85 | } 86 | 87 | .shopping-cart .shopping-cart-header { 88 | border-bottom: 1px solid #E8E8E8; 89 | padding-bottom: 15px; 90 | } 91 | 92 | .shopping-cart .shopping-cart-header .shopping-cart-total { 93 | float: right; 94 | } 95 | 96 | .shopping-cart .shopping-cart-items { 97 | padding-top: 20px; 98 | } 99 | 100 | .shopping-cart .shopping-cart-items li { 101 | margin-bottom: 18px; 102 | } 103 | 104 | .shopping-cart .shopping-cart-items img { 105 | float: left; 106 | margin-right: 12px; 107 | } 108 | 109 | .shopping-cart .shopping-cart-items .item-name { 110 | display: block; 111 | padding-top: 10px; 112 | font-size: 16px; 113 | } 114 | 115 | .shopping-cart .shopping-cart-items .item-price { 116 | color: #EC1C24; 117 | margin-right: 8px; 118 | } 119 | 120 | .shopping-cart .shopping-cart-items .item-quantity { 121 | color: #ABB0BE; 122 | } 123 | 124 | .shopping-cart:after { 125 | bottom: 100%; 126 | left: 89%; 127 | border: solid transparent; 128 | content: " "; 129 | height: 0; 130 | width: 0; 131 | position: absolute; 132 | pointer-events: none; 133 | border-bottom-color: white; 134 | border-width: 8px; 135 | margin-left: -8px; 136 | } 137 | 138 | .cart-icon { 139 | color: #000000; 140 | font-size: 24px; 141 | margin-right: 7px; 142 | float: left; 143 | } 144 | 145 | .phoneInput { 146 | display: block; 147 | margin: 0; 148 | width: 100%; 149 | font-size: 18px; 150 | appearance: none; 151 | box-shadow: none; 152 | } 153 | 154 | .phoneInput input[type="tel"] { 155 | font-size: 18px; 156 | padding: 10px; 157 | border: none; 158 | border-bottom: solid 2px #c9c9c9; 159 | transition: border 0.3s; 160 | } 161 | 162 | .phoneInput input[type="tel"]:focus { 163 | outline: none; 164 | border-bottom: solid 2px #6eb43f; 165 | } 166 | 167 | .button { 168 | background: #6eb43f; 169 | color: white; 170 | text-align: center; 171 | padding: 12px; 172 | text-decoration: none; 173 | display: block; 174 | border-radius: 3px; 175 | font-size: 16px; 176 | margin: 25px 0 15px 0; 177 | } 178 | 179 | .button:hover { 180 | background: #EC1C24; 181 | } 182 | 183 | .disabled { 184 | background: gray; 185 | } 186 | 187 | .disabled:hover { 188 | background: grey; 189 | } 190 | 191 | .clearfix:after { 192 | content: ""; 193 | display: table; 194 | clear: both; 195 | } 196 | -------------------------------------------------------------------------------- /example/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Awesome Store 6 | 7 | 8 | 9 | 10 | 11 | 12 | 26 | 27 | 28 |
29 |
30 |
31 | 3 32 | 33 |
34 | Total: 35 | KES 22,229.97 36 |
37 |
38 | 39 | 40 |
    41 |
  • 42 | item1 43 | Sony DSC-RX100M III 44 | KES 89,949.99 45 | Qty: 1 46 |
  • 47 | 48 |
  • 49 | item1 50 | KS Automatic Mechanic... 51 | KES 11,249.99 52 | Qty: 12 53 |
  • 54 | 55 |
  • 56 | item1 57 | Kindle, 6" Glare-Free To... 58 | KES 12,439.99 59 | Qty: 1 60 |
  • 61 |
62 | 63 | Checkout 64 |
65 | 66 | 67 |
68 |
69 | 3 70 | 71 |
72 | Total: 73 | KES 22,229.97 74 |
75 |
76 | 77 |
78 | 79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 |
88 |
    89 |
  • 90 | 91 |
  • 92 |
93 | 94 | 254 95 | 96 | 97 |
98 | 99 |
100 |
101 | 102 | Lipa Na M-Pesa 103 | 104 |
105 | 106 | 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : consts 9 | * Date : 9/5/15 7:25 PM 10 | * Description : From M-PESA G2 API; DEVELOPERS GUIDE - LIPA NA M-PESA ONLINE CHECKOUT v 2.0 11 | * 12 | */ 13 | 'use strict'; 14 | const request = require("request"); 15 | const packageJson = require("./../package.json"); 16 | 17 | 18 | exports.URLS = { 19 | CHECKOUT: 'https://www.safaricom.co.ke/mpesa_online/lnmo_checkout_server.php', 20 | CHECKOUT_WSDL: 'https://www.safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl' 21 | }; 22 | 23 | 24 | exports.Error = function (code, description) { 25 | return { 26 | code: code, 27 | description: description 28 | } 29 | }; 30 | 31 | exports.RETURNCODES = { 32 | 33 | // Succesful transaction, all went well 34 | SUCCESS: new exports.Error(0, "Success. The Request has been successfully received or the transaction has successfully completed."), 35 | 36 | // Unsuccesful due to insufficient funds on the MSISDN account 37 | NO_FUNDS: new exports.Error(1, "Insufficient Funds on MSISDN account"), 38 | 39 | // Unsuccesful due to less than allowed minimum amount per transaction 40 | BAD_MIN_AMOUNT: new exports.Error(3,"Amount less than the minimum single transfer allowed on the system."), 41 | 42 | // Unsuccesful due to more than allowed maximum amount per transaction 43 | BAD_MAX_AMOUNT: new exports.Error(4,"Amount more than the maximum single transfer amount allowed."), 44 | 45 | // Unsuccesful because the transaction got expired, because it wasnt picked in time for processing 46 | TXN_EXPIRED: new exports.Error(5,"Transaction expired in the instance where it wasn’t picked in time for processing."), 47 | 48 | // Unsuccesful due to failure in confimation operation 49 | TXN_CONFIRM_FAILED: new exports.Error(6,"Transaction could not be confirmed possibly due to confirm operation failure."), 50 | 51 | // Unsuccesful due to reached maximum account limit for the day 52 | ACCOUNT_LIMIT_REACHED: new exports.Error(8,"Balance would rise above the allowed maximum amount. This happens if the MSISDN has reached its maximum transaction limit for the day."), 53 | 54 | // Unsuccesful because of wrong paybill number 55 | WRONG_PAYBILL: new exports.Error(9,"The store number specified in the transaction could not be found. This happens if the Merchant Pay bill number was incorrectly captured during registration."), 56 | 57 | // Unsuccesful because the account could not be found in the system 58 | ACCOUNT_NOT_FOUND: new exports.Error(10,"This occurs when the system is unable to resolve the credit account i.e the MSISDN provided isn’t registered on M-PESA"), 59 | 60 | // Unsuccesful transaction because the system could not complete the transaction 61 | TXN_FAILED: new exports.Error(11,"This message is returned when the system is unable to complete the transaction."), 62 | 63 | // Unsuccesful because somehow the details are different from when they got captured 64 | TXN_BAD_DATA: new exports.Error(12,"Message returned when if the transaction details are different from original captured request details."), 65 | 66 | // Unsuccesful because the system is down. 67 | SYSTEM_DOWN: new exports.Error(29,"System Downtime message when the system is inaccessible."), 68 | 69 | // Unsuccesful transaction due to inavailability of reference ID 70 | BAD_REFERENCE: new exports.Error(30,"Returned when the request is missing reference ID"), 71 | 72 | // Unsuccesful due to invalid request amount supplied 73 | BAD_AMOUNT: new exports.Error(31,"Returned when the request amount is Invalid or blank"), 74 | 75 | // Unsuccesful beacuse the account is not activated 76 | ACCOUNT_NOT_ACTIVE: new exports.Error(32,"Returned when the account in the request hasn’t been activated."), 77 | 78 | // Unsuccesful beacuse the account is not authorised to make transactions 79 | ACCOUNT_NOT_AUTHORIZED: new exports.Error(33,"Returned when the account hasn’t been approved to transact."), 80 | 81 | 82 | // Unsuccesul beacuse there is delays in transaction processing. 83 | SYSTEM_DELAY: new exports.Error(34,"Returned when there is a request processing delay."), 84 | 85 | // Unsuccesful because the request has been processed before. 86 | DUPLICATE_REQUEST: new exports.Error(35,"Response when a duplicate request is detected."), 87 | 88 | // Unsuccesful because the request credintial supplied are wrong. 89 | BAD_CREDENTIALS: new exports.Error(36,"Response given if incorrect credentials are provided in the request"), 90 | 91 | // Unsuccesful due to missing parameters in the request 92 | BAD_REQUEST: new exports.Error(40,"Missing parameters"), 93 | 94 | // Unsuccesful due to bad format for the MSISDN 95 | BAD_MSISDN: new exports.Error(41, "MSISDN is in incorrect format"), 96 | 97 | // Unsuccesful because the authentication failed. 98 | UNAUTHORIZED: new exports.Error(42, "Authentication Failed") 99 | }; 100 | 101 | exports.BaseRequest = request.defaults({ 102 | headers: { 103 | "User-Agent" : packageJson.name + "/" + packageJson.version, 104 | 'X-Powered-By': packageJson.name 105 | } 106 | }); -------------------------------------------------------------------------------- /lib/services/lnmo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Salama AB 3 | * All rights reserved 4 | * Contact: aksalj@aksalj.com 5 | * Website: http://www.aksalj.com 6 | * 7 | * Project : pesajs 8 | * File : lnmo/index 9 | * Date : 9/5/15 2:34 PM 10 | * Description : M-PESA G2 API; DEVELOPERS GUIDE - LIPA NA M-PESA ONLINE CHECKOUT v 2.0 11 | * 12 | * 1. The Merchant captures the payment details and prepares call to the SAG’s endpoint. 13 | * 2. The Merchant invokes SAG’s processCheckOut interface. 14 | * The SAG validates the request sent and returns a response. 15 | * 3. Merchant receives the processCheckoutResponse parameters namely TRX_ID, ENC_PARAMS, RETURN_CODE, DESCRIPTION and 16 | * CUST_MSG (Customer message). 17 | * 4. The merchant is supposed to display the CUST_MSG to the customer after which the merchant should invoke SAG’s 18 | * confirmPaymentRequest interface to confirm the transaction 19 | * 5. The system will push a USSD menu to the customer and prompt the customer to enter their BONGA PIN and any other 20 | * validation information. 21 | * 6. The transaction is processed on M-PESA and a callback is executed after completion of the transaction. 22 | 23 | * 24 | */ 25 | 'use strict'; 26 | const UTF8 = require("crypto-js/enc-utf8"); 27 | const SHA256 = require("crypto-js/sha256"); 28 | const BASE64 = require("crypto-js/enc-base64"); 29 | const xml2json = require("xml2json"); 30 | 31 | const Const = require("../../constants"); 32 | 33 | const OPERATIONS = { 34 | PAYMENT_RESULT: "LNMOResult", 35 | REQUEST_PAYMENT: "processCheckoutRequest", 36 | QUERY_PAYMENT_STATUS: "transactionStatusRequest", 37 | CONFIRM_PAYMENT: "transactionConfirmRequest" 38 | }; 39 | 40 | let request = Const.BaseRequest; 41 | let DEBUG = false; 42 | 43 | 44 | class PaymentService { 45 | 46 | constructor(params) { 47 | let options = params || {}; 48 | 49 | if (!options.merchant || !options.passkey) throw new Error("Need to specify both merchant ID and passkey"); 50 | 51 | DEBUG = options.debug || false; 52 | 53 | if (DEBUG) { 54 | require('request-debug')(request); 55 | } 56 | 57 | this._makePassword = function (timestamp) { 58 | if (!timestamp) { 59 | timestamp = Date.now(); 60 | } 61 | return BASE64.stringify(UTF8.parse(SHA256(options.merchant + options.passkey + timestamp).toString().toUpperCase())); 62 | }; 63 | 64 | this._parseXMLResponse = function(op, xml) { 65 | 66 | var data = null; 67 | try { 68 | var json = xml2json.toJson(xml,{object: true}); 69 | var body = json["SOAP-ENV:Envelope"]["SOAP-ENV:Body"]; 70 | var response = null; 71 | 72 | if(body.hasOwnProperty("SOAP-ENV:Fault")){ 73 | var fault = body["SOAP-ENV:Fault"]; 74 | data = { 75 | error: new Const.Error(fault.faultcode, fault.faultstring) 76 | }; 77 | return data; 78 | } 79 | 80 | 81 | switch (op) { 82 | case OPERATIONS.PAYMENT_RESULT: 83 | response = body["ns1:ResultMsg"]; 84 | 85 | data = { // FIXME: Seems to be same as OPERATIONS[2] 86 | code: parseInt(response['RETURN_CODE']), 87 | description: response['DESCRIPTION'], 88 | 89 | account: response['MSISDN'], 90 | amount: response['AMOUNT'], 91 | date: response['M-PESA_TRX_DATE'], 92 | status: response['TRX_STATUS'], 93 | mpesa_txn: response['TRX_ID'], 94 | internal_txn: response['M-PESA_TRX_ID'], 95 | transaction: response['MERCHANT_TRANSACTION_ID'], 96 | details: response['ENC_PARAMS'] 97 | }; 98 | break; 99 | case OPERATIONS.REQUEST_PAYMENT: 100 | response = body["ns1:processCheckOutResponse"]; 101 | data = { 102 | code: parseInt(response['RETURN_CODE']), 103 | description: response['DESCRIPTION'], 104 | mpesa_txn: response['TRX_ID'], 105 | message: response['CUST_MSG'], 106 | details: response['ENC_PARAMS'] 107 | }; 108 | break; 109 | case OPERATIONS.QUERY_PAYMENT_STATUS: 110 | response = body["ns1:transactionStatusResponse"]; 111 | 112 | data = { 113 | code: parseInt(response['RETURN_CODE']), 114 | description: response['DESCRIPTION'], 115 | 116 | account: response['MSISDN'], 117 | amount: response['AMOUNT'], 118 | date: response['M-PESA_TRX_DATE'], 119 | status: response['TRX_STATUS'], 120 | mpesa_txn: response['TRX_ID'], 121 | internal_txn: response['M-PESA_TRX_ID'], 122 | transaction: response['MERCHANT_TRANSACTION_ID'], 123 | details: response['ENC_PARAMS'] 124 | }; 125 | break; 126 | case OPERATIONS.CONFIRM_PAYMENT: 127 | response = body["ns1:transactionConfirmResponse"]; 128 | data = { 129 | code: parseInt(response['RETURN_CODE']), 130 | description: response['DESCRIPTION'], 131 | mpesa_txn: response['TRX_ID'], 132 | transaction: response['MERCHANT_TRANSACTION_ID'] 133 | }; 134 | break; 135 | } 136 | 137 | if(data.code && data.code != 0){ 138 | data.error = new Const.Error(data.code, data.description); 139 | } 140 | 141 | } catch(er) { 142 | data = { 143 | error: er 144 | }; 145 | } 146 | 147 | return data; 148 | }; 149 | 150 | this._trigger = function(op, data) { 151 | return new Promise((resolve, reject) => { 152 | 153 | // Auth 154 | data.merchant = options.merchant; 155 | data.password = this._makePassword(data.timestamp || Date.now()); 156 | 157 | 158 | let body = require(`./xml/${op}.xml`)(data); 159 | if (!body) { 160 | return reject(new Error("Could not construct soap message")); 161 | } 162 | 163 | let params = { 164 | url: Const.URLS.CHECKOUT, 165 | method: 'POST', 166 | body: body 167 | }; 168 | request(params, (err, response, result) => { 169 | if (err) { 170 | reject(err); 171 | } else { 172 | let data = this._parseXMLResponse(op, result); 173 | if (data.error) { 174 | reject(data.error); 175 | } else { 176 | resolve(data); 177 | } 178 | } 179 | }); 180 | }); 181 | } 182 | } 183 | 184 | /** 185 | * Initiate payment process 186 | * @param args Cart 187 | */ 188 | requestPayment(args) { 189 | 190 | return new Promise((resolve, reject) => { 191 | 192 | if (!args) { 193 | return reject(new Error("No payment data provided")); 194 | } 195 | 196 | // TODO: Validate args content 197 | /* 198 | args.transaction; 199 | args.ref; 200 | args.account; 201 | args.amount; 202 | args.details; // this is optional 203 | args.callbackUrl; 204 | args.timestamp = Date.now(); 205 | */ 206 | 207 | args.timestamp = args.timestamp || Date.now(); 208 | args.callbackMethod = "xml"; // get | post | xml; documentation not clear on how to send this param. 209 | 210 | 211 | if(DEBUG) { 212 | console.log("D: requesting checkout..."); 213 | } 214 | return this._trigger(OPERATIONS.REQUEST_PAYMENT, args).then(resolve).catch(reject); 215 | }); 216 | } 217 | 218 | /** 219 | * Confirm payment request 220 | * @param args 221 | */ 222 | confirmPayment(args) { 223 | 224 | return new Promise((resolve, reject) => { 225 | 226 | if (!args) { 227 | return reject(new Error("No transaction info provided")); 228 | } 229 | 230 | // TODO: Validate args content 231 | // args.transaction 232 | // args.mpesa_txn 233 | 234 | 235 | if(DEBUG) { 236 | console.log("D: confirming checkout..."); 237 | } 238 | return this._trigger(OPERATIONS.CONFIRM_PAYMENT, args).then(resolve).catch(reject); 239 | }); 240 | 241 | } 242 | 243 | 244 | /** 245 | * Check transaction status 246 | * @param args 247 | */ 248 | getPaymentStatus(args) { 249 | return new Promise((resolve, reject) => { 250 | 251 | if (!args) { 252 | return reject(new Error("No transaction info provided")); 253 | } 254 | 255 | // TODO: Validate args content 256 | // args.transaction 257 | // args.mpesa_txn 258 | 259 | 260 | if(DEBUG) { 261 | console.log("D: getting transaction status..."); 262 | } 263 | return this._trigger(OPERATIONS.QUERY_PAYMENT_STATUS, args).then(resolve).catch(reject); 264 | }); 265 | } 266 | 267 | 268 | /** 269 | * Payment notification express/connect middleware 270 | * @param req 271 | * @param res 272 | * @param next 273 | */ 274 | paymentNotification(req, res, next) { 275 | 276 | let xml = req.body; // GET params? Plain POST? XML POST? 277 | 278 | let data = this._parseXMLResponse(OPERATIONS.PAYMENT_RESULT, xml); 279 | 280 | if (data.error) { 281 | req.payment = null; 282 | 283 | console.error(error); 284 | 285 | res.sendStatus(500); 286 | } else { 287 | req.payment = data; 288 | if (next) { 289 | next(req, res); 290 | } else { 291 | res.sendStatus(200); 292 | } 293 | } 294 | } 295 | } 296 | 297 | exports = module.exports = PaymentService; 298 | --------------------------------------------------------------------------------