├── 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 |
65 |
66 |
67 |
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 |
--------------------------------------------------------------------------------