├── .gitignore ├── .npmignore ├── .jsdoc.json ├── example.env ├── test ├── index.js ├── query.test.js ├── c2b.test.js ├── config.test.js ├── b2c.test.js └── reversal.test.js ├── .travis.yml ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── package.json ├── index.js.map ├── index.js ├── README.md └── src └── transaction.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | .nyc_output 4 | coverage 5 | node_modules 6 | docs 7 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .github 3 | .vscode 4 | .nyc_output 5 | coverage 6 | node_modules 7 | docs 8 | dist 9 | src 10 | test 11 | example.env 12 | package-lock.json 13 | .jsdoc.json 14 | .travis.yml 15 | .gitignore 16 | .codeclimate.yml 17 | index.js.map -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["src", "package.json", "README.md"], 4 | "includePattern": ".js$", 5 | "excludePattern": "(node_modules/|docs)" 6 | }, 7 | "opts": { 8 | "encoding": "utf8", 9 | "readme": "./README.md", 10 | "destination": "docs/", 11 | "recurse": true, 12 | "verbose": true, 13 | "template": "./node_modules/clean-jsdoc-theme" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | PUBLIC_KEY= 2 | API_HOST=api.sandbox.vm.co.mz 3 | API_KEY= 4 | ORIGIN=* 5 | SERVICE_PROVIDER_CODE= 6 | INITIATOR_IDENTIFIER= 7 | SECURITY_CREDENTIAL= 8 | 9 | TEST_AMOUNT=1 10 | TEST_MSISDN= 11 | TEST_REFERENCE=12345 12 | TEST_THIRD_PARTY_REFERENCE= 13 | TEST_QUERY_REFERENCE= 14 | TEST_TRANSACTION_ID= -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | Tx = require('../src/transaction') 3 | 4 | // Loading test suites for individual Tx features 5 | config_tests = require('./config.test') 6 | c2b_tests = require('./c2b.test') 7 | b2c_tests = require('./b2c.test') 8 | query_tests = require('./query.test') 9 | reversal_tests = require('./reversal.test') 10 | 11 | /** @test {Transaction} */ 12 | describe('Transaction', function () { 13 | config_tests(Tx) 14 | c2b_tests(Tx) 15 | b2c_tests(Tx) 16 | query_tests(Tx) 17 | reversal_tests(Tx) 18 | }) 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | env: 5 | global: 6 | - CC_TEST_REPORTER_ID=0c0f562adec669b8f4aed91a94cda433f3605dcc68554cebb11fbf434da552ca 7 | 8 | install: 9 | - npm ci 10 | cache: 11 | directories: 12 | - '$HOME/.npm' 13 | before_script: 14 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 15 | - chmod +x ./cc-test-reporter 16 | - ./cc-test-reporter before-build 17 | 18 | after_script: 19 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 20 | 21 | script: 22 | - npm test 23 | - npm run coverage 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ivan Ruby 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpesa-mz-nodejs-lib", 3 | "version": "0.8.1", 4 | "description": "A node.js library for the M-Pesa Mozambique API", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "npx standard src/transaction.js --fix", 8 | "test": "npx mocha", 9 | "coverage": "nyc --reporter=lcov mocha", 10 | "coverage:html": "nyc --reporter=html mocha", 11 | "build": "npx uglifyjs src/transaction.js --compress --mangle --comments --source-map -o index.js", 12 | "release:git": "npm run build && git add . && git commit && npm version patch && git push", 13 | "release:npm": "npm run build && git add . && git commit && npx np", 14 | "predocs": "npx jsdoc --configure .jsdoc.json --verbose -d docs", 15 | "docs": "start docs", 16 | "docs:mac": "open docs" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ivanruby/mpesa-mz-nodejs-lib.git" 21 | }, 22 | "keywords": [ 23 | "node", 24 | "payment", 25 | "e-commerce", 26 | "mpesa", 27 | "mozambique", 28 | "api" 29 | ], 30 | "author": "Ivan Ruby (https://ivanruby.com)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/ivanruby/mpesa-mz-nodejs-lib/issues" 34 | }, 35 | "homepage": "https://github.com/ivanruby/mpesa-mz-nodejs-lib#readme", 36 | "dependencies": { 37 | "axios": "^0.21.2", 38 | "node-rsa": "^1.0.8" 39 | }, 40 | "devDependencies": { 41 | "clean-jsdoc-theme": "^2.2.11", 42 | "dotenv": "^8.2.0", 43 | "jsdoc": "^3.6.5", 44 | "mocha": "^8.1.1", 45 | "npm-version": "^1.1.0", 46 | "nyc": "^15.1.0", 47 | "standard": "^14.3.4", 48 | "uglify-js": "^3.10.2" 49 | }, 50 | "engines": { 51 | "node": ">14.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/query.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | assert = require('assert') 3 | require('dotenv').config() 4 | 5 | module.exports = function (Tx) { 6 | tx = new Tx({ 7 | public_key: process.env.PUBLIC_KEY, 8 | api_key: process.env.API_KEY, 9 | api_host: process.env.API_HOST, 10 | origin: process.env.ORIGIN, 11 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 12 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 13 | security_credential: process.env.SECURITY_CREDENTIAL 14 | }) 15 | 16 | /** @test {Transaction#Query} */ 17 | describe('Query', function () { 18 | it('Should not initialize if query data object is empty', function () { 19 | assert.throws(function () { 20 | tx.query({}) 21 | }, Error, /Missing or invalid/) 22 | }) 23 | 24 | it('Reference: should be present and non-empty', function () { 25 | assert.throws(function () { 26 | tx.query({ 27 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 28 | }) 29 | }, Error, /Missing or invalid Query parameters: Query Reference/) 30 | 31 | assert.throws(function () { 32 | tx.query({ 33 | query_reference: '', 34 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 35 | }) 36 | }, Error, /Missing or invalid Query parameters: Query Reference/) 37 | }) 38 | 39 | it('Third-party reference: should be present and non-empty', function () { 40 | assert.throws(function () { 41 | tx.query({ 42 | query_reference: process.env.TEST_THIRD_PARTY_REFERENCE 43 | }) 44 | }, Error, /Missing or invalid Query parameters: Query 3rd-party Reference/) 45 | 46 | assert.throws(function () { 47 | tx.query({ 48 | query_reference: process.env.TEST_QUERY_REFERENCE, 49 | third_party_reference: '' 50 | }) 51 | }, Error, /Missing or invalid Query parameters: Query 3rd-party Reference/) 52 | }) 53 | }) 54 | // let transaction_query = { 55 | // query_reference: process.env.TEST_QUERY_REFERENCE, 56 | // third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 57 | // } 58 | // await transaction.query(transaction_query) 59 | // .then(function(response){ 60 | // console.log('Success on Transaction Query') 61 | // console.log(response) 62 | // }) 63 | // .catch(function(response){ 64 | // console.log('Error on Transaction Query') 65 | // console.log(response) 66 | // }) 67 | // }) 68 | } 69 | -------------------------------------------------------------------------------- /test/c2b.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | assert = require('assert') 3 | require('dotenv').config() 4 | 5 | module.exports = function (Tx) { 6 | // Initialize transaction object 7 | tx = Tx({ 8 | public_key: process.env.PUBLIC_KEY, 9 | api_host: process.env.API_HOST, 10 | api_key: process.env.API_KEY, 11 | origin: process.env.ORIGIN, 12 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 13 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 14 | security_credential: process.env.SECURITY_CREDENTIAL 15 | }) 16 | /** @test {Transaction#C2B} */ 17 | describe('C2B', function () { 18 | it('Should not initialize if transaction data object is incomplete or invalid', function () { 19 | assert.throws(function () { 20 | tx.c2b({}) 21 | }, Error, /Missing or invalid configuration parameters/) 22 | }) 23 | 24 | describe('Amount', function () { 25 | it('Should be present', function () { 26 | assert.throws(function () { 27 | tx.c2b({ 28 | msisdn: process.env.TEST_MSISDN, 29 | reference: process.env.TEST_REFERENCE, 30 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 31 | }) 32 | }, Error, /Missing or invalid configuration parameters: {2}C2B Amount/) 33 | }) 34 | 35 | it('Should be a valid number', function () { 36 | assert.throws(function () { 37 | tx.c2b({ 38 | amount: 'a1.5', 39 | msisdn: process.env.TEST_MSISDN, 40 | reference: process.env.TEST_REFERENCE, 41 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 42 | }) 43 | }, Error, /Missing or invalid configuration parameters: {2}C2B Amount/) 44 | }) 45 | 46 | it('Should be greater than zero', function () { 47 | assert.throws(function () { 48 | tx.c2b({ 49 | amount: '-1', 50 | msisdn: process.env.TEST_MSISDN, 51 | reference: process.env.TEST_REFERENCE, 52 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 53 | }) 54 | }, Error, /Missing or invalid configuration parameters: C2B Amount/) 55 | }) 56 | }) 57 | 58 | it('MSISDN: should be present and a valid 9 or 12 digits number', function () { 59 | assert.throws(function () { 60 | tx.c2b({ 61 | amount: process.env.TEST_AMOUNT, 62 | reference: process.env.TEST_REFERENCE, 63 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 64 | }) 65 | }, Error, /Missing or invalid configuration parameters: C2B MSISDN/) 66 | 67 | assert.throws(function () { 68 | tx.c2b({ 69 | amount: process.env.TEST_AMOUNT, 70 | msisdn: '0841234567', 71 | reference: process.env.TEST_REFERENCE, 72 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 73 | }) 74 | }, Error, /Missing or invalid configuration parameters: C2B MSISDN/) 75 | 76 | assert.throws(function () { 77 | tx.c2b({ 78 | amount: process.env.TEST_AMOUNT, 79 | msisdn: '0258841234567', 80 | reference: process.env.TEST_REFERENCE, 81 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 82 | }) 83 | }, Error, /Missing or invalid configuration parameters: C2B MSISDN/) 84 | }) 85 | 86 | it('Reference: should not be empty', function () { 87 | assert.throws(function () { 88 | tx.c2b({ 89 | amount: process.env.TEST_AMOUNT, 90 | msisdn: '841234567', 91 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 92 | }) 93 | }, Error, /Missing or invalid configuration parameters: C2B Reference/) 94 | }) 95 | 96 | it('Third-party reference: should not be empty', function () { 97 | assert.throws(function () { 98 | tx.c2b({ 99 | amount: process.env.TEST_AMOUNT, 100 | msisdn: '841234567' 101 | }) 102 | }, Error, /Missing or invalid configuration parameters: C2B 3rd-Party reference/) 103 | }) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | assert = require('assert') 3 | require('dotenv').config() 4 | 5 | module.exports = function (Tx) { 6 | /** @test {Transaction#Config} */ 7 | describe('Config', function () { 8 | it('Should not initialize if config object is incomplete or non-valid', function () { 9 | assert.throws( 10 | function () { 11 | tx = new Tx() 12 | }, 13 | Error, 14 | /Missing or invalid configuration parameters/ 15 | ) 16 | 17 | assert.throws( 18 | function () { 19 | tx = new Tx({}) 20 | }, 21 | Error, 22 | /Missing or invalid configuration parameters/ 23 | ) 24 | }) 25 | 26 | it('API Host: May be present and non-empty - Optional param', function () { 27 | assert.doesNotThrow( 28 | function () { 29 | tx = new Tx({ 30 | public_key: process.env.PUBLIC_KEY, 31 | api_key: process.env.API_KEY, 32 | origin: process.env.ORIGIN, 33 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 34 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 35 | security_credential: process.env.SECURITY_CREDENTIAL 36 | }) 37 | }, 38 | Error, 39 | /Missing or invalid configuration parameters: API Host/ 40 | ) 41 | 42 | assert.doesNotThrow( 43 | function () { 44 | tx = new Tx({ 45 | public_key: process.env.PUBLIC_KEY, 46 | api_key: process.env.API_KEY, 47 | api_host: '', 48 | origin: process.env.ORIGIN, 49 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 50 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 51 | security_credential: process.env.SECURITY_CREDENTIAL 52 | }) 53 | }, 54 | Error, 55 | /Missing or invalid configuration parameters: API Host/ 56 | ) 57 | }) 58 | 59 | it('API Key: should be present and non-empty', function () { 60 | assert.throws( 61 | function () { 62 | tx = new Tx({ 63 | public_key: process.env.PUBLIC_KEY, 64 | api_host: process.env.API_HOST, 65 | origin: process.env.ORIGIN, 66 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 67 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 68 | security_credential: process.env.SECURITY_CREDENTIAL 69 | }) 70 | }, 71 | Error, 72 | /Missing or invalid configuration parameters: API Key/ 73 | ) 74 | }) 75 | 76 | it('Public Key: should be present and non-empty', function () { 77 | assert.throws( 78 | function () { 79 | tx = new Tx({ 80 | api_host: process.env.API_HOST, 81 | api_key: process.env.API_KEY, 82 | origin: process.env.ORIGIN, 83 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 84 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 85 | security_credential: process.env.SECURITY_CREDENTIAL 86 | }) 87 | }, 88 | Error, 89 | /Missing or invalid configuration parameters: Public Key/ 90 | ) 91 | }) 92 | 93 | it('Origin: should be present and non-empty', function () { 94 | assert.throws( 95 | function () { 96 | tx = new Tx({ 97 | public_key: process.env.PUBLIC_KEY, 98 | api_host: process.env.API_HOST, 99 | api_key: process.env.API_KEY, 100 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 101 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 102 | security_credential: process.env.SECURITY_CREDENTIAL 103 | }) 104 | }, 105 | Error, 106 | /Missing or invalid configuration parameters: Origin/ 107 | ) 108 | }) 109 | 110 | it('Service Provider Code: should be present and non-empty', function () { 111 | assert.throws( 112 | function () { 113 | tx = new Tx({ 114 | public_key: process.env.PUBLIC_KEY, 115 | api_host: process.env.API_HOST, 116 | api_key: process.env.API_KEY, 117 | origin: process.env.ORIGIN, 118 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 119 | security_credential: process.env.SECURITY_CREDENTIAL 120 | }) 121 | }, 122 | Error, 123 | /Missing or invalid configuration parameters: Service Provider Code/ 124 | ) 125 | }) 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /test/b2c.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | assert = require('assert') 3 | require('dotenv').config() 4 | 5 | module.exports = function (Tx) { 6 | // Initialize transaction object 7 | tx = Tx({ 8 | public_key: process.env.PUBLIC_KEY, 9 | api_host: process.env.API_HOST, 10 | api_key: process.env.API_KEY, 11 | origin: process.env.ORIGIN, 12 | service_provider_code: process.env.SERVICE_PROVIDER_CODE, 13 | initiator_identifier: process.env.INITIATOR_IDENTIFIER, 14 | security_credential: process.env.SECURITY_CREDENTIAL 15 | }) 16 | /** @test {Transaction#B2C} */ 17 | describe('B2C', function () { 18 | it('Should not initialize if transaction data object is incomplete or invalid', function () { 19 | assert.throws( 20 | function () { 21 | tx.b2c({}) 22 | }, 23 | Error, 24 | /Missing or invalid configuration parameters/ 25 | ) 26 | }) 27 | 28 | describe('Amount', function () { 29 | it('Should be present', function () { 30 | assert.throws( 31 | function () { 32 | tx.b2c({ 33 | msisdn: process.env.TEST_MSISDN, 34 | reference: process.env.TEST_REFERENCE, 35 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 36 | }) 37 | }, 38 | Error, 39 | /Missing or invalid configuration parameters: {2}B2C Amount/ 40 | ) 41 | }) 42 | 43 | it('Should be a valid number', function () { 44 | assert.throws( 45 | function () { 46 | tx.b2c({ 47 | amount: 'a1.5', 48 | msisdn: process.env.TEST_MSISDN, 49 | reference: process.env.TEST_REFERENCE, 50 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 51 | }) 52 | }, 53 | Error, 54 | /Missing or invalid configuration parameters: {2}B2C Amount/ 55 | ) 56 | }) 57 | 58 | it('Should be greater than zero', function () { 59 | assert.throws( 60 | function () { 61 | tx.b2c({ 62 | amount: '-1', 63 | msisdn: process.env.TEST_MSISDN, 64 | reference: process.env.TEST_REFERENCE, 65 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 66 | }) 67 | }, 68 | Error, 69 | /Missing or invalid configuration parameters: B2C Amount/ 70 | ) 71 | }) 72 | }) 73 | 74 | it('MSISDN: should be present and a valid 9 or 12 digits number', function () { 75 | assert.throws( 76 | function () { 77 | tx.b2c({ 78 | amount: process.env.TEST_AMOUNT, 79 | reference: process.env.TEST_REFERENCE, 80 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 81 | }) 82 | }, 83 | Error, 84 | /Missing or invalid configuration parameters: B2C MSISDN/ 85 | ) 86 | 87 | assert.throws( 88 | function () { 89 | tx.b2c({ 90 | amount: process.env.TEST_AMOUNT, 91 | msisdn: '0841234567', 92 | reference: process.env.TEST_REFERENCE, 93 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 94 | }) 95 | }, 96 | Error, 97 | /Missing or invalid configuration parameters: B2C MSISDN/ 98 | ) 99 | 100 | assert.throws( 101 | function () { 102 | tx.b2c({ 103 | amount: process.env.TEST_AMOUNT, 104 | msisdn: '0258841234567', 105 | reference: process.env.TEST_REFERENCE, 106 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 107 | }) 108 | }, 109 | Error, 110 | /Missing or invalid configuration parameters: B2C MSISDN/ 111 | ) 112 | }) 113 | 114 | it('Reference: should not be empty', function () { 115 | assert.throws( 116 | function () { 117 | tx.b2c({ 118 | amount: process.env.TEST_AMOUNT, 119 | msisdn: '841234567', 120 | third_party_reference: process.env.TEST_THIRD_PARTY_REFERENCE 121 | }) 122 | }, 123 | Error, 124 | /Missing or invalid configuration parameters: B2C Reference/ 125 | ) 126 | }) 127 | 128 | it('Third-party reference: should not be empty', function () { 129 | assert.throws( 130 | function () { 131 | tx.b2c({ 132 | amount: process.env.TEST_AMOUNT, 133 | msisdn: '841234567' 134 | }) 135 | }, 136 | Error, 137 | /Missing or invalid configuration parameters: B2C 3rd-Party reference/ 138 | ) 139 | }) 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["src/transaction.js"],"names":["axios","require","NodeRSA","module","exports","options","this","_public_key","public_key","_api_host","api_host","_api_key","api_key","_origin","origin","_service_provider_code","service_provider_code","_initiator_identifier","initiator_identifier","_security_credential","security_credential","_validMSISDN","_isValidMSISDN","msisdn","isValid","parseInt","length","substring","buffer","_validateAmount","amount","isNaN","parseFloat","validation_errors","_isValidated","type","data","push","reference","third_party_reference","query_reference","transaction_id","_getBearerToken","certificate","setOptions","encryptionScheme","importKey","Buffer","from","token","encrypt","toString","Error","_request_headers","_requestAsPromiseFrom","request","Promise","resolve","reject","then","response","catch","error","toJSON","c2b","transaction_data","method","url","input_ServiceProviderCode","input_CustomerMSISDN","input_Amount","toFixed","input_TransactionReference","input_ThirdPartyReference","headers","query","query_data","reverse","input_ReversalAmount","Number","input_TransactionID","input_InitiatorIdentifier","input_SecurityCredential","b2c","Content-Type","Origin","Authorization"],"mappings":"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"} -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * A Node.JS library for the M-Pesa Mozambique API 3 | * 4 | * @author Ivan Ruby 5 | * @license MIT 6 | */ 7 | var axios=require("axios"),NodeRSA=require("node-rsa");module.exports=function(e){if(this._public_key=e.public_key||"",this._api_host=e.api_host||"api.sandbox.vm.co.mz",this._api_key=e.api_key||"",this._origin=e.origin||"",this._service_provider_code=e.service_provider_code||"",this._initiator_identifier=e.initiator_identifier||"",this._security_credential=e.security_credential||"",this._validMSISDN,this._isValidMSISDN=function(e){return this._validMSISDN="",isValid=!1,"number"==typeof parseInt(e)&&(12===e.length&&"258"===e.substring(0,3)?(buffer=e.substring(3,5),"84"!==buffer&&"85"!==buffer||(this._validMSISDN=e,isValid=!0)):9==e.length&&(buffer=e.substring(0,2),"84"!==buffer&&"85"!==buffer||(this._validMSISDN="258"+e,isValid=!0))),isValid},this._validateAmount=function(e){return!e||""===e||isNaN(parseFloat(e))||parseFloat(e)<=0},this.validation_errors,this._isValidated=function(e,i){switch(this.validation_errors=[],e){case"config":this._api_host&&""!==this._api_host||this.validation_errors.push(" API Host"),this._api_key&&""!==this._api_key||this.validation_errors.push(" API Key"),this._service_provider_code&&""!==this._service_provider_code||this.validation_errors.push(" Service provider code "),this._origin&&""!==this._origin||this.validation_errors.push(" Origin"),this._public_key&&""!==this._public_key||this.validation_errors.push(" Public key");break;case"c2b":this._validateAmount(i.amount)&&this.validation_errors.push(" C2B Amount"),i.msisdn&&""!==i.msisdn&&this._isValidMSISDN(i.msisdn)||this.validation_errors.push(" C2B MSISDN"),i.reference&&""!==i.reference||this.validation_errors.push(" C2B Reference"),i.third_party_reference&&""!==i.third_party_reference||this.validation_errors.push(" C2B 3rd-party Reference");break;case"query":i.query_reference&&""!==i.query_reference||this.validation_errors.push(" Query Reference"),i.third_party_reference&&""!==i.third_party_reference||this.validation_errors.push(" Query 3rd-party Reference");break;case"reversal":this._initiator_identifier&&""!==this._initiator_identifier||this.validation_errors.push(" Initiator Identifier"),this._security_credential&&""!==this._security_credential||this.validation_errors.push(" Security credentials"),this._validateAmount(i.amount)&&this.validation_errors.push(" Reversal Amount"),i.transaction_id&&""!==i.transaction_id||this.validation_errors.push(" Reversal Transaction ID"),i.third_party_reference&&""!==i.third_party_reference||this.validation_errors.push(" Reversal 3rd-party Reference")}return!(0', 63 | api_host: 'api.sandbox.vm.co.mz', 64 | api_key: '', 65 | origin: '', 66 | service_provider_code: '', 67 | initiator_identifier: '', 68 | security_credential: '' 69 | } 70 | 71 | // instantiate the Transaction object, initializing it with valid config 72 | transaction = new Transaction(config) 73 | 74 | // initiate a promise-based C2B transaction 75 | transaction.c2b({ 76 | amount: , 77 | msisdn: '', 78 | reference: '', 79 | third_party_reference: '' 80 | }) 81 | // handle success 82 | .then(function(response){ 83 | console.log(response) 84 | }) 85 | // handle error 86 | .catch(function(error){ 87 | console.log(error) 88 | }) 89 | ``` 90 | 91 | ### Querying status of an existing transaction 92 | 93 | ```javascript 94 | // ... Assuming an initialized Transaction object 95 | 96 | // query the status of an existing transaction 97 | transaction 98 | .query({ 99 | query_reference: '', 100 | third_party_reference: '' 101 | }) 102 | // handle success 103 | .then(function (response) { 104 | console.log(response) 105 | }) 106 | // handle error 107 | .catch(function (error) { 108 | console.log(error) 109 | }) 110 | ``` 111 | 112 | ### Reversal of an existing transaction 113 | 114 | ```javascript 115 | // ... Assuming an initialized Transaction object 116 | 117 | // Reverse a committed transaction 118 | transaction 119 | .reverse({ 120 | amount: '', 121 | transaction_id: '', 122 | third_party_reference: '' 123 | }) 124 | // handle success 125 | .then(function (response) { 126 | console.log(response) 127 | }) 128 | // handle error 129 | .catch(function (error) { 130 | console.log(error) 131 | }) 132 | ``` 133 | 134 | ### Error-handling 135 | 136 | The parameters for the initialization of the `Transaction` object as well as for the `c2b`, `query` and `reversal` methods are validated by the library. 137 | 138 | If any parameter is non-existent, empty or invalid, the library throws the error: 139 | 140 | `Missing or invalid Config/C2B/Query/Reversal parameters` 141 | 142 | The names of the missing or invalid parameters are also appended to the error message. 143 | 144 | Example 145 | 146 | ```js 147 | Transaction = require('mpesa-mz-nodejs-lib') 148 | 149 | // create the config object, missing api_host 150 | var config = { 151 | public_key: '', 152 | api_key: '', 153 | origin: '', 154 | service_provider_code: '', 155 | initiator_identifier: '', 156 | security_credential: '' 157 | } 158 | 159 | // instantiate the Transaction object, initializing it with incomplete config 160 | transaction = new Transaction(config) 161 | ``` 162 | 163 | Will throw a `Missing or invalid Configuration parameter: API Host` error 164 | 165 | And 166 | 167 | ```js 168 | Transaction = require('mpesa-mz-nodejs-lib') 169 | 170 | // create the config object, missing api_host 171 | var config = { 172 | public_key: '', 173 | api_host: '', 174 | api_key: '', 175 | origin: '', 176 | service_provider_code: '', 177 | initiator_identifier: '', 178 | security_credential: '' 179 | } 180 | 181 | // instantiate the Transaction object, initializing it with incomplete config 182 | transaction = new Transaction(config) 183 | 184 | // initiate a C2B transaction, missing reference parameter 185 | transaction.c2b({ 186 | amount: , 187 | msisdn: '', 188 | third_party_reference: '' 189 | }) 190 | // handle success 191 | .then(function(response){ 192 | console.log(response) 193 | }) 194 | // handle error 195 | .catch(function(error){ 196 | console.log(error) 197 | }) 198 | }) 199 | ``` 200 | 201 | Will throw a `Missing or invalid C2B parameter: C2B Reference` error 202 | 203 | ### Responses 204 | 205 | Response format from MPesa API: 206 | 207 | ```JS 208 | { 209 | output_ResponseCode: 'INS-0', 210 | output_ResponseDesc: 'Request processed successfully', 211 | output_ResponseTransactionStatus: 'Completed', 212 | output_ConversationID: '3b46f68931324acb857ae4fe52b826b5', 213 | output_ThirdPartyReference: 'XXXXX' 214 | } 215 | ``` 216 | 217 | The response format provided by the HTTP client library used, ([Axios](https://github.com/axios/axios)), is structured as: 218 | 219 | ``` 220 | { 221 | status: '', 222 | statusText: '', 223 | headers: {}, 224 | config: {}, 225 | data: {} 226 | } 227 | ``` 228 | 229 | In the current version of the library, all returned objects correspond to the `data` property (data returned from MPesa API). 230 | Future versions will distinguish `response` (full Axios response object) from `response.data` in returned messages according to the environment (dev/prod) 231 | 232 | ## Issue templates 233 | 234 | [Bug report](https://github.com/ivanruby/mpesa-mz-nodejs-lib/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) 235 | 236 | [Feature request](https://github.com/ivanruby/mpesa-mz-nodejs-lib/blob/master/.github/ISSUE_TEMPLATE/feature_request.md) 237 | 238 | ## License 239 | 240 | [MIT License](LICENSE) © 2020 Ivan Ruby 241 | -------------------------------------------------------------------------------- /src/transaction.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * A Node.JS library for the M-Pesa Mozambique API 3 | * 4 | * @author Ivan Ruby 5 | * @license MIT 6 | */ 7 | 8 | var axios = require('axios') 9 | var NodeRSA = require('node-rsa') 10 | 11 | /** 12 | * Transaction module - Interacts with the M-Pesa API by exposing c2b, query and reverse methods. 13 | * Throws errors if any of the methods receives incomplete or invalid parameters, including the class constructor 14 | * 15 | * @module Transaction 16 | * @param {object} options 17 | * @param {string} [options.api_host=api.sandbox.vm.co.mz] - Hostname for the API 18 | * @param {string} options.api_key=empty-string - Used for creating authorize trasactions on the API 19 | * @param {string} options.initiator_identifier=empty-string - Provided by Vodacom MZ 20 | * @param {string} options.origin=empty-string - Used for identifying hostname which is sending transaction requests 21 | * @param {string} options.public_key=empty-string - Public Key for the M-Pesa API. Used for generating Authorization bearer tokens 22 | * @param {string} options.security_credential=empty-string - Provided by Vodacom MZ 23 | * @param {number} options.service_provider_code=empty-string - Provided by Vodacom MZ 24 | * @throws 'Missing or invalid configuration parameters' Error if options object is incomplete or invalid 25 | * @example 26 | * Transaction = require('mpesa-mz-nodejs-lib') 27 | * tx = new Transaction(options) 28 | * 29 | * @return {Class} Transaction 30 | */ 31 | module.exports = function (options) { 32 | /** Public key - Required by the M-Pesa API 33 | * @member _public_key 34 | * @type {string} 35 | */ 36 | ;(this._public_key = options.public_key || ''), 37 | /** API Host - Required by the M-Pesa API 38 | * @member _api_host 39 | * @type {string} 40 | */ 41 | (this._api_host = options.api_host || 'api.sandbox.vm.co.mz'), 42 | /** API key - Required by the M-Pesa API 43 | * @member _api_key 44 | * @type {string} 45 | */ 46 | (this._api_key = options.api_key || ''), 47 | /** Origin - Required by the M-Pesa API 48 | * @member _origin 49 | * @type {string} 50 | */ 51 | (this._origin = options.origin || ''), 52 | /** Service Provider Code - Required by the M-Pesa API 53 | * @member _service_provider_code 54 | * @type {number} 55 | */ 56 | (this._service_provider_code = options.service_provider_code || ''), 57 | /** Initiator Identifier - Required by the M-Pesa API 58 | * @member _initiator_identifier 59 | * @type {string} 60 | */ 61 | (this._initiator_identifier = options.initiator_identifier || ''), 62 | /** Security Credential - Required by the M-Pesa API 63 | * @member _security_credential 64 | * @type {string} 65 | */ 66 | (this._security_credential = options.security_credential || ''), 67 | /** 68 | * MSISDN Validation 69 | * @member _validMSISDN - Holds a validated phone number 70 | */ 71 | this._validMSISDN 72 | 73 | /** 74 | * Validates a customer's MSISDN (Phone number) 75 | * 76 | * @member _isValidMSISDN 77 | * @function 78 | * @param {string} msisdn 79 | * @return {boolean} isValid 80 | */ 81 | this._isValidMSISDN = function (msisdn) { 82 | this._validMSISDN = '' 83 | isValid = false 84 | 85 | // Is it a number? 86 | if (typeof parseInt(msisdn) === 'number') { 87 | // Is the length 12 and starts with 258? 88 | if (msisdn.length === 12 && msisdn.substring(0, 3) === '258') { 89 | buffer = msisdn.substring(3, 5) 90 | // Is it an 84 or 85 number? 91 | if (buffer === '84' || buffer === '85') { 92 | this._validMSISDN = msisdn 93 | isValid = true 94 | } 95 | // Otherwise, is the length 9? 96 | } else if (msisdn.length == 9) { 97 | buffer = msisdn.substring(0, 2) 98 | // Is it an 84 or 85 number? 99 | if (buffer === '84' || buffer === '85') { 100 | this._validMSISDN = '258' + msisdn 101 | isValid = true 102 | } 103 | } 104 | } 105 | 106 | return isValid 107 | } 108 | 109 | this._validateAmount = function (amount) { 110 | return ( 111 | !amount || 112 | amount === '' || 113 | isNaN(parseFloat(amount)) || 114 | parseFloat(amount) <= 0 115 | ) 116 | } 117 | 118 | /** 119 | * Validation buffer 120 | * @member validation_errors - Hold array of errors after error validation 121 | */ 122 | this.validation_errors 123 | 124 | /** 125 | * Validates all configuration parameters 126 | * 127 | * @member _isValidated 128 | * @function 129 | * @param {string} type 130 | * @param {object} data 131 | * @return {boolean} 132 | */ 133 | this._isValidated = function (type, data) { 134 | this.validation_errors = [] 135 | 136 | switch (type) { 137 | case 'config': 138 | if (!this._api_host || this._api_host === '') { 139 | this.validation_errors.push(' API Host') 140 | } 141 | 142 | if (!this._api_key || this._api_key === '') { 143 | this.validation_errors.push(' API Key') 144 | } 145 | 146 | if ( 147 | !this._service_provider_code || 148 | this._service_provider_code === '' 149 | ) { 150 | this.validation_errors.push(' Service provider code ') 151 | } 152 | 153 | if (!this._origin || this._origin === '') { 154 | this.validation_errors.push(' Origin') 155 | } 156 | 157 | if (!this._public_key || this._public_key === '') { 158 | this.validation_errors.push(' Public key') 159 | } 160 | break 161 | case 'c2b': 162 | if (this._validateAmount(data.amount)) { 163 | this.validation_errors.push(' C2B Amount') 164 | } 165 | 166 | if ( 167 | !data.msisdn || 168 | data.msisdn === '' || 169 | !this._isValidMSISDN(data.msisdn) 170 | ) { 171 | this.validation_errors.push(' C2B MSISDN') 172 | } 173 | 174 | if (!data.reference || data.reference === '') { 175 | this.validation_errors.push(' C2B Reference') 176 | } 177 | 178 | if (!data.third_party_reference || data.third_party_reference === '') { 179 | this.validation_errors.push(' C2B 3rd-party Reference') 180 | } 181 | 182 | break 183 | case 'query': 184 | if (!data.query_reference || data.query_reference === '') { 185 | this.validation_errors.push(' Query Reference') 186 | } 187 | 188 | if (!data.third_party_reference || data.third_party_reference === '') { 189 | this.validation_errors.push(' Query 3rd-party Reference') 190 | } 191 | 192 | break 193 | case 'reversal': 194 | if (!this._initiator_identifier || this._initiator_identifier === '') { 195 | this.validation_errors.push(' Initiator Identifier') 196 | } 197 | 198 | if (!this._security_credential || this._security_credential === '') { 199 | this.validation_errors.push(' Security credentials') 200 | } 201 | 202 | if (this._validateAmount(data.amount)) { 203 | this.validation_errors.push(' Reversal Amount') 204 | } 205 | 206 | if (!data.transaction_id || data.transaction_id === '') { 207 | this.validation_errors.push(' Reversal Transaction ID') 208 | } 209 | 210 | if (!data.third_party_reference || data.third_party_reference === '') { 211 | this.validation_errors.push(' Reversal 3rd-party Reference') 212 | } 213 | } 214 | 215 | if (this.validation_errors.length > 0) { 216 | return false 217 | } 218 | 219 | return true 220 | } 221 | 222 | /** 223 | * Generates a Bearer Token 224 | * @member _getBearerToken 225 | * @function 226 | * @throws 'Missing or invalid configuration parameters' Error if _public_key or _api_key are missing or invalid from object instantiation 227 | * 228 | * @return {string} bearer_token 229 | */ 230 | this._getBearerToken = function () { 231 | if (this._isValidated('config', {})) { 232 | // Structuring certificate string 233 | certificate = 234 | '-----BEGIN PUBLIC KEY-----\n' + 235 | this._public_key + 236 | '\n-----END PUBLIC KEY-----' 237 | 238 | // Create NodeRSA object with public from formatted certificate 239 | public_key = new NodeRSA() 240 | public_key.setOptions({ encryptionScheme: 'pkcs1' }) 241 | public_key.importKey(Buffer.from(certificate), 'public') 242 | 243 | // Encryt API key (data) using public key 244 | token = public_key.encrypt(Buffer.from(this._api_key)) 245 | 246 | // Return formatted string, Bearer token in base64 format 247 | return 'Bearer ' + Buffer.from(token).toString('base64') 248 | } else { 249 | throw new Error( 250 | 'Missing or invalid configuration parameters:' + 251 | this.validation_errors.toString() 252 | ) 253 | } 254 | } 255 | 256 | /** 257 | * Holds the request headers for each API request 258 | * @member _request_headers 259 | * @function 260 | * @type {object} 261 | * @param {string} _origin - origin value from initialization 262 | * @param {string} _public_key - public_key value from initialization 263 | * @param {string} _api_key - api_key value from initialization 264 | * @throws 'Missing or invalid configuration parameters' Error if _api_key, _origin or _public_key are missing or invalid from object instantiation 265 | */ 266 | this._request_headers = {} 267 | 268 | this._requestAsPromiseFrom = function (request) { 269 | return new Promise(function (resolve, reject) { 270 | axios(request) 271 | .then(function (response) { 272 | resolve(response.data) 273 | }) 274 | .catch(function (error) { 275 | reject(error.toJSON()) 276 | }) 277 | }) 278 | } 279 | 280 | /** 281 | * Initiates a C2B (Client-to-Business) transaction on the M-Pesa API. 282 | * 283 | * @param {object} transaction_data 284 | * @param {float} transaction_data.amount - Value to transfer from Client to Business 285 | * @param {string} transaction_data.msisdn - Client's phone number 286 | * @param {string} transaction_data.reference - Transaction reference (unique) 287 | * @param {string} transaction_data.third_party_reference - Third-party reference provided by Vodacom MZ 288 | * @throws 'Missing or invalid C2B parameters' Error if params are missing or invalid 289 | * @example 290 | * Transaction = require('mpesa-mz-nodejs-lib') 291 | * // Instantiate Transaction object with valid options params 292 | * tx = new Transaction(options) 293 | * 294 | * tx.c2b({ 295 | * amount: 1, 296 | * msisdn: '821234567' 297 | * reference: 'T001', 298 | * third_party_reference: '12345' 299 | * }).then(function(data){ 300 | * console.log(data) 301 | * }).catch(function(error){ 302 | * console.log(error) 303 | * }) 304 | * 305 | * @return {object} Promise 306 | */ 307 | this.c2b = function (transaction_data) { 308 | if (this._isValidated('c2b', transaction_data)) { 309 | request = { 310 | method: 'post', 311 | url: 312 | 'https://' + 313 | this._api_host + 314 | ':18352/ipg/v1x/c2bPayment/singleStage/', 315 | data: { 316 | input_ServiceProviderCode: this._service_provider_code, 317 | input_CustomerMSISDN: this._validMSISDN, 318 | input_Amount: parseFloat(transaction_data.amount).toFixed(2), 319 | input_TransactionReference: transaction_data.reference, 320 | input_ThirdPartyReference: transaction_data.third_party_reference 321 | }, 322 | headers: this._request_headers 323 | } 324 | 325 | return this._requestAsPromiseFrom(request) 326 | } else { 327 | throw new Error( 328 | 'Missing or invalid C2B parameters:' + this.validation_errors.toString() 329 | ) 330 | } 331 | } 332 | 333 | /** 334 | * Initiates a C2B (Client-to-Business) transaction Query on the M-Pesa API. 335 | * 336 | * @param {object} query_data 337 | * @param {string} query_data.query_reference - TransactionID or ConversationID returned from the M-Pesa API 338 | * @param {string} query_data.third_party_reference - Unique reference of the third-party system 339 | * @throws 'Missing or invalid Query parameters' Error is params are missing or invalid 340 | * @example 341 | * Transaction = require('mpesa-mz-nodejs-lib') 342 | * // Instantiate Transaction object with valid params 343 | * tx = new Transaction(options) 344 | * 345 | * tx.query({ 346 | * query_reference:'08y844du6gs', 347 | * third_party_reference:'12345' 348 | * }).then(function(data){ 349 | * console.log(data) 350 | * }).catch(function(error){ 351 | * console.log(error) 352 | * }) 353 | * 354 | * @return {object} Promise 355 | */ 356 | this.query = function (query_data) { 357 | if (this._isValidated('query', query_data)) { 358 | request = { 359 | method: 'get', 360 | url: 361 | 'https://' + 362 | this._api_host + 363 | ':18353/ipg/v1x/queryTransactionStatus/?input_ServiceProviderCode=' + 364 | this._service_provider_code + 365 | '&input_QueryReference=' + 366 | query_data.query_reference + 367 | '&input_ThirdPartyReference=' + 368 | query_data.third_party_reference, 369 | headers: this._request_headers 370 | } 371 | 372 | // If all transaction properties exist and are valid, return promise 373 | return this._requestAsPromiseFrom(request) 374 | } else { 375 | throw new Error( 376 | 'Missing or invalid Query parameters:' + 377 | this.validation_errors.toString() 378 | ) 379 | } 380 | } 381 | 382 | /** 383 | * Initiates a C2B (Client-to-Business) transaction Reversal on the M-Pesa API 384 | * 385 | * @param {object} transaction_data 386 | * @param {number} [transaction_data.amount] - Amount of the transaction 387 | * @param {string} transaction_data.transaction_id - TransactionID returned from the M-Pesa API 388 | * @param {string} transaction_data.third_party_reference - Unique reference of the third-party system 389 | * @throws 'Missing or invalid Reversal parameters' Error if params are missing or invalid 390 | * @example 391 | * Transaction = require('mpesa-mz-nodejs-lib') 392 | * // Instantiate Transaction object with valid params 393 | * tx = new Transaction(options) 394 | * 395 | * tx.reversal({ 396 | * amount:1, 397 | * transaction_id: 'tvfs2503x1d' 398 | * third_party_reference:'12345' 399 | * }).then(function(data){ 400 | * console.log(data) 401 | * }).catch(function(error){ 402 | * console.log(error) 403 | * }) 404 | * 405 | * @return {object} Promise 406 | */ 407 | this.reverse = function (transaction_data) { 408 | if (this._isValidated('reversal', transaction_data)) { 409 | request = { 410 | method: 'put', 411 | url: 'https://' + this._api_host + ':18354/ipg/v1x/reversal/', 412 | data: { 413 | input_ReversalAmount: Number.parseFloat( 414 | transaction_data.amount 415 | ).toFixed(2), 416 | input_TransactionID: transaction_data.transaction_id, 417 | input_ThirdPartyReference: transaction_data.third_party_reference, 418 | input_ServiceProviderCode: this._service_provider_code, 419 | input_InitiatorIdentifier: this._initiator_identifier, 420 | input_SecurityCredential: this._security_credential 421 | }, 422 | headers: this._request_headers 423 | } 424 | 425 | return this._requestAsPromiseFrom(request) 426 | } else { 427 | throw new Error( 428 | 'Missing or invalid Reversal parameters:' + 429 | this.validation_errors.toString() 430 | ) 431 | } 432 | } 433 | 434 | /** 435 | * Initiates a B2C (Business-to-Client) transaction on the M-Pesa API 436 | * 437 | * @param {object} transaction_data 438 | * @param {float} transaction_data.amount - Value to transfer from Business to Client 439 | * @param {string} transaction_data.msisdn - Client's phone number 440 | * @param {string} transaction_data.reference - Transaction reference (unique) 441 | * @param {string} transaction_data.third_party_reference - Third-party reference provided by Vodacom MZ 442 | * @throws 'Missing or invalid B2C parameters' Error if params are missing or invalid 443 | * @example 444 | * Transaction = require('mpesa-mz-nodejs-lib') 445 | * // Instantiate Transaction object with valid options params 446 | * tx = new Transaction(options) 447 | * 448 | * tx.b2c({ 449 | * amount: 1, 450 | * msisdn: '821234567' 451 | * reference: 'T001', 452 | * third_party_reference: '12345' 453 | * }).then(function(data){ 454 | * console.log(data) 455 | * }).catch(function(error){ 456 | * console.log(error) 457 | * }) 458 | * 459 | * @return {object} Promise 460 | */ 461 | this.b2c = function (transaction_data) { 462 | if (this._isValidated('c2b', transaction_data)) { 463 | request = { 464 | method: 'post', 465 | url: 'https://' + this._api_host + ':18345/ipg/v1x/b2cPayment/', 466 | data: { 467 | input_ReversalAmount: Number.parseFloat( 468 | transaction_data.amount 469 | ).toFixed(2), 470 | input_ServiceProviderCode: this._service_provider_code, 471 | input_CustomerMSISDN: this._validMSISDN, 472 | input_Amount: parseFloat(transaction_data.amount).toFixed(2), 473 | input_TransactionReference: transaction_data.reference, 474 | input_ThirdPartyReference: transaction_data.third_party_reference 475 | }, 476 | headers: this._request_headers 477 | } 478 | 479 | return this._requestAsPromiseFrom(request) 480 | } else { 481 | throw new Error( 482 | 'Missing or invalid B2C parameters:' + this.validation_errors.toString() 483 | ) 484 | } 485 | } 486 | 487 | // Validate config data and throw config errors if any param is missing or invalid 488 | if (this._isValidated('config', {})) { 489 | this._request_headers = { 490 | 'Content-Type': 'application/json', 491 | Origin: this._origin, 492 | Authorization: this._getBearerToken() 493 | } 494 | } else { 495 | throw new Error( 496 | 'Missing or invalid configuration parameters:' + 497 | this.validation_errors.toString() 498 | ) 499 | } 500 | } 501 | --------------------------------------------------------------------------------