├── .gitattributes
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── pull_request_template.md
├── .gitignore
├── .npmignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── examples
├── collections.js
└── disbursements.js
├── package-lock.json
├── package.json
├── release.config.js
├── src
├── auth.ts
├── cli.ts
├── client.ts
├── collections.ts
├── common.ts
├── disbursements.ts
├── errors.ts
├── index.ts
├── users.ts
└── validate.ts
├── test
├── auth.test.ts
├── chai.ts
├── client.test.ts
├── collections.test.ts
├── disbursements.test.ts
├── errors.test.ts
├── index.test.ts
├── mocha.opts
├── mock.ts
├── users.test.ts
└── validate.test.ts
├── tsconfig.json
└── tslint.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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/pull_request_template.md:
--------------------------------------------------------------------------------
1 | **IMPORTANT: Please do not create a Pull Request without creating an issue first.**
2 |
3 | *Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.*
4 |
5 | Please provide enough information so that others can review your pull request:
6 |
7 |
8 |
9 | Explain the **details** for making this change. What existing problem does the pull request solve?
10 |
11 |
12 |
13 | **Test plan (required)**
14 |
15 | Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI.
16 |
17 |
18 |
19 | **Code formatting**
20 |
21 |
22 |
23 | **Closing issues**
24 |
25 | Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such).
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | lib
4 |
5 | # dependencies
6 | node_modules/
7 | yarn-error.log
8 | npm-debug*
9 |
10 | # logs
11 | *.log
12 | .env
13 | # coverage
14 | .nyc_output
15 | coverage
16 |
17 | # test output
18 | test/**/out
19 | .DS_Store
20 |
21 | #editors
22 | .idea
23 | .vscode
24 |
25 | #secrets
26 | *.pem
27 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # source code
2 | src/
3 | examples/
4 |
5 | # dependencies
6 | node_modules/
7 | yarn-error.log
8 | npm-debug*
9 |
10 | # logs
11 | *.log
12 | .env
13 |
14 | # coverage
15 | .nyc_output
16 | coverage
17 |
18 | # test output
19 | test/**/out
20 | .DS_Store
21 |
22 | #editors
23 | .idea
24 | .vscode
25 |
26 | #secrets
27 | *.pem
28 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js: 10
4 |
5 | install:
6 | - npm ci
7 |
8 | script:
9 | - npm run lint
10 | - npm run test
11 | - cat ./coverage/lcov.info | npx coveralls
12 | - npm run compile
13 |
14 | deploy:
15 | provider: script
16 | skip_cleanup: true
17 | script:
18 | - npx semantic-release
19 | on:
20 | tags: false
21 | repo: sparkplug/momoapi-node
22 | branch: master
23 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Contributing Guidelines
2 |
3 | 1. Fork this repo. Please be sure to use the current _master_ branch as your starting point:
4 |
5 | ```bash
6 |
7 | https://github.com/sparkplug/momoapi-node
8 |
9 | ```
10 |
11 | 2. You'll be redirected to:
12 |
13 | ```bash
14 |
15 | https://github.com/your-username/momoapi-node
16 |
17 | ```
18 |
19 | 3. Clone the repository:
20 |
21 | ```bash
22 |
23 | git clone https://github.com/your-username/momoapi-node.git
24 |
25 | ```
26 |
27 | 4. Install the project dependencies:
28 |
29 | ```bash
30 |
31 | npm install
32 |
33 | ```
34 |
35 | or
36 |
37 | ```
38 |
39 | yarn
40 |
41 | ```
42 |
43 | 5. Open in the text editor of your choice
44 |
45 | 6. Create a New Branch from _master_: Please make the branch name descriptive.
46 |
47 | ```bash
48 |
49 | cd momoapi-node
50 | git branch new-branch
51 | git checkout new-branch
52 |
53 | ```
54 |
55 | 7. Make your edits locally:
56 |
57 | ```bash
58 |
59 | git add -A
60 |
61 | ```
62 |
63 | 8. Commit the changes:
64 |
65 | ```bash
66 |
67 | git commit -m "Commit Message Here"
68 |
69 | ```
70 |
71 | 9. Submit a pull request:
72 |
73 | ```bash
74 |
75 | git push --set-upstream origin new-branch-name
76 |
77 | ```
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Sparkplug
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MTN MoMo API NodeJS Client
2 |
3 | Power your apps with our MTN MoMo API
4 |
5 |
6 | Join our active, engaged community:
7 |
Website
8 |
|
9 |
Spectrum
10 |
11 |
12 |
13 |
14 | [](https://travis-ci.com/sparkplug/momoapi-node)
15 | [](https://badge.fury.io/js/mtn-momo)
16 | 
17 | [](https://snyk.io/test/npm/mtn-momo)
18 | [](https://coveralls.io/github/sparkplug/momoapi-node?branch=master)
19 | [](https://spectrum.chat/momo-api-developers/)
20 |
21 |
22 | ## Usage
23 |
24 | ### Installation
25 |
26 | Add the library to your project
27 |
28 | ```sh
29 | npm install --save mtn-momo
30 | ```
31 |
32 | ## Sandbox Credentials
33 |
34 | Next, we need to get the `User ID` and `User Secret` and to do this we shall need to use the `Primary Key` for the `Product` to which we are subscribed, as well as specify a `host`. We run the `momo-sandbox` as below.
35 |
36 | ```sh
37 | ## Within a project
38 | npx momo-sandbox --host example.com --primary-key 23e2r2er2342blahblah
39 | ```
40 |
41 | If all goes well, it will print the credentials on the terminal;
42 |
43 | ```sh
44 | Momo Sandbox Credentials {
45 | "userSecret": "b2e23bf4e3984a16a55dbfc2d45f66b0",
46 | "userId": "8ecc7cf3-0db8-4013-9c7b-da4894460041"
47 | }
48 | ```
49 |
50 | These are the credentials we shall use for the `sandbox` environment. In production, these credentials are provided for you on the MTN OVA management dashboard after KYC requirements are met.
51 |
52 | ## Configuration
53 |
54 | Before we can fully utilize the library, we need to specify global configurations. The global configuration must contain the following:
55 |
56 | - `baseUrl`: An optional base url to the MTN Momo API. By default the staging base url will be used
57 | - `environment`: Optional enviroment, either "sandbox" or "production". Default is 'sandbox'
58 | - `callbackHost`: The domain where you webhooks urls are hosted. This is mandatory.
59 |
60 | As an example, you might configure the library like this:
61 |
62 | ```js
63 | const momo = require("mtn-momo");
64 |
65 | const { Collections, Disbursements } = momo.create({
66 | callbackHost: process.env.CALLBACK_HOST
67 | });
68 | ```
69 |
70 | ## Collections
71 |
72 | The collections client can be created with the following paramaters. Note that the `userId` and `userSecret` for production are provided on the MTN OVA dashboard;
73 |
74 | - `primaryKey`: Primary Key for the `Collections` product.
75 | - `userId`: For sandbox, use the one generated with the `momo-sandbox` command.
76 | - `userSecret`: For sandbox, use the one generated with the `momo-sandbox` command.
77 |
78 | You can create a collections client with the following
79 |
80 | ```js
81 | const collections = Collections({
82 | userSecret: process.env.USER_SECRET,
83 | userId: process.env.USER_ID,
84 | primaryKey: process.env.PRIMARY_KEY
85 | });
86 | ```
87 |
88 | #### Methods
89 |
90 | 1. `requestToPay(request: PaymentRequest): Promise`: This operation is used to request a payment from a consumer (Payer). The payer will be asked to authorize the payment. The transaction is executed once the payer has authorized the payment. The transaction will be in status PENDING until it is authorized or declined by the payer or it is timed out by the system. Status of the transaction can be validated by using `getTransaction`
91 |
92 | 2. `getTransaction(transactionId: string): Promise`: Retrieve transaction information using the `transactionId` returned by `requestToPay`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. The error will be a subclass of `MtnMoMoError`. Check [`src/error.ts`](https://github.com/sparkplug/momoapi-node/blob/master/src/errors.ts) for the various errors that can be thrown
93 |
94 | 3. `getBalance(): Promise`: Get the balance of the account.
95 |
96 | 4. `isPayerActive(id: string, type: PartyIdType = "MSISDN"): Promise`: check if an account holder is registered and active in the system.
97 |
98 | #### Sample Code
99 |
100 | ```js
101 | const momo = require("mtn-momo");
102 |
103 | const { Collections } = momo.create({
104 | callbackHost: process.env.CALLBACK_HOST
105 | });
106 |
107 | const collections = Collections({
108 | userSecret: process.env.COLLECTIONS_USER_SECRET,
109 | userId: process.env.COLLECTIONS_USER_ID,
110 | primaryKey: process.env.COLLECTIONS_PRIMARY_KEY
111 | });
112 |
113 | // Request to pay
114 | collections
115 | .requestToPay({
116 | amount: "50",
117 | currency: "EUR",
118 | externalId: "123456",
119 | payer: {
120 | partyIdType: "MSISDN",
121 | partyId: "256774290781"
122 | },
123 | payerMessage: "testing",
124 | payeeNote: "hello"
125 | })
126 | .then(transactionId => {
127 | console.log({ transactionId });
128 |
129 | // Get transaction status
130 | return collections.getTransaction(transactionId);
131 | })
132 | .then(transaction => {
133 | console.log({ transaction });
134 |
135 | // Get account balance
136 | return collections.getBalance();
137 | })
138 | .then(accountBalance => console.log({ accountBalance }))
139 | .catch(error => {
140 | console.log(error);
141 | });
142 | ```
143 |
144 | ## Disbursement
145 |
146 | The disbursements client can be created with the following paramaters. Note that the `userId` and `userSecret` for production are provided on the MTN OVA dashboard;
147 |
148 | - `primaryKey`: Primary Key for the `Disbursements` product.
149 | - `userId`: For sandbox, use the one generated with the `momo-sandbox` command.
150 | - `userSecret`: For sandbox, use the one generated with the `momo-sandbox` command.
151 |
152 | You can create a disbursements client with the following
153 |
154 | ```js
155 | const disbursements = Disbursements({
156 | userSecret: process.env.DISBURSEMENTS_USER_SECRET,
157 | userId: process.env.DISBURSEMENTS_USER_ID,
158 | primaryKey: process.env.DISBURSEMENTS_PRIMARY_KEY
159 | });
160 | ```
161 |
162 | #### Methods
163 |
164 | 1. `transfer(request: TransferRequest): Promise`
165 |
166 | Used to transfer an amount from the owner’s account to a payee account. It returns a transaction id which can use to check the transaction status with the `getTransaction` function
167 |
168 | 2. `getTransaction(transactionId: string): Promise`: Retrieve transaction information using the `transactionId` returned by `transfer`. You can invoke it at intervals until the transaction fails or succeeds. If the transaction has failed, it will throw an appropriate error. The error will be a subclass of `MtnMoMoError`. Check [`src/error.ts`](https://github.com/sparkplug/momoapi-node/blob/master/src/errors.ts) for the various errors that can be thrown
169 |
170 | 3. `getBalance(): Promise`: Get your account balance.
171 |
172 | 4. `isPayerActive(id: string, type: PartyIdType = "MSISDN"): Promise`: This method is used to check if an account holder is registered and active in the system.
173 |
174 | #### Sample Code
175 |
176 | ```js
177 | const momo = require("mtn-momo");
178 |
179 | // initialise momo library
180 | const { Disbursements } = momo.create({
181 | callbackHost: process.env.CALLBACK_HOST
182 | });
183 |
184 | // initialise disbursements
185 | const disbursements = Disbursements({
186 | userSecret: process.env.DISBURSEMENTS_USER_SECRET,
187 | userId: process.env.DISBURSEMENTS_USER_ID,
188 | primaryKey: process.env.DISBURSEMENTS_PRIMARY_KEY
189 | });
190 |
191 | // Transfer
192 | disbursements
193 | .transfer({
194 | amount: "100",
195 | currency: "EUR",
196 | externalId: "947354",
197 | payee: {
198 | partyIdType: "MSISDN",
199 | partyId: "+256776564739"
200 | },
201 | payerMessage: "testing",
202 | payeeNote: "hello",
203 | callbackUrl: "https://75f59b50.ngrok.io"
204 | })
205 | .then(transactionId => {
206 | console.log({ transactionId });
207 |
208 | // Get transaction status
209 | return disbursements.getTransaction(transactionId);
210 | })
211 | .then(transaction => {
212 | console.log({ transaction });
213 |
214 | // Get account balance
215 | return disbursements.getBalance();
216 | })
217 | .then(accountBalance => console.log({ accountBalance }))
218 | .catch(error => {
219 | console.log(error);
220 | });
221 | ```
222 |
223 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | '@commitlint/config-conventional',
4 | ],
5 | };
--------------------------------------------------------------------------------
/examples/collections.js:
--------------------------------------------------------------------------------
1 | const momo = require("../lib/");
2 |
3 | const { Collections } = momo.create({
4 | callbackHost: process.env.CALLBACK_HOST
5 | });
6 |
7 | // initialise collections
8 | const collections = Collections({
9 | userSecret: process.env.COLLECTIONS_USER_SECRET,
10 | userId: process.env.COLLECTIONS_USER_ID,
11 | primaryKey: process.env.COLLECTIONS_PRIMARY_KEY
12 | });
13 |
14 | // Request to pay
15 | collections
16 | .requestToPay({
17 | amount: "50",
18 | currency: "EUR",
19 | externalId: "123456",
20 | payer: {
21 | partyIdType: "MSISDN",
22 | partyId: "256774290781"
23 | },
24 | payerMessage: "testing",
25 | payeeNote: "hello"
26 | })
27 | .then(transactionId => {
28 | console.log({ transactionId });
29 |
30 | // Get transaction status
31 | return collections.getTransaction(transactionId);
32 | })
33 | .then(transaction => {
34 | console.log({ transaction });
35 |
36 | // Get account balance
37 | return collections.getBalance();
38 | })
39 | .then(accountBalance => console.log({ accountBalance }))
40 | .catch(error => {
41 | console.log(error);
42 | });
43 |
--------------------------------------------------------------------------------
/examples/disbursements.js:
--------------------------------------------------------------------------------
1 | const momo = require("../lib");
2 |
3 | const { Disbursements } = momo.create({ callbackHost: process.env.CALLBACK_HOST });
4 |
5 | // initialise collections
6 | const disbursements = Disbursements({
7 | userSecret: process.env.DISBURSEMENTS_USER_SECRET,
8 | userId: process.env.DISBURSEMENTS_USER_ID,
9 | primaryKey: process.env.DISBURSEMENTS_PRIMARY_KEY
10 | });
11 |
12 | const partyId = "256776564739";
13 | const partyIdType = momo.PayerType.MSISDN;
14 | // Transfer
15 | disbursements
16 | .isPayerActive(partyId, partyIdType)
17 | .then((isActive) => {
18 | console.log("Is Active ? ", isActive);
19 | if (!isActive) {
20 | return Promise.reject("Party not active");
21 | }
22 | return disbursements.transfer({
23 | amount: "100",
24 | currency: "EUR",
25 | externalId: "947354",
26 | payee: {
27 | partyIdType,
28 | partyId
29 | },
30 | payerMessage: "testing",
31 | payeeNote: "hello",
32 | callbackUrl: process.env.CALLBACK_URL ? process.env.CALLBACK_URL : "https://75f59b50.ngrok.io"
33 | });
34 | })
35 |
36 | .then(transactionId => {
37 | console.log({ transactionId });
38 |
39 | // Get transaction status
40 | return disbursements.getTransaction(transactionId);
41 | })
42 | .then(transaction => {
43 | console.log({ transaction });
44 |
45 | // Get account balance
46 | return disbursements.getBalance();
47 | })
48 | .then(accountBalance => console.log({ accountBalance }))
49 | .catch(error => {
50 | console.log(error);
51 | });
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mtn-momo",
3 | "version": "0.1.1",
4 | "description": "MTN Mobile Money API Client for NodeJS written in TypeScript",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "scripts": {
8 | "clean": "rm -r lib",
9 | "compile": "tsc -p tsconfig.json",
10 | "lint": "tslint --project tsconfig.json --config tslint.json \"{src,test}/**/*.ts\"",
11 | "test": "NODE_ENV=test nyc mocha",
12 | "commitmsg": "commitlint -e $GIT_PARAMS"
13 | },
14 | "nyc": {
15 | "extension": [
16 | ".ts"
17 | ],
18 | "exclude": [
19 | "**/*.d.ts",
20 | "examples"
21 | ],
22 | "reporter": [
23 | "lcov"
24 | ],
25 | "all": true
26 | },
27 | "keywords": [
28 | "MTN",
29 | "Mobile",
30 | "Money",
31 | "Momo",
32 | "TypeScript",
33 | "NodeJS"
34 | ],
35 | "bin": {
36 | "momo-sandbox": "./lib/cli.js"
37 | },
38 | "license": "ISC",
39 | "dependencies": {
40 | "axios": "^0.21.1",
41 | "commander": "^2.19.0",
42 | "uuid": "^8.3.2"
43 | },
44 | "devDependencies": {
45 | "@commitlint/cli": "^8.1.0",
46 | "@commitlint/config-conventional": "^6.1.3",
47 | "@semantic-release/git": "^4.0.2",
48 | "@types/bluebird": "^3.5.24",
49 | "@types/chai": "^4.1.7",
50 | "@types/chai-as-promised": "^7.1.0",
51 | "@types/mocha": "^5.2.5",
52 | "@types/sinon": "^7.0.4",
53 | "@types/uuid": "^8.3.0",
54 | "axios-mock-adapter": "^1.16.0",
55 | "bluebird": "^3.5.3",
56 | "chai": "^4.2.0",
57 | "chai-as-promised": "^7.1.1",
58 | "coveralls": "^3.0.2",
59 | "cz-conventional-changelog": "^2.1.0",
60 | "husky": "^0.14.3",
61 | "mocha": "^5.2.0",
62 | "mocha-lcov-reporter": "^1.3.0",
63 | "nyc": "^14.1.1",
64 | "semantic-release": "^19.0.3",
65 | "sinon": "^7.2.3",
66 | "ts-node": "^7.0.1",
67 | "tslint": "^5.11.0",
68 | "typescript": "^3.1.6"
69 | },
70 | "directories": {
71 | "lib": "lib"
72 | },
73 | "repository": {
74 | "type": "git",
75 | "url": "git+https://github.com/sparkplug/momoapi-node.git"
76 | },
77 | "bugs": {
78 | "url": "https://github.com/sparkplug/momoapi-node/issues"
79 | },
80 | "homepage": "https://github.com/sparkplug/momoapi-node#readme"
81 | }
82 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "branch": "master",
3 | };
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 |
3 | import { createClient } from "./client";
4 |
5 | import { AccessToken, Config, UserConfig } from "./common";
6 |
7 | export type TokenRefresher = () => Promise;
8 |
9 | export type Authorizer = (
10 | config: Config,
11 | client?: AxiosInstance
12 | ) => Promise;
13 |
14 | interface OAuthCredentials {
15 | accessToken: string;
16 | expires: number;
17 | }
18 |
19 | export function createTokenRefresher(
20 | authorize: Authorizer,
21 | config: Config
22 | ): TokenRefresher {
23 | let credentials: OAuthCredentials;
24 | return () => {
25 | if (isExpired(credentials)) {
26 | return authorize(config)
27 | .then((accessToken: AccessToken) => {
28 | const { access_token, expires_in }: AccessToken = accessToken;
29 | const expires: number = Date.now() + expires_in * 1000 - 60000;
30 | return {
31 | accessToken: access_token,
32 | expires
33 | };
34 | })
35 | .then(freshCredentials => {
36 | credentials = freshCredentials;
37 | return credentials.accessToken;
38 | });
39 | }
40 |
41 | return Promise.resolve(credentials.accessToken);
42 | };
43 | }
44 |
45 | export const authorizeCollections: Authorizer = function(
46 | config: Config,
47 | client: AxiosInstance = createClient(config)
48 | ): Promise {
49 | const basicAuthToken: string = createBasicAuthToken(config);
50 | return client
51 | .post("/collection/token/", null, {
52 | headers: {
53 | Authorization: `Basic ${basicAuthToken}`
54 | }
55 | })
56 | .then(response => response.data);
57 | };
58 |
59 | export const authorizeDisbursements: Authorizer = function(
60 | config: Config,
61 | client: AxiosInstance = createClient(config)
62 | ): Promise {
63 | const basicAuthToken: string = createBasicAuthToken(config);
64 | return client
65 | .post("/disbursement/token/", null, {
66 | headers: {
67 | Authorization: `Basic ${basicAuthToken}`
68 | }
69 | })
70 | .then(response => response.data);
71 | };
72 |
73 | export function createBasicAuthToken(config: UserConfig): string {
74 | return Buffer.from(`${config.userId}:${config.userSecret}`).toString(
75 | "base64"
76 | );
77 | }
78 |
79 | function isExpired(credentials: OAuthCredentials): boolean {
80 | if (!credentials || !credentials.expires) {
81 | return true;
82 | }
83 |
84 | return Date.now() > credentials.expires;
85 | }
86 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import program from "commander";
4 |
5 | import * as momo from "./";
6 | import { Credentials } from "./common";
7 | import { MtnMoMoError } from "./errors";
8 |
9 | const { version } = require("../package.json");
10 |
11 | program
12 | .version(version)
13 | .description("Create sandbox credentials")
14 | .option("-x, --host ", "Your webhook host")
15 | .option("-p, --primary-key ", "Primary Key")
16 | .parse(process.argv);
17 |
18 | const stringify = (obj: object | string) => JSON.stringify(obj, null, 2);
19 |
20 | const { Users } = momo.create({ callbackHost: program.host });
21 |
22 | const users = Users({ primaryKey: program.primaryKey });
23 |
24 | users
25 | .create(program.host)
26 | .then((userId: string) => {
27 | return users.login(userId).then((credentials: Credentials) => {
28 | console.log(
29 | "Momo Sandbox Credentials",
30 | stringify({
31 | userSecret: credentials.apiKey,
32 | userId
33 | })
34 | );
35 | });
36 | })
37 | .catch((error: MtnMoMoError) => {
38 | console.log(error);
39 | });
40 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
2 |
3 | import { handleError } from "./errors";
4 |
5 | import { TokenRefresher } from "./auth";
6 | import { GlobalConfig, SubscriptionConfig } from "./common";
7 |
8 | export function createClient(
9 | config: SubscriptionConfig & GlobalConfig,
10 | client: AxiosInstance = axios.create()
11 | ): AxiosInstance {
12 | client.defaults.baseURL = config.baseUrl;
13 | client.defaults.headers = {
14 | "Ocp-Apim-Subscription-Key": config.primaryKey,
15 | "X-Target-Environment": config.environment || "sandbox"
16 | };
17 |
18 | return withErrorHandling(client);
19 | }
20 |
21 | export function createAuthClient(
22 | refresh: TokenRefresher,
23 | client: AxiosInstance
24 | ): AxiosInstance {
25 | client.interceptors.request.use((request: AxiosRequestConfig) => {
26 | return refresh().then(accessToken => {
27 | return {
28 | ...request,
29 | headers: {
30 | ...request.headers,
31 | Authorization: `Bearer ${accessToken}`
32 | }
33 | };
34 | });
35 | });
36 |
37 | return client;
38 | }
39 |
40 | export function withErrorHandling(client: AxiosInstance): AxiosInstance {
41 | client.interceptors.response.use(
42 | response => response,
43 | error => Promise.reject(handleError(error))
44 | );
45 |
46 | return client;
47 | }
48 |
--------------------------------------------------------------------------------
/src/collections.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 | import { v4 as uuid } from "uuid";
3 |
4 | import { getTransactionError } from "./errors";
5 | import { validateRequestToPay } from "./validate";
6 |
7 | import {
8 | Balance,
9 | FailureReason,
10 | Party,
11 | PartyIdType,
12 | TransactionStatus
13 | } from "./common";
14 |
15 | export interface PaymentRequest {
16 | /**
17 | * Amount that will be debited from the payer account
18 | */
19 | amount: string;
20 |
21 | /**
22 | * ISO4217 Currency
23 | */
24 | currency: string;
25 |
26 | /**
27 | * External id is used as a reference to the transaction.
28 | * External id is used for reconciliation.
29 | * The external id will be included in transaction history report.
30 | * External id is not required to be unique.
31 | */
32 | externalId?: string;
33 |
34 | /**
35 | * Party identifies a account holder in the wallet platform.
36 | * Party consists of two parameters, type and partyId.
37 | * Each type have its own validation of the partyId
38 | * - MSISDN - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
39 | * - EMAIL - Validated to be a valid e-mail format. Validated with IsEmail
40 | * - PARTY_CODE - UUID of the party. Validated with IsUuid
41 | */
42 | payer: Party;
43 |
44 | /**
45 | * Message that will be written in the payer transaction history message field.
46 | */
47 | payerMessage?: string;
48 |
49 | /**
50 | * Message that will be written in the payee transaction history note field.
51 | */
52 | payeeNote?: string;
53 |
54 | /**
55 | * URL to the server where the callback should be sent.
56 | */
57 | callbackUrl?: string;
58 | }
59 |
60 | export interface Payment {
61 | /**
62 | * Financial transactionIdd from mobile money manager.
63 | * Used to connect to the specific financial transaction made in the account
64 | */
65 | financialTransactionId: string;
66 |
67 | /**
68 | * External id provided in the creation of the requestToPay transaction
69 | */
70 | externalId: string;
71 |
72 | /**
73 | * Amount that will be debited from the payer account.
74 | */
75 | amount: string;
76 |
77 | /**
78 | * ISO4217 Currency
79 | */
80 | currency: string;
81 |
82 | /**
83 | * Party identifies a account holder in the wallet platform.
84 | * Party consists of two parameters, type and partyId.
85 | * Each type have its own validation of the partyId
86 | * - MSISDN - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
87 | * - EMAIL - Validated to be a valid e-mail format. Validated with IsEmail
88 | * - PARTY_CODE - UUID of the party. Validated with IsUuid
89 | */
90 | payer: Party;
91 |
92 | /**
93 | * Message that will be written in the payer transaction history message field.
94 | */
95 | payerMessage: string;
96 |
97 | /**
98 | * Message that will be written in the payee transaction history note field.
99 | */
100 | payeeNote: string;
101 |
102 | reason?: FailureReason;
103 |
104 | status: TransactionStatus;
105 | }
106 |
107 | export default class Collections {
108 | private client: AxiosInstance;
109 |
110 | constructor(client: AxiosInstance) {
111 | this.client = client;
112 | }
113 |
114 | /**
115 | * This method is used to request a payment from a consumer (Payer).
116 | * The payer will be asked to authorize the payment. The transaction will
117 | * be executed once the payer has authorized the payment.
118 | * The requesttopay will be in status PENDING until the transaction
119 | * is authorized or declined by the payer or it is timed out by the system.
120 | * Status of the transaction can be validated by using `getTransation`
121 | *
122 | * @param paymentRequest
123 | */
124 | public requestToPay({
125 | callbackUrl,
126 | ...paymentRequest
127 | }: PaymentRequest): Promise {
128 | return validateRequestToPay(paymentRequest).then(() => {
129 | const referenceId: string = uuid();
130 | return this.client
131 | .post("/collection/v1_0/requesttopay", paymentRequest, {
132 | headers: {
133 | "X-Reference-Id": referenceId,
134 | ...(callbackUrl ? { "X-Callback-Url": callbackUrl } : {})
135 | }
136 | })
137 | .then(() => referenceId);
138 | });
139 | }
140 |
141 | /**
142 | * This method is used to retrieve transaction information. You can invoke it
143 | * at intervals until your transaction fails or succeeds.
144 | *
145 | * If the transaction has failed, it will throw an appropriate error. The error will be a subclass
146 | * of `MtnMoMoError`. Check [`src/error.ts`](https://github.com/sparkplug/momoapi-node/blob/master/src/errors.ts)
147 | * for the various errors that can be thrown
148 | *
149 | * @param referenceId the value returned from `requestToPay`
150 | */
151 | public getTransaction(referenceId: string): Promise {
152 | return this.client
153 | .get(`/collection/v1_0/requesttopay/${referenceId}`)
154 | .then(response => response.data)
155 | .then(transaction => {
156 | if (transaction.status === TransactionStatus.FAILED) {
157 | return Promise.reject(getTransactionError(transaction));
158 | }
159 |
160 | return Promise.resolve(transaction);
161 | });
162 | }
163 |
164 | /**
165 | * Get the balance of the account.
166 | */
167 | public getBalance(): Promise {
168 | return this.client
169 | .get("/collection/v1_0/account/balance")
170 | .then(response => response.data);
171 | }
172 |
173 | /**
174 | * This method is used to check if an account holder is registered and active in the system.
175 | *
176 | * @param id Specifies the type of the party ID. Allowed values [msisdn, email, party_code].
177 | * accountHolderId should explicitly be in small letters.
178 | *
179 | * @param type The party number. Validated according to the party ID type (case Sensitive).
180 | * msisdn - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
181 | * email - Validated to be a valid e-mail format. Validated with IsEmail
182 | * party_code - UUID of the party. Validated with IsUuid
183 | */
184 | public isPayerActive(
185 | id: string,
186 | type: PartyIdType = PartyIdType.MSISDN
187 | ): Promise {
188 | return this.client
189 | .get<{result: boolean}>(`/collection/v1_0/accountholder/${type}/${id}/active`)
190 | .then(response => response.data)
191 | .then(data => data.result ? data.result : false);
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/common.ts:
--------------------------------------------------------------------------------
1 | export type Config = GlobalConfig & ProductConfig;
2 |
3 | export type ProductConfig = SubscriptionConfig & UserConfig;
4 |
5 | export interface GlobalConfig {
6 | /**
7 | * The provider callback host
8 | */
9 | callbackHost?: string;
10 |
11 | /**
12 | * The base URL of the EWP system where the transaction shall be processed.
13 | * This parameter is used to route the request to the EWP system that will
14 | * initiate the transaction.
15 | */
16 | baseUrl?: string;
17 |
18 | /**
19 | * The identifier of the EWP system where the transaction shall be processed.
20 | * This parameter is used to route the request to the EWP system that will
21 | * initiate the transaction.
22 | */
23 | environment?: Environment;
24 | }
25 |
26 | export interface SubscriptionConfig {
27 | /**
28 | * Subscription key which provides access to this API. Found in your Profile
29 | */
30 | primaryKey: string;
31 | }
32 |
33 | export interface UserConfig {
34 | /**
35 | * The API user's key
36 | */
37 | userSecret: string;
38 |
39 | /**
40 | * Recource ID for the API user
41 | */
42 | userId: string;
43 | }
44 |
45 | export interface Credentials {
46 | apiKey: string;
47 | }
48 |
49 | export interface AccessToken {
50 | /**
51 | * A JWT token which can be used to authrize against the other API end-points.
52 | * The format of the token follows the JWT standard format (see jwt.io for an example).
53 | * This is the token that should be sent in in the Authorization header when calling the other API end-points.
54 | */
55 | access_token: string;
56 |
57 | /**
58 | * The token type.
59 | *
60 | * TODO: Find list of complete token types
61 | */
62 | token_type: string;
63 |
64 | /**
65 | * The validity time in seconds of the token
66 | */
67 | expires_in: number;
68 | }
69 |
70 | /**
71 | * The available balance of the account
72 | */
73 | export interface Balance {
74 | /**
75 | * The available balance of the account
76 | */
77 | availableBalance: string;
78 |
79 | /**
80 | * ISO4217 Currency
81 | */
82 | currency: string;
83 | }
84 |
85 | export interface Party {
86 | partyIdType: PartyIdType;
87 | partyId: string;
88 | }
89 |
90 | export enum PartyIdType {
91 | MSISDN = "MSISDN",
92 | EMAIL = "EMAIL",
93 | PARTY_CODE = "PARTY_CODE"
94 | }
95 |
96 | export enum Environment {
97 | SANDBOX = "sandbox",
98 | PRODUCTION = "production"
99 | }
100 |
101 | export enum TransactionStatus {
102 | SUCCESSFUL = "SUCCESSFUL",
103 | PENDING = "PENDING",
104 | FAILED = "FAILED"
105 | }
106 |
107 | export enum FailureReason {
108 | PAYEE_NOT_FOUND = "PAYEE_NOT_FOUND",
109 | PAYER_NOT_FOUND = "PAYER_NOT_FOUND",
110 | NOT_ALLOWED = "NOT_ALLOWED",
111 | NOT_ALLOWED_TARGET_ENVIRONMENT = "NOT_ALLOWED_TARGET_ENVIRONMENT",
112 | INVALID_CALLBACK_URL_HOST = "INVALID_CALLBACK_URL_HOST",
113 | INVALID_CURRENCY = "INVALID_CURRENCY",
114 | SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE",
115 | INTERNAL_PROCESSING_ERROR = "INTERNAL_PROCESSING_ERROR",
116 | NOT_ENOUGH_FUNDS = "NOT_ENOUGH_FUNDS",
117 | PAYER_LIMIT_REACHED = "PAYER_LIMIT_REACHED",
118 | PAYEE_NOT_ALLOWED_TO_RECEIVE = "PAYEE_NOT_ALLOWED_TO_RECEIVE",
119 | PAYMENT_NOT_APPROVED = "PAYMENT_NOT_APPROVED",
120 | RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND",
121 | APPROVAL_REJECTED = "APPROVAL_REJECTED",
122 | EXPIRED = "EXPIRED",
123 | TRANSACTION_CANCELED = "TRANSACTION_CANCELED",
124 | RESOURCE_ALREADY_EXIST = "RESOURCE_ALREADY_EXIST"
125 | }
126 |
--------------------------------------------------------------------------------
/src/disbursements.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 | import { v4 as uuid } from "uuid";
3 |
4 | import { getTransactionError } from "./errors";
5 | import { validateTransfer } from "./validate";
6 |
7 | import {
8 | Balance,
9 | FailureReason,
10 | PartyIdType,
11 | TransactionStatus
12 | } from "./common";
13 |
14 | export interface TransferRequest {
15 | /**
16 | * Unique Transfer Reference (UUID v4), will be automatically generated if not explicitly supplied
17 | */
18 | referenceId?: string;
19 | /**
20 | * Amount that will be debited from the payer account.
21 | */
22 | amount: string;
23 |
24 | /**
25 | * ISO4217 Currency
26 | */
27 | currency: string;
28 |
29 | /**
30 | * External id is used as a reference to the transaction.
31 | * External id is used for reconciliation.
32 | * The external id will be included in transaction history report.
33 | * External id is not required to be unique.
34 | */
35 | externalId?: string;
36 |
37 | /**
38 | * Party identifies a account holder in the wallet platform.
39 | * Party consists of two parameters, type and partyId.
40 | * Each type have its own validation of the partyId
41 | * MSISDN - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
42 | * EMAIL - Validated to be a valid e-mail format. Validated with IsEmail
43 | * PARTY_CODE - UUID of the party. Validated with IsUuid
44 | */
45 | payee: {
46 | partyIdType: PartyIdType;
47 | partyId: string;
48 | };
49 |
50 | /**
51 | * Message that will be written in the payer transaction history message field.
52 | */
53 | payerMessage?: string;
54 | /**
55 | * Message that will be written in the payee transaction history note field.
56 | */
57 | payeeNote?: string;
58 | /**
59 | * URL to the server where the callback should be sent.
60 | */
61 | callbackUrl?: string;
62 | }
63 |
64 | export interface Transfer {
65 | /**
66 | * Amount that will be debited from the payer account.
67 | */
68 | amount: string;
69 |
70 | /**
71 | * ISO4217 Currency
72 | */
73 | currency: string;
74 |
75 | /**
76 | * Financial transactionIdd from mobile money manager.
77 | * Used to connect to the specific financial transaction made in the account
78 | */
79 | financialTransactionId: string;
80 |
81 | /**
82 | * External id is used as a reference to the transaction.
83 | * External id is used for reconciliation.
84 | * The external id will be included in transaction history report.
85 | * External id is not required to be unique.
86 | */
87 | externalId: string;
88 |
89 | /**
90 | * Party identifies a account holder in the wallet platform.
91 | * Party consists of two parameters, type and partyId.
92 | * Each type have its own validation of the partyId
93 | * MSISDN - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
94 | * EMAIL - Validated to be a valid e-mail format. Validated with IsEmail
95 | * PARTY_CODE - UUID of the party. Validated with IsUuid
96 | */
97 | payee: {
98 | partyIdType: "MSISDN";
99 | partyId: string;
100 | };
101 | status: TransactionStatus;
102 | reason?: FailureReason;
103 | }
104 |
105 | export default class Disbursements {
106 | private client: AxiosInstance;
107 |
108 | constructor(client: AxiosInstance) {
109 | this.client = client;
110 | }
111 |
112 | /**
113 | * Transfer operation is used to transfer an amount from the owner’s
114 | * account to a payee account.
115 | * Status of the transaction can be validated by using the
116 | *
117 | * @param paymentRequest
118 | */
119 | public transfer({
120 | callbackUrl,
121 | referenceId = uuid(),
122 | ...payoutRequest
123 | }: TransferRequest): Promise {
124 | return validateTransfer({ referenceId, ...payoutRequest }).then(() => {
125 | return this.client
126 | .post("/disbursement/v1_0/transfer", payoutRequest, {
127 | headers: {
128 | "X-Reference-Id": referenceId,
129 | ...(callbackUrl ? { "X-Callback-Url": callbackUrl } : {})
130 | }
131 | })
132 | .then(() => referenceId);
133 | });
134 | }
135 |
136 | /**
137 | * This method is used to retrieve the transaction. You can invoke this method
138 | * to at intervals until your transaction fails or succeeds.
139 | *
140 | * If the transaction has failed, it will throw an appropriate error. The error will be a subclass
141 | * of `MtnMoMoError`. Check [`src/error.ts`](https://github.com/sparkplug/momoapi-node/blob/master/src/errors.ts)
142 | * for the various errors that can be thrown
143 | *
144 | * @param referenceId the value returned from `transfer`
145 | */
146 | public getTransaction(referenceId: string): Promise {
147 | return this.client
148 | .get(`/disbursement/v1_0/transfer/${referenceId}`)
149 | .then(response => response.data)
150 | .then(transaction => {
151 | if (transaction.status === TransactionStatus.FAILED) {
152 | return Promise.reject(getTransactionError(transaction));
153 | }
154 |
155 | return Promise.resolve(transaction);
156 | });
157 | }
158 |
159 | /**
160 | * Get the balance of the account.
161 | */
162 | public getBalance(): Promise {
163 | return this.client
164 | .get("/disbursement/v1_0/account/balance")
165 | .then(response => response.data);
166 | }
167 |
168 | /**
169 | * This method is used to check if an account holder is registered and active in the system.
170 | *
171 | * @param id Specifies the type of the party ID. Allowed values [msisdn, email, party_code].
172 | * accountHolderId should explicitly be in small letters.
173 | *
174 | * @param type The party number. Validated according to the party ID type (case Sensitive).
175 | * msisdn - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
176 | * email - Validated to be a valid e-mail format. Validated with IsEmail
177 | * party_code - UUID of the party. Validated with IsUuid
178 | */
179 | public isPayerActive(
180 | id: string,
181 | type: PartyIdType = PartyIdType.MSISDN
182 | ): Promise {
183 | return this.client
184 | .get<{ result: boolean }>(`/disbursement/v1_0/accountholder/${String(type).toLowerCase()}/${id}/active`)
185 | .then(response => response.data)
186 | .then(data => data.result ? data.result : false);
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/errors.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from "axios";
2 | import { Payment } from "./collections";
3 | import { FailureReason } from "./common";
4 | import { Transfer } from "./disbursements";
5 |
6 | interface ErrorBody {
7 | code: FailureReason;
8 | message: string;
9 | }
10 |
11 | export class MtnMoMoError extends Error {
12 | public transaction?: Payment | Transfer;
13 |
14 | constructor(message?: string) {
15 | super(message);
16 | Object.setPrototypeOf(this, new.target.prototype);
17 | }
18 | }
19 |
20 | export class ApprovalRejectedError extends MtnMoMoError {
21 | public name = "ApprovalRejectedError";
22 | }
23 |
24 | export class ExpiredError extends MtnMoMoError {
25 | public name = "ExpiredError";
26 | }
27 |
28 | export class InternalProcessingError extends MtnMoMoError {
29 | public name = "InternalProcessingError";
30 | }
31 |
32 | export class InvalidCallbackUrlHostError extends MtnMoMoError {
33 | public name = "InvalidCallbackUrlHostError";
34 | }
35 |
36 | export class InvalidCurrencyError extends MtnMoMoError {
37 | public name = "InvalidCurrencyError";
38 | }
39 |
40 | export class NotAllowedTargetEnvironmentError extends MtnMoMoError {
41 | public name = "NotAllowedTargetEnvironmentError";
42 | }
43 |
44 | export class NotAllowedError extends MtnMoMoError {
45 | public name = "NotAllowedError";
46 | }
47 |
48 | export class NotEnoughFundsError extends MtnMoMoError {
49 | public name = "NotEnoughFundsError";
50 | }
51 |
52 | export class PayeeNotFoundError extends MtnMoMoError {
53 | public name = "PayeeNotFoundError";
54 | }
55 |
56 | export class PayeeNotAllowedToReceiveError extends MtnMoMoError {
57 | public name = "PayeeNotAllowedToReceiveError";
58 | }
59 |
60 | export class PayerLimitReachedError extends MtnMoMoError {
61 | public name = "PayerLimitReachedError";
62 | }
63 |
64 | export class PayerNotFoundError extends MtnMoMoError {
65 | public name = "PayerNotFoundError";
66 | }
67 |
68 | export class PaymentNotApprovedError extends MtnMoMoError {
69 | public name = "PaymentNotApprovedError";
70 | }
71 |
72 | export class ResourceAlreadyExistError extends MtnMoMoError {
73 | public name = "ResourceAlreadyExistError";
74 | }
75 |
76 | export class ResourceNotFoundError extends MtnMoMoError {
77 | public name = "ResourceNotFoundError";
78 | }
79 |
80 | export class ServiceUnavailableError extends MtnMoMoError {
81 | public name = "ServiceUnavailableError";
82 | }
83 |
84 | export class TransactionCancelledError extends MtnMoMoError {
85 | public name = "TransactionCancelledError";
86 | }
87 |
88 | export class UnspecifiedError extends MtnMoMoError {
89 | public name = "UnspecifiedError";
90 | }
91 |
92 | export function handleError(error: AxiosError): Error {
93 | if (!error.response) {
94 | return error;
95 | }
96 |
97 | const { code, message }: ErrorBody = error.response.data || {};
98 |
99 | return getError(code, message);
100 | }
101 |
102 | export function getError(code?: FailureReason, message?: string) {
103 | if (code === FailureReason.APPROVAL_REJECTED) {
104 | return new ApprovalRejectedError(message);
105 | }
106 |
107 | if (code === FailureReason.EXPIRED) {
108 | return new ExpiredError(message);
109 | }
110 |
111 | if (code === FailureReason.INTERNAL_PROCESSING_ERROR) {
112 | return new InternalProcessingError(message);
113 | }
114 |
115 | if (code === FailureReason.INVALID_CALLBACK_URL_HOST) {
116 | return new InvalidCallbackUrlHostError(message);
117 | }
118 |
119 | if (code === FailureReason.INVALID_CURRENCY) {
120 | return new InvalidCurrencyError(message);
121 | }
122 |
123 | if (code === FailureReason.NOT_ALLOWED) {
124 | return new NotAllowedError(message);
125 | }
126 |
127 | if (code === FailureReason.NOT_ALLOWED_TARGET_ENVIRONMENT) {
128 | return new NotAllowedTargetEnvironmentError(message);
129 | }
130 |
131 | if (code === FailureReason.NOT_ENOUGH_FUNDS) {
132 | return new NotEnoughFundsError(message);
133 | }
134 |
135 | if (code === FailureReason.PAYEE_NOT_FOUND) {
136 | return new PayeeNotFoundError(message);
137 | }
138 |
139 | if (code === FailureReason.PAYEE_NOT_ALLOWED_TO_RECEIVE) {
140 | return new PayeeNotAllowedToReceiveError(message);
141 | }
142 |
143 | if (code === FailureReason.PAYER_LIMIT_REACHED) {
144 | return new PayerLimitReachedError(message);
145 | }
146 |
147 | if (code === FailureReason.PAYER_NOT_FOUND) {
148 | return new PayerNotFoundError(message);
149 | }
150 |
151 | if (code === FailureReason.PAYMENT_NOT_APPROVED) {
152 | return new PaymentNotApprovedError(message);
153 | }
154 |
155 | if (code === FailureReason.RESOURCE_ALREADY_EXIST) {
156 | return new ResourceAlreadyExistError(message);
157 | }
158 |
159 | if (code === FailureReason.RESOURCE_NOT_FOUND) {
160 | return new ResourceNotFoundError(message);
161 | }
162 |
163 | if (code === FailureReason.SERVICE_UNAVAILABLE) {
164 | return new ServiceUnavailableError(message);
165 | }
166 |
167 | if (code === FailureReason.TRANSACTION_CANCELED) {
168 | return new TransactionCancelledError(message);
169 | }
170 |
171 | return new UnspecifiedError(message);
172 | }
173 |
174 | export function getTransactionError(transaction: Payment | Transfer) {
175 | const error: MtnMoMoError = getError(transaction.reason as FailureReason);
176 | error.transaction = transaction;
177 |
178 | return error;
179 | }
180 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Payment, PaymentRequest } from "./collections";
2 | export { Transfer, TransferRequest } from "./disbursements";
3 | export * from "./errors";
4 | export {
5 | PartyIdType as PayerType,
6 | Party as Payer,
7 | TransactionStatus as Status,
8 | Balance,
9 | Environment,
10 | FailureReason,
11 | GlobalConfig,
12 | ProductConfig
13 | } from "./common";
14 |
15 | import { AxiosInstance } from "axios";
16 |
17 | import Collections from "./collections";
18 | import Disbursements from "./disbursements";
19 | import Users from "./users";
20 |
21 | import {
22 | authorizeCollections,
23 | authorizeDisbursements,
24 | createTokenRefresher
25 | } from "./auth";
26 | import { createAuthClient, createClient } from "./client";
27 | import {
28 | validateGlobalConfig,
29 | validateProductConfig,
30 | validateSubscriptionConfig
31 | } from "./validate";
32 |
33 | import {
34 | Config,
35 | Environment,
36 | GlobalConfig,
37 | ProductConfig,
38 | SubscriptionConfig
39 | } from "./common";
40 |
41 | export interface MomoClient {
42 | Collections(productConfig: ProductConfig): Collections;
43 | Disbursements(productConfig: ProductConfig): Disbursements;
44 | Users(subscription: SubscriptionConfig): Users;
45 | }
46 |
47 | const defaultGlobalConfig: GlobalConfig = {
48 | baseUrl: "https://sandbox.momodeveloper.mtn.com",
49 | environment: Environment.SANDBOX
50 | };
51 |
52 | /**
53 | * Initialise the library
54 | *
55 | * @param globalConfig Global configuration required to use any product
56 | */
57 | export function create(globalConfig: GlobalConfig): MomoClient {
58 | validateGlobalConfig(globalConfig);
59 |
60 | return {
61 | Collections(productConfig: ProductConfig): Collections {
62 | validateProductConfig(productConfig);
63 |
64 | const config: Config = {
65 | ...defaultGlobalConfig,
66 | ...globalConfig,
67 | ...productConfig
68 | };
69 |
70 | const client: AxiosInstance = createAuthClient(
71 | createTokenRefresher(authorizeCollections, config),
72 | createClient(config)
73 | );
74 | return new Collections(client);
75 | },
76 |
77 | Disbursements(productConfig: ProductConfig): Disbursements {
78 | const config: Config = {
79 | ...defaultGlobalConfig,
80 | ...globalConfig,
81 | ...productConfig
82 | };
83 |
84 | const client: AxiosInstance = createAuthClient(
85 | createTokenRefresher(authorizeDisbursements, config),
86 | createClient(config)
87 | );
88 |
89 | return new Disbursements(client);
90 | },
91 |
92 | Users(subscriptionConfig: SubscriptionConfig): Users {
93 | validateSubscriptionConfig(subscriptionConfig);
94 |
95 | const config: GlobalConfig & SubscriptionConfig = {
96 | ...defaultGlobalConfig,
97 | ...globalConfig,
98 | ...subscriptionConfig
99 | };
100 |
101 | const client: AxiosInstance = createClient(config);
102 |
103 | return new Users(client);
104 | }
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/src/users.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 | import { v4 as uuid } from "uuid";
3 |
4 | import { Credentials } from "./common";
5 |
6 | export default class Users {
7 | private client: AxiosInstance;
8 |
9 | constructor(client: AxiosInstance) {
10 | this.client = client;
11 | }
12 |
13 | /**
14 | * Used to create an API user in the sandbox target environment
15 | * @param host The provider callback host
16 | */
17 | public create(host: string): Promise {
18 | const userId: string = uuid();
19 | return this.client
20 | .post(
21 | "/v1_0/apiuser",
22 | { providerCallbackHost: host },
23 | {
24 | headers: {
25 | "X-Reference-Id": userId
26 | }
27 | }
28 | )
29 | .then(() => userId);
30 | }
31 |
32 | /**
33 | * Used to create an API key for an API user in the sandbox target environment.
34 | * @param userId
35 | */
36 | public login(userId: string): Promise {
37 | return this.client
38 | .post(`/v1_0/apiuser/${userId}/apikey`)
39 | .then(response => response.data);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/validate.ts:
--------------------------------------------------------------------------------
1 | import { strictEqual } from "assert";
2 |
3 | import { PaymentRequest } from "./collections";
4 | import {
5 | Environment,
6 | GlobalConfig,
7 | ProductConfig,
8 | SubscriptionConfig,
9 | UserConfig
10 | } from "./common";
11 | import { TransferRequest } from "./disbursements";
12 |
13 | export function validateRequestToPay(
14 | paymentRequest: PaymentRequest
15 | ): Promise {
16 | const { amount, currency, payer }: PaymentRequest = paymentRequest || {};
17 | return Promise.resolve().then(() => {
18 | strictEqual(isTruthy(amount), true, "amount is required");
19 | strictEqual(isNumeric(amount), true, "amount must be a number");
20 | strictEqual(isTruthy(currency), true, "currency is required");
21 | strictEqual(isTruthy(payer), true, "payer is required");
22 | strictEqual(isTruthy(payer.partyId), true, "payer.partyId is required");
23 | strictEqual(
24 | isTruthy(payer.partyIdType),
25 | true,
26 | "payer.partyIdType is required"
27 | );
28 | strictEqual(isString(currency), true, "amount must be a string");
29 | });
30 | }
31 |
32 | export function validateTransfer(
33 | payoutRequest: TransferRequest
34 | ): Promise {
35 | const { amount, currency, payee, referenceId }: TransferRequest = payoutRequest || {};
36 | return Promise.resolve().then(() => {
37 | strictEqual(isTruthy(referenceId), true, "referenceId is required");
38 | strictEqual(isUuid4(referenceId as string), true, "referenceId must be a valid uuid v4");
39 | strictEqual(isTruthy(amount), true, "amount is required");
40 | strictEqual(isNumeric(amount), true, "amount must be a number");
41 | strictEqual(isTruthy(currency), true, "currency is required");
42 | strictEqual(isTruthy(payee), true, "payee is required");
43 | strictEqual(isTruthy(payee.partyId), true, "payee.partyId is required");
44 | strictEqual(
45 | isTruthy(payee.partyIdType),
46 | true,
47 | "payee.partyIdType is required"
48 | );
49 | strictEqual(isString(currency), true, "amount must be a string");
50 | });
51 | }
52 |
53 | export function validateGlobalConfig(config: GlobalConfig): void {
54 | const { callbackHost, baseUrl, environment } = config;
55 | strictEqual(isTruthy(callbackHost), true, "callbackHost is required");
56 |
57 | if (environment && environment !== Environment.SANDBOX) {
58 | strictEqual(
59 | isTruthy(baseUrl),
60 | true,
61 | "baseUrl is required if environment is not sandbox"
62 | );
63 | strictEqual(isString(baseUrl), true, "baseUrl must be a string");
64 | }
65 | }
66 |
67 | export function validateProductConfig(config: ProductConfig): void {
68 | validateSubscriptionConfig(config);
69 | validateUserConfig(config);
70 | }
71 |
72 | export function validateSubscriptionConfig(config: SubscriptionConfig): void {
73 | const { primaryKey } = config;
74 | strictEqual(isTruthy(primaryKey), true, "primaryKey is required");
75 | strictEqual(isString(primaryKey), true, "primaryKey must be a string");
76 | }
77 |
78 | export function validateUserConfig({ userId, userSecret }: UserConfig): void {
79 | strictEqual(isTruthy(userId), true, "userId is required");
80 | strictEqual(isString(userId), true, "userId must be a string");
81 |
82 | strictEqual(isTruthy(userSecret), true, "userSecret is required");
83 | strictEqual(isString(userSecret), true, "userSecret must be a string");
84 |
85 | strictEqual(isUuid4(userId), true, "userId must be a valid uuid v4");
86 | }
87 |
88 | function isNumeric(value: any): boolean {
89 | return !isNaN(parseInt(value, 10));
90 | }
91 |
92 | function isTruthy(value: any): boolean {
93 | return !!value;
94 | }
95 |
96 | function isString(value: any): boolean {
97 | return typeof value === "string";
98 | }
99 |
100 | function isUuid4(value: string): boolean {
101 | return /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
102 | value
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/test/auth.test.ts:
--------------------------------------------------------------------------------
1 | import sinon from "sinon";
2 |
3 | import { expect } from "./chai";
4 |
5 | import {
6 | authorizeCollections,
7 | authorizeDisbursements,
8 | createBasicAuthToken,
9 | createTokenRefresher
10 | } from "../src/auth";
11 | import { createMock } from "./mock";
12 |
13 | import { AccessToken, Config, Environment } from "../src/common";
14 |
15 | describe("Auth", function() {
16 | const config: Config = {
17 | environment: Environment.SANDBOX,
18 | baseUrl: "test",
19 | primaryKey: "key",
20 | userId: "id",
21 | userSecret: "secret"
22 | };
23 |
24 | describe("getTokenRefresher", function() {
25 | context("when the access token is not expired", function() {
26 | it("doesn't call the authorizer for a new access token", function() {
27 | const authorizer = sinon.fake.resolves({
28 | access_token: "token",
29 | token_type: "string",
30 | expires_in: 3600
31 | } as AccessToken);
32 | const refresh = createTokenRefresher(authorizer, config);
33 | return expect(refresh().then(() => refresh())).to.be.fulfilled.then(
34 | () => {
35 | expect(authorizer.callCount).to.eq(1);
36 | }
37 | );
38 | });
39 | });
40 |
41 | context("when the access token expires", function() {
42 | it("calls the authorizer again for a new access token", function() {
43 | const authorizer = sinon.fake.resolves({
44 | access_token: "token",
45 | token_type: "string",
46 | expires_in: -3600
47 | } as AccessToken);
48 | const refresh = createTokenRefresher(authorizer, config);
49 | return expect(refresh().then(() => refresh())).to.be.fulfilled.then(
50 | () => {
51 | expect(authorizer.callCount).to.eq(2);
52 | }
53 | );
54 | });
55 | });
56 | });
57 |
58 | describe("authorizeCollections", function() {
59 | it("makes the correct request", function() {
60 | const [mockClient, mockAdapter] = createMock();
61 | return expect(
62 | authorizeCollections(config, mockClient)
63 | ).to.be.fulfilled.then(() => {
64 | expect(mockAdapter.history.post).to.have.lengthOf(1);
65 | expect(mockAdapter.history.post[0].url).to.eq("/collection/token/");
66 | expect(mockAdapter.history.post[0].headers.Authorization).to.eq(
67 | "Basic " + Buffer.from("id:secret").toString("base64")
68 | );
69 | });
70 | });
71 | });
72 |
73 | describe("authorizeDisbursements", function() {
74 | it("makes the correct request", function() {
75 | const [mockClient, mockAdapter] = createMock();
76 | return expect(
77 | authorizeDisbursements(config, mockClient)
78 | ).to.be.fulfilled.then(() => {
79 | expect(mockAdapter.history.post).to.have.lengthOf(1);
80 | expect(mockAdapter.history.post[0].url).to.eq("/disbursement/token/");
81 | expect(mockAdapter.history.post[0].headers.Authorization).to.eq(
82 | "Basic " + Buffer.from("id:secret").toString("base64")
83 | );
84 | });
85 | });
86 | });
87 |
88 | describe("createBasicAuthToken", function() {
89 | it("encodes id and secret in base64", function() {
90 | expect(
91 | createBasicAuthToken({
92 | userId: "id",
93 | userSecret: "secret"
94 | })
95 | ).to.equal(Buffer.from("id:secret").toString("base64"));
96 | });
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/test/chai.ts:
--------------------------------------------------------------------------------
1 | import * as chai from "chai";
2 | import chaiAsPromised from "chai-as-promised";
3 |
4 | chai.use(chaiAsPromised);
5 |
6 | export = chai;
7 |
--------------------------------------------------------------------------------
/test/client.test.ts:
--------------------------------------------------------------------------------
1 | import sinon from "sinon";
2 |
3 | import { createAuthClient, createClient } from "../src/client";
4 | import { expect } from "./chai";
5 | import { createMock } from "./mock";
6 |
7 | import { Config, Environment } from "../src/common";
8 |
9 | describe("Client", function() {
10 | const config: Config = {
11 | environment: Environment.SANDBOX,
12 | baseUrl: "test",
13 | primaryKey: "key",
14 | userId: "id",
15 | userSecret: "secret"
16 | };
17 |
18 | describe("createClient", function() {
19 | it("creates an axios instance with the right default headers", function() {
20 | const [mockClient] = createMock();
21 | const client = createClient(config, mockClient);
22 | expect(client.defaults.headers).to.have.deep.property(
23 | "Ocp-Apim-Subscription-Key",
24 | "key"
25 | );
26 | expect(client.defaults.headers).to.have.deep.property(
27 | "X-Target-Environment",
28 | "sandbox"
29 | );
30 | });
31 |
32 | it("makes requests with the right headers", function() {
33 | const [mockClient, mockAdapter] = createMock();
34 | const client = createClient(config, mockClient);
35 | return expect(client.get("/test")).to.be.fulfilled.then(() => {
36 | expect(mockAdapter.history.get[0].headers).to.have.deep.property(
37 | "Ocp-Apim-Subscription-Key",
38 | "key"
39 | );
40 | expect(mockAdapter.history.get[0].headers).to.have.deep.property(
41 | "X-Target-Environment",
42 | "sandbox"
43 | );
44 | });
45 | });
46 | });
47 |
48 | describe("createAuthClient", function() {
49 | it("makes requests with the right headers", function() {
50 | const [mockClient, mockAdapter] = createMock();
51 | const refresher = sinon.fake.resolves("token");
52 | const client = createAuthClient(refresher, mockClient);
53 | return expect(client.get("/test")).to.be.fulfilled.then(() => {
54 | expect(mockAdapter.history.get[0].headers).to.have.deep.property(
55 | "Authorization",
56 | "Bearer token"
57 | );
58 | expect(refresher.callCount).to.eq(1);
59 | });
60 | });
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/test/collections.test.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 | import MockAdapter from "axios-mock-adapter";
3 | import { expect } from "chai";
4 |
5 | import Collections from "../src/collections";
6 |
7 | import { createMock } from "./mock";
8 |
9 | import { PaymentRequest } from "../src/collections";
10 | import { PartyIdType } from "../src/common";
11 |
12 | describe("Collections", function() {
13 | let collections: Collections;
14 | let mockAdapter: MockAdapter;
15 | let mockClient: AxiosInstance;
16 |
17 | beforeEach(() => {
18 | [mockClient, mockAdapter] = createMock();
19 | collections = new Collections(mockClient);
20 | });
21 |
22 | describe("requestToPay", function() {
23 | context("when the amount is missing", function() {
24 | it("throws an error", function() {
25 | const request = {} as PaymentRequest;
26 | return expect(collections.requestToPay(request)).to.be.rejectedWith(
27 | "amount is required"
28 | );
29 | });
30 | });
31 |
32 | context("when the amount is not numeric", function() {
33 | it("throws an error", function() {
34 | const request = { amount: "alphabetic" } as PaymentRequest;
35 | return expect(collections.requestToPay(request)).to.be.rejectedWith(
36 | "amount must be a number"
37 | );
38 | });
39 | });
40 |
41 | context("when the currency is missing", function() {
42 | it("throws an error", function() {
43 | const request = {
44 | amount: "1000"
45 | } as PaymentRequest;
46 | return expect(collections.requestToPay(request)).to.be.rejectedWith(
47 | "currency is required"
48 | );
49 | });
50 | });
51 |
52 | context("when the payer is missing", function() {
53 | it("throws an error", function() {
54 | const request = {
55 | amount: "1000",
56 | currency: "UGX"
57 | } as PaymentRequest;
58 | return expect(collections.requestToPay(request)).to.be.rejectedWith(
59 | "payer is required"
60 | );
61 | });
62 | });
63 |
64 | context("when the party id is missing", function() {
65 | it("throws an error", function() {
66 | const request = {
67 | amount: "1000",
68 | currency: "UGX",
69 | payer: {
70 | }
71 | } as PaymentRequest;
72 | return expect(collections.requestToPay(request)).to.be.rejectedWith(
73 | "payer.partyId is required"
74 | );
75 | });
76 | });
77 |
78 | context("when the party id type is missing", function() {
79 | it("throws an error", function() {
80 | const request = {
81 | amount: "1000",
82 | currency: "UGX",
83 | payer: {
84 | partyId: "xxx",
85 | }
86 | } as PaymentRequest;
87 | return expect(collections.requestToPay(request)).to.be.rejectedWith(
88 | "payer.partyIdType is required"
89 | );
90 | });
91 | });
92 |
93 | it("makes the correct request", function() {
94 | const request: PaymentRequest = {
95 | amount: "50",
96 | currency: "EUR",
97 | externalId: "123456",
98 | payer: {
99 | partyIdType: PartyIdType.MSISDN,
100 | partyId: "256774290781"
101 | },
102 | payerMessage: "testing",
103 | payeeNote: "hello"
104 | };
105 | return expect(
106 | collections.requestToPay({ ...request, callbackUrl: "callback url" })
107 | ).to.be.fulfilled.then(() => {
108 | expect(mockAdapter.history.post).to.have.lengthOf(1);
109 | expect(mockAdapter.history.post[0].url).to.eq(
110 | "/collection/v1_0/requesttopay"
111 | );
112 | expect(mockAdapter.history.post[0].data).to.eq(JSON.stringify(request));
113 | expect(mockAdapter.history.post[0].headers["X-Reference-Id"]).to.be.a(
114 | "string"
115 | );
116 | expect(mockAdapter.history.post[0].headers["X-Callback-Url"]).to.eq(
117 | "callback url"
118 | );
119 | });
120 | });
121 | });
122 |
123 | describe("getTransaction", function() {
124 | it("makes the correct request", function() {
125 | return expect(
126 | collections.getTransaction("reference")
127 | ).to.be.fulfilled.then(() => {
128 | expect(mockAdapter.history.get).to.have.lengthOf(1);
129 | expect(mockAdapter.history.get[0].url).to.eq(
130 | "/collection/v1_0/requesttopay/reference"
131 | );
132 | });
133 | });
134 | });
135 |
136 | describe("getBalance", function() {
137 | it("makes the correct request", function() {
138 | return expect(collections.getBalance()).to.be.fulfilled.then(() => {
139 | expect(mockAdapter.history.get).to.have.lengthOf(1);
140 | expect(mockAdapter.history.get[0].url).to.eq(
141 | "/collection/v1_0/account/balance"
142 | );
143 | });
144 | });
145 | });
146 |
147 | describe("isPayerActive", function() {
148 | it("makes the correct request", function() {
149 | return expect(
150 | collections.isPayerActive("0772000000", PartyIdType.MSISDN)
151 | ).to.be.fulfilled.then(() => {
152 | expect(mockAdapter.history.get).to.have.lengthOf(1);
153 | expect(mockAdapter.history.get[0].url).to.eq(
154 | "/collection/v1_0/accountholder/MSISDN/0772000000/active"
155 | );
156 | });
157 | });
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/test/disbursements.test.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 | import MockAdapter from "axios-mock-adapter";
3 | import { expect } from "chai";
4 |
5 | import Disbursements from "../src/disbursements";
6 |
7 | import { createMock } from "./mock";
8 |
9 | import { PartyIdType } from "../src/common";
10 | import { TransferRequest } from "../src/disbursements";
11 |
12 | describe("Disbursements", function() {
13 | let disbursements: Disbursements;
14 | let mockAdapter: MockAdapter;
15 | let mockClient: AxiosInstance;
16 |
17 | beforeEach(() => {
18 | [mockClient, mockAdapter] = createMock();
19 | disbursements = new Disbursements(mockClient);
20 | });
21 |
22 | describe("transfer", function() {
23 | context("when the amount is missing", function() {
24 | it("throws an error", function() {
25 | const request = {} as TransferRequest;
26 | return expect(disbursements.transfer(request)).to.be.rejectedWith(
27 | "amount is required"
28 | );
29 | });
30 | });
31 |
32 | context("when the amount is not numeric", function() {
33 | it("throws an error", function() {
34 | const request = { amount: "alphabetic" } as TransferRequest;
35 | return expect(disbursements.transfer(request)).to.be.rejectedWith(
36 | "amount must be a number"
37 | );
38 | });
39 | });
40 |
41 | context("when the currency is missing", function() {
42 | it("throws an error", function() {
43 | const request = {
44 | amount: "1000"
45 | } as TransferRequest;
46 | return expect(disbursements.transfer(request)).to.be.rejectedWith(
47 | "currency is required"
48 | );
49 | });
50 | });
51 |
52 | context("when the payee is missing", function() {
53 | it("throws an error", function() {
54 | const request = {
55 | amount: "1000",
56 | currency: "UGX"
57 | } as TransferRequest;
58 | return expect(disbursements.transfer(request)).to.be.rejectedWith(
59 | "payee is required"
60 | );
61 | });
62 | });
63 |
64 | context("when the party id is missing", function() {
65 | it("throws an error", function() {
66 | const request = {
67 | amount: "1000",
68 | currency: "UGX",
69 | payee: {
70 | }
71 | } as TransferRequest;
72 | return expect(disbursements.transfer(request)).to.be.rejectedWith(
73 | "payee.partyId is required"
74 | );
75 | });
76 | });
77 |
78 | context("when the party id type is missing", function() {
79 | it("throws an error", function() {
80 | const request = {
81 | amount: "1000",
82 | currency: "UGX",
83 | payee: {
84 | partyId: "xxx",
85 | }
86 | } as TransferRequest;
87 | return expect(disbursements.transfer(request)).to.be.rejectedWith(
88 | "payee.partyIdType is required"
89 | );
90 | });
91 | });
92 |
93 | it("makes the correct request", function() {
94 | const request: TransferRequest = {
95 | amount: "50",
96 | currency: "EUR",
97 | externalId: "123456",
98 | payee: {
99 | partyIdType: PartyIdType.MSISDN,
100 | partyId: "256774290781"
101 | },
102 | payerMessage: "testing",
103 | payeeNote: "hello"
104 | };
105 | return expect(
106 | disbursements.transfer({ ...request, callbackUrl: "callback url" })
107 | ).to.be.fulfilled.then(() => {
108 | expect(mockAdapter.history.post).to.have.lengthOf(1);
109 | expect(mockAdapter.history.post[0].url).to.eq(
110 | "/disbursement/v1_0/transfer"
111 | );
112 | expect(mockAdapter.history.post[0].data).to.eq(JSON.stringify(request));
113 | expect(mockAdapter.history.post[0].headers["X-Reference-Id"]).to.be.a(
114 | "string"
115 | );
116 | expect(mockAdapter.history.post[0].headers["X-Callback-Url"]).to.eq(
117 | "callback url"
118 | );
119 | });
120 | });
121 | });
122 |
123 | describe("getTransaction", function() {
124 | it("makes the correct request", function() {
125 | return expect(
126 | disbursements.getTransaction("reference")
127 | ).to.be.fulfilled.then(() => {
128 | expect(mockAdapter.history.get).to.have.lengthOf(1);
129 | expect(mockAdapter.history.get[0].url).to.eq(
130 | "/disbursement/v1_0/transfer/reference"
131 | );
132 | });
133 | });
134 | });
135 |
136 | describe("getBalance", function() {
137 | it("makes the correct request", function() {
138 | return expect(disbursements.getBalance()).to.be.fulfilled.then(() => {
139 | expect(mockAdapter.history.get).to.have.lengthOf(1);
140 | expect(mockAdapter.history.get[0].url).to.eq(
141 | "/disbursement/v1_0/account/balance"
142 | );
143 | });
144 | });
145 | });
146 |
147 | describe("ispayeeActive", function() {
148 | it("makes the correct request", function() {
149 | return expect(
150 | disbursements.isPayerActive("0772000000", PartyIdType.MSISDN)
151 | ).to.be.fulfilled.then(() => {
152 | expect(mockAdapter.history.get).to.have.lengthOf(1);
153 | expect(mockAdapter.history.get[0].url).to.eq(
154 | "/disbursement/v1_0/accountholder/msisdn/0772000000/active"
155 | );
156 | });
157 | });
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/test/errors.test.ts:
--------------------------------------------------------------------------------
1 | import { FailureReason } from "../src/common";
2 | import {
3 | ApprovalRejectedError,
4 | ExpiredError,
5 | getError,
6 | InternalProcessingError,
7 | InvalidCallbackUrlHostError,
8 | InvalidCurrencyError,
9 | NotAllowedError,
10 | NotAllowedTargetEnvironmentError,
11 | NotEnoughFundsError,
12 | PayeeNotAllowedToReceiveError,
13 | PayeeNotFoundError,
14 | PayerLimitReachedError,
15 | PayerNotFoundError,
16 | PaymentNotApprovedError,
17 | ResourceAlreadyExistError,
18 | ResourceNotFoundError,
19 | ServiceUnavailableError,
20 | TransactionCancelledError,
21 | UnspecifiedError
22 | } from "../src/errors";
23 | import { expect } from "./chai";
24 |
25 | describe("Errors", function() {
26 | describe("getError", function() {
27 | context("when there is no error code", function() {
28 | it("returns unspecified error", function() {
29 | expect(getError()).is.instanceOf(UnspecifiedError);
30 | });
31 | });
32 |
33 | context("when there is an error code", function() {
34 | it("returns the correct error", function() {
35 | expect(getError(FailureReason.APPROVAL_REJECTED, "test message"))
36 | .is.instanceOf(ApprovalRejectedError)
37 | .and.has.property("message", "test message");
38 |
39 | expect(getError(FailureReason.EXPIRED, "test message"))
40 | .is.instanceOf(ExpiredError)
41 | .and.has.property("message", "test message");
42 |
43 | expect(
44 | getError(FailureReason.INTERNAL_PROCESSING_ERROR, "test message")
45 | )
46 | .is.instanceOf(InternalProcessingError)
47 | .and.has.property("message", "test message");
48 |
49 | expect(
50 | getError(FailureReason.INVALID_CALLBACK_URL_HOST, "test message")
51 | )
52 | .is.instanceOf(InvalidCallbackUrlHostError)
53 | .and.has.property("message", "test message");
54 |
55 | expect(getError(FailureReason.INVALID_CURRENCY, "test message"))
56 | .is.instanceOf(InvalidCurrencyError)
57 | .and.has.property("message", "test message");
58 |
59 | expect(getError(FailureReason.NOT_ALLOWED, "test message"))
60 | .is.instanceOf(NotAllowedError)
61 | .and.has.property("message", "test message");
62 |
63 | expect(
64 | getError(FailureReason.NOT_ALLOWED_TARGET_ENVIRONMENT, "test message")
65 | )
66 | .is.instanceOf(NotAllowedTargetEnvironmentError)
67 | .and.has.property("message", "test message");
68 |
69 | expect(getError(FailureReason.NOT_ENOUGH_FUNDS, "test message"))
70 | .is.instanceOf(NotEnoughFundsError)
71 | .and.has.property("message", "test message");
72 |
73 | expect(
74 | getError(FailureReason.PAYEE_NOT_ALLOWED_TO_RECEIVE, "test message")
75 | )
76 | .is.instanceOf(PayeeNotAllowedToReceiveError)
77 | .and.has.property("message", "test message");
78 |
79 | expect(getError(FailureReason.PAYEE_NOT_FOUND, "test message"))
80 | .is.instanceOf(PayeeNotFoundError)
81 | .and.has.property("message", "test message");
82 |
83 | expect(getError(FailureReason.PAYER_LIMIT_REACHED, "test message"))
84 | .is.instanceOf(PayerLimitReachedError)
85 | .and.has.property("message", "test message");
86 |
87 | expect(getError(FailureReason.PAYER_NOT_FOUND, "test message"))
88 | .is.instanceOf(PayerNotFoundError)
89 | .and.has.property("message", "test message");
90 |
91 | expect(getError(FailureReason.PAYMENT_NOT_APPROVED, "test message"))
92 | .is.instanceOf(PaymentNotApprovedError)
93 | .and.has.property("message", "test message");
94 |
95 | expect(getError(FailureReason.RESOURCE_ALREADY_EXIST, "test message"))
96 | .is.instanceOf(ResourceAlreadyExistError)
97 | .and.has.property("message", "test message");
98 |
99 | expect(getError(FailureReason.RESOURCE_NOT_FOUND, "test message"))
100 | .is.instanceOf(ResourceNotFoundError)
101 | .and.has.property("message", "test message");
102 |
103 | expect(getError(FailureReason.SERVICE_UNAVAILABLE, "test message"))
104 | .is.instanceOf(ServiceUnavailableError)
105 | .and.has.property("message", "test message");
106 |
107 | expect(getError(FailureReason.TRANSACTION_CANCELED, "test message"))
108 | .is.instanceOf(TransactionCancelledError)
109 | .and.has.property("message", "test message");
110 | });
111 | });
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { AssertionError } from "assert";
2 |
3 | import * as momo from "../src";
4 |
5 | import { expect } from "./chai";
6 |
7 | describe("MomoClient", function() {
8 | describe("#create", function() {
9 | context("when there is no callback host", function() {
10 | it("throws an error", function() {
11 | expect(momo.create.bind(null, {})).to.throw(AssertionError);
12 | });
13 | });
14 |
15 | context("when there is a callback host", function() {
16 | it("throws doesn't throw an error", function() {
17 | expect(
18 | momo.create.bind(null, { callbackHost: "example.com" })
19 | ).to.not.throw();
20 | });
21 |
22 | it("returns a creator for Collections client", function() {
23 | expect(momo.create({ callbackHost: "example.com" }))
24 | .to.have.property("Collections")
25 | .that.is.a("function");
26 | });
27 |
28 | it("returns a creator for Disbursements client", function() {
29 | expect(momo.create({ callbackHost: "example.com" }))
30 | .to.have.property("Disbursements")
31 | .that.is.a("function");
32 | });
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require ts-node/register
2 | --require source-map-support/register
3 | --recursive
4 | --exit
5 | ./test/**/*.ts
6 |
--------------------------------------------------------------------------------
/test/mock.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance } from "axios";
2 | import MockAdapter from "axios-mock-adapter";
3 |
4 | import { Payment } from "../src/collections";
5 | import { AccessToken, Balance, Credentials } from "../src/common";
6 | import { Transfer } from "../src/disbursements";
7 |
8 | export function createMock(): [AxiosInstance, MockAdapter] {
9 | const client = axios.create({
10 | headers: {
11 | "Content-Type": "application/json"
12 | }
13 | });
14 |
15 | const mock = new MockAdapter(client);
16 |
17 | mock.onGet("/test").reply(200);
18 |
19 | mock.onPost("/v1_0/apiuser").reply(201);
20 |
21 | mock.onPost(/\/v1_0\/apiuser\/[\w\-]+\/apikey/).reply(200, {
22 | apiKey: "api-key"
23 | } as Credentials);
24 |
25 | mock.onPost("/collection/token/").reply(200, {
26 | access_token: "token",
27 | token_type: "access_token",
28 | expires_in: 3600
29 | } as AccessToken);
30 |
31 | mock
32 | .onGet(/\/collection\/v1_0\/accountholder\/(MSISDN|EMAIL|PARTY_CODE)\/\w+/)
33 | .reply(200, "true");
34 |
35 | mock.onPost("/collection/v1_0/requesttopay").reply(201);
36 |
37 | mock.onGet(/\/collection\/v1_0\/requesttopay\/[\w\-]+/).reply(200, {
38 | financialTransactionId: "tx id",
39 | externalId: "string",
40 | amount: "2000",
41 | currency: "UGX",
42 | payer: {
43 | partyIdType: "MSISDN",
44 | partyId: "256772000000"
45 | },
46 | payerMessage: "test",
47 | payeeNote: "test",
48 | status: "SUCCESSFUL"
49 | } as Payment);
50 |
51 | mock.onGet("/collection/v1_0/account/balance").reply(200, {
52 | availableBalance: "2000",
53 | currency: "UGX"
54 | } as Balance);
55 |
56 | mock.onPost("/disbursement/token/").reply(200, {
57 | access_token: "token",
58 | token_type: "access_token",
59 | expires_in: 3600
60 | } as AccessToken);
61 |
62 | mock
63 | .onGet(
64 | /\/disbursement\/v1_0\/accountholder\/(msisdn|email|party_code)\/\w+/
65 | )
66 | .reply(200, "true");
67 |
68 | mock.onPost("/disbursement/v1_0/transfer").reply(201);
69 |
70 | mock.onGet(/\/disbursement\/v1_0\/transfer\/[\w\-]+/).reply(200, {
71 | financialTransactionId: "tx id",
72 | externalId: "string",
73 | amount: "2000",
74 | currency: "UGX",
75 | payee: {
76 | partyIdType: "MSISDN",
77 | partyId: "256772000000"
78 | },
79 | status: "SUCCESSFUL"
80 | } as Transfer);
81 |
82 | mock.onGet("/disbursement/v1_0/account/balance").reply(200, {
83 | availableBalance: "2000",
84 | currency: "UGX"
85 | } as Balance);
86 |
87 | return [client, mock];
88 | }
89 |
--------------------------------------------------------------------------------
/test/users.test.ts:
--------------------------------------------------------------------------------
1 | import { AxiosInstance } from "axios";
2 | import MockAdapter from "axios-mock-adapter/types";
3 | import { expect } from "./chai";
4 |
5 | import Users from "../src/users";
6 | import { createMock } from "./mock";
7 |
8 | describe("Users", function() {
9 | let users: Users;
10 | let mockAdapter: MockAdapter;
11 | let mockClient: AxiosInstance;
12 |
13 | beforeEach(() => {
14 | [mockClient, mockAdapter] = createMock();
15 | users = new Users(mockClient);
16 | });
17 |
18 | describe("create", function() {
19 | it("makes the correct request", function() {
20 | return expect(users.create("host")).to.be.fulfilled.then(() => {
21 | expect(mockAdapter.history.post).to.have.lengthOf(1);
22 | expect(mockAdapter.history.post[0].url).to.eq("/v1_0/apiuser");
23 | expect(mockAdapter.history.post[0].data).to.eq(
24 | JSON.stringify({ providerCallbackHost: "host" })
25 | );
26 | expect(mockAdapter.history.post[0].headers["X-Reference-Id"]).to.be.a(
27 | "string"
28 | );
29 | });
30 | });
31 | });
32 |
33 | describe("login", function() {
34 | it("makes the correct request", function() {
35 | return expect(users.login("id")).to.be.fulfilled.then(() => {
36 | expect(mockAdapter.history.post).to.have.lengthOf(1);
37 | expect(mockAdapter.history.post[0].url).to.eq(
38 | "/v1_0/apiuser/id/apikey"
39 | );
40 | });
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/test/validate.test.ts:
--------------------------------------------------------------------------------
1 | import { AssertionError } from "assert";
2 | import { v4 as uuid } from "uuid";
3 |
4 | import { PaymentRequest } from "../src/collections";
5 | import { expect } from "./chai";
6 |
7 | import { Environment, PartyIdType, ProductConfig, SubscriptionConfig, UserConfig } from "../src/common";
8 | import { TransferRequest } from "../src/disbursements";
9 | import {
10 | validateGlobalConfig,
11 | validateProductConfig,
12 | validateRequestToPay,
13 | validateSubscriptionConfig,
14 | validateTransfer,
15 | validateUserConfig
16 | } from "../src/validate";
17 |
18 | describe("Validate", function() {
19 | describe("validateGlobalConfig", function() {
20 | context("when callbackHost is not specified", function() {
21 | it("throws an error", function() {
22 | expect(validateGlobalConfig.bind(null, {})).to.throw(
23 | AssertionError,
24 | "callbackHost is required"
25 | );
26 | });
27 | });
28 |
29 | context("when callbackHost is specified", function() {
30 | it("doesn't throw", function() {
31 | expect(
32 | validateGlobalConfig.bind(null, { callbackHost: "example.com" })
33 | ).to.not.throw();
34 | });
35 | });
36 |
37 | context("when environment is specified", function() {
38 | context("and is not sandbox", function() {
39 | context("and baseUrl is not specified", function() {
40 | it("throws", function() {
41 | expect(
42 | validateGlobalConfig.bind(null, {
43 | callbackHost: "example.com",
44 | environment: Environment.PRODUCTION
45 | })
46 | ).to.throw(
47 | AssertionError,
48 | "baseUrl is required if environment is not sandbox"
49 | );
50 | });
51 | });
52 |
53 | context("and baseUrl is specified", function() {
54 | it("doesn't throw", function() {
55 | expect(
56 | validateGlobalConfig.bind(null, {
57 | callbackHost: "example.com",
58 | environment: Environment.PRODUCTION,
59 | baseUrl: "mtn production base url"
60 | })
61 | ).to.not.throw();
62 | });
63 | });
64 | });
65 | });
66 | });
67 |
68 | describe("validateProductConfig", function() {
69 | context("when primaryKey is not specified", function() {
70 | it("throws an error", function() {
71 | expect(validateProductConfig.bind(null, {} as ProductConfig)).to.throw(
72 | AssertionError,
73 | "primaryKey is required"
74 | );
75 | });
76 | });
77 |
78 | context("when userId is not specified", function() {
79 | it("throws an error", function() {
80 | expect(
81 | validateProductConfig.bind(null, {
82 | primaryKey: "test primary key"
83 | } as ProductConfig)
84 | ).to.throw(AssertionError, "userId is required");
85 | });
86 | });
87 |
88 | context("when userSecret is not specified", function() {
89 | it("throws an error", function() {
90 | expect(
91 | validateProductConfig.bind(null, {
92 | primaryKey: "test primary key",
93 | userId: "test user id"
94 | } as ProductConfig)
95 | ).to.throw(AssertionError, "userSecret is required");
96 | });
97 | });
98 |
99 | context("when userId is not a valid uuid", function() {
100 | it("throws an error", function() {
101 | expect(
102 | validateProductConfig.bind(null, {
103 | primaryKey: "test primary key",
104 | userId: "test user id",
105 | userSecret: "test user secret"
106 | })
107 | ).to.throw(AssertionError, "userId must be a valid uuid v4");
108 | });
109 | });
110 |
111 | context("when the config is valid", function() {
112 | it("throws an error", function() {
113 | expect(
114 | validateProductConfig.bind(null, {
115 | primaryKey: "test primary key",
116 | userId: uuid(),
117 | userSecret: "test user secret"
118 | })
119 | ).to.not.throw();
120 | });
121 | });
122 | });
123 |
124 | describe("validateSubscriptionConfig", function() {
125 | context("when primaryKey is not specified", function() {
126 | it("throws an error", function() {
127 | expect(validateSubscriptionConfig.bind(null, {} as SubscriptionConfig)).to.throw(
128 | AssertionError,
129 | "primaryKey is required"
130 | );
131 | });
132 | });
133 |
134 | context("when primaryKey is specified", function() {
135 | it("throws an error", function() {
136 | expect(
137 | validateSubscriptionConfig.bind(null, {
138 | primaryKey: "test primary key"
139 | })
140 | ).to.not.throw();
141 | });
142 | });
143 | });
144 |
145 | describe("validateUserConfig", function() {
146 | context("when userId is not specified", function() {
147 | it("throws an error", function() {
148 | expect(validateUserConfig.bind(null, {} as UserConfig)).to.throw(
149 | AssertionError,
150 | "userId is required"
151 | );
152 | });
153 | });
154 |
155 | context("when userSecret is not specified", function() {
156 | it("throws an error", function() {
157 | expect(
158 | validateUserConfig.bind(null, {
159 | userId: "test user id"
160 | } as UserConfig)
161 | ).to.throw(AssertionError, "userSecret is required");
162 | });
163 | });
164 |
165 | context("when userId is not a valid uuid", function() {
166 | it("throws an error", function() {
167 | expect(
168 | validateUserConfig.bind(null, {
169 | userId: "test user id",
170 | userSecret: "test user secret"
171 | })
172 | ).to.throw(AssertionError, "userId must be a valid uuid v4");
173 | });
174 | });
175 |
176 | context("when the config is valid", function() {
177 | it("throws an error", function() {
178 | expect(
179 | validateUserConfig.bind(null, {
180 | userId: uuid(),
181 | userSecret: "test user secret"
182 | })
183 | ).to.not.throw();
184 | });
185 | });
186 | });
187 |
188 | describe("validateRequestToPay", function() {
189 | context("when the amount is missing", function() {
190 | it("throws an error", function() {
191 | const request = {} as PaymentRequest;
192 | return expect(validateRequestToPay(request)).to.be.rejectedWith(
193 | "amount is required"
194 | );
195 | });
196 | });
197 |
198 | context("when the amount is not numeric", function() {
199 | it("throws an error", function() {
200 | const request = { amount: "alphabetic" } as PaymentRequest;
201 | return expect(validateRequestToPay(request)).to.be.rejectedWith(
202 | "amount must be a number"
203 | );
204 | });
205 | });
206 |
207 | context("when the currency is missing", function() {
208 | it("throws an error", function() {
209 | const request = {
210 | amount: "1000"
211 | } as PaymentRequest;
212 | return expect(validateRequestToPay(request)).to.be.rejectedWith(
213 | "currency is required"
214 | );
215 | });
216 | });
217 |
218 | context("when the payer is missing", function() {
219 | it("throws an error", function() {
220 | const request = {
221 | amount: "1000",
222 | currency: "UGX"
223 | } as PaymentRequest;
224 | return expect(validateRequestToPay(request)).to.be.rejectedWith(
225 | "payer is required"
226 | );
227 | });
228 | });
229 |
230 | context("when the party id is missing", function() {
231 | it("throws an error", function() {
232 | const request = {
233 | amount: "1000",
234 | currency: "UGX",
235 | payer: {}
236 | } as PaymentRequest;
237 | return expect(validateRequestToPay(request)).to.be.rejectedWith(
238 | "payer.partyId is required"
239 | );
240 | });
241 | });
242 |
243 | context("when the party id type is missing", function() {
244 | it("throws an error", function() {
245 | const request = {
246 | amount: "1000",
247 | currency: "UGX",
248 | payer: {
249 | partyId: "xxx"
250 | }
251 | } as PaymentRequest;
252 | return expect(validateRequestToPay(request)).to.be.rejectedWith(
253 | "payer.partyIdType is required"
254 | );
255 | });
256 | });
257 |
258 | context("when the request is valid", function() {
259 | it("fulfills", function() {
260 | const request = {
261 | amount: "1000",
262 | currency: "UGX",
263 | payer: {
264 | partyId: "xxx",
265 | partyIdType: PartyIdType.MSISDN
266 | }
267 | } as PaymentRequest;
268 | return expect(validateRequestToPay(request)).to.be.fulfilled;
269 | });
270 | });
271 | });
272 |
273 | describe("validateTransfer", function() {
274 | context("when the referenceId is missing", function() {
275 | it("throws an error", function() {
276 | const request = {} as TransferRequest;
277 | return expect(validateTransfer(request)).to.be.rejectedWith(
278 | "referenceId is required"
279 | );
280 | });
281 | });
282 |
283 | context("when referenceId is not a valid uuid", function() {
284 | it("throws an error", function () {
285 | const request = { referenceId: "test reference id" } as TransferRequest;
286 | return expect(validateTransfer(request)).to.be.rejectedWith(
287 | "referenceId must be a valid uuid v4"
288 | );
289 | });
290 | });
291 |
292 | context("when the amount is missing", function() {
293 | it("throws an error", function() {
294 | const request = { referenceId: uuid() } as TransferRequest;
295 | return expect(validateTransfer(request)).to.be.rejectedWith(
296 | "amount is required"
297 | );
298 | });
299 | });
300 |
301 | context("when the amount is not numeric", function() {
302 | it("throws an error", function() {
303 | const request = { referenceId: uuid(), amount: "alphabetic" } as TransferRequest;
304 | return expect(validateTransfer(request)).to.be.rejectedWith(
305 | "amount must be a number"
306 | );
307 | });
308 | });
309 |
310 | context("when the currency is missing", function() {
311 | it("throws an error", function() {
312 | const request = {
313 | referenceId: uuid(),
314 | amount: "1000"
315 | } as TransferRequest;
316 | return expect(validateTransfer(request)).to.be.rejectedWith(
317 | "currency is required"
318 | );
319 | });
320 | });
321 |
322 | context("when the payee is missing", function() {
323 | it("throws an error", function() {
324 | const request = {
325 | referenceId: uuid(),
326 | amount: "1000",
327 | currency: "UGX"
328 | } as TransferRequest;
329 | return expect(validateTransfer(request)).to.be.rejectedWith(
330 | "payee is required"
331 | );
332 | });
333 | });
334 |
335 | context("when the party id is missing", function() {
336 | it("throws an error", function() {
337 | const request = {
338 | referenceId: uuid(),
339 | amount: "1000",
340 | currency: "UGX",
341 | payee: {}
342 | } as TransferRequest;
343 | return expect(validateTransfer(request)).to.be.rejectedWith(
344 | "payee.partyId is required"
345 | );
346 | });
347 | });
348 |
349 | context("when the party id type is missing", function() {
350 | it("throws an error", function() {
351 | const request = {
352 | referenceId: uuid(),
353 | amount: "1000",
354 | currency: "UGX",
355 | payee: {
356 | partyId: "xxx"
357 | }
358 | } as TransferRequest;
359 | return expect(validateTransfer(request)).to.be.rejectedWith(
360 | "payee.partyIdType is required"
361 | );
362 | });
363 | });
364 |
365 | context("when the request is valid", function() {
366 | it("fulfills", function() {
367 | const request = {
368 | referenceId: uuid(),
369 | amount: "1000",
370 | currency: "UGX",
371 | payee: {
372 | partyId: "xxx",
373 | partyIdType: PartyIdType.MSISDN
374 | }
375 | } as TransferRequest;
376 | return expect(validateTransfer(request)).to.be.fulfilled;
377 | });
378 | });
379 | });
380 | });
381 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "lib": ["es2015", "es2016"],
7 | "outDir": "./lib",
8 | "rootDir": "./src",
9 | "strict": true,
10 | "esModuleInterop": true
11 | },
12 | "exclude": ["lib", "node_modules", "test"]
13 | }
14 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended"],
3 | "rules": {
4 | "object-literal-sort-keys": false,
5 | "interface-name": false,
6 | "trailing-comma": false,
7 | "no-console": false,
8 | "arrow-parens": false,
9 | "only-arrow-functions": false,
10 | "no-var-requires": false,
11 | "ordered-imports": true,
12 | "max-classes-per-file": false
13 | }
14 | }
15 |
--------------------------------------------------------------------------------