diff --git a/.gitignore b/.gitignore index 0edf183..c5642df 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ .idea/ .env package-lock.json +dist/ +types/ \ No newline at end of file diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..d81a4e0 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,5 @@ +{ + "extensions": ["ts"], + "spec": "test/**/*.ts", + "require": "ts-node/register" +} \ No newline at end of file diff --git a/index.js b/index.js deleted file mode 100644 index f9017ec..0000000 --- a/index.js +++ /dev/null @@ -1,119 +0,0 @@ -require('dotenv').config(); -const axios = require('axios').default; -const crypto = require('crypto'); -const constants = require('constants'); - -let mpesaConfig; - -function _getBearerToken(mpesa_public_key, mpesa_api_key) { - const publicKey = "-----BEGIN PUBLIC KEY-----\n"+mpesa_public_key+"\n"+"-----END PUBLIC KEY-----"; - const buffer = Buffer.from(mpesa_api_key); - const encrypted = crypto.publicEncrypt({ - 'key': publicKey, - 'padding': constants.RSA_PKCS1_PADDING, - }, buffer); - return encrypted.toString("base64"); -} - -function initialize_api_from_dotenv() { - if (!mpesaConfig) { - mpesaConfig = { - baseUrl: process.env.MPESA_API_HOST, - apiKey: process.env.MPESA_API_KEY, - publicKey: process.env.MPESA_PUBLIC_KEY, - origin: process.env.MPESA_ORIGIN, - serviceProviderCode: process.env.MPESA_SERVICE_PROVIDER_CODE - }; - validateConfig(mpesaConfig); - console.log("Using M-Pesa environment configuration"); - } else { - console.log("Using custom M-Pesa configuration"); - } -} - -function required_config_arg(argName) { - return "Please provide a valid " + argName + " in the configuration when calling initializeApi()"; -} - -function validateConfig(configParams) { - if (!configParams.baseUrl) { - throw required_config_arg("baseUrl") - } - if (!configParams.apiKey) { - throw required_config_arg("apiKey") - } - if (!configParams.publicKey) { - throw required_config_arg("publicKey") - } - if (!configParams.origin) { - throw required_config_arg("origin") - } - if (!configParams.serviceProviderCode) { - throw required_config_arg("serviceProviderCode") - } -} - -module.exports.initializeApi = function (configParams) { - validateConfig(configParams); - mpesaConfig = configParams; -}; - -module.exports.initiate_c2b = async function (amount, msisdn, transaction_ref, thirdparty_ref) { - initialize_api_from_dotenv(); - try { - let response; - response = await axios({ - method: 'post', - url: 'https://' + mpesaConfig.baseUrl + ':18352/ipg/v1x/c2bPayment/singleStage/', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + _getBearerToken(mpesaConfig.publicKey, mpesaConfig.apiKey), - 'Origin': mpesaConfig.origin - }, - data: { - "input_TransactionReference": transaction_ref, - "input_CustomerMSISDN": msisdn + "", - "input_Amount": amount + "", - "input_ThirdPartyReference": thirdparty_ref, - "input_ServiceProviderCode": mpesaConfig.serviceProviderCode + "" - } - }); - return response.data; - } catch (e) { - if (e.response.data) { - throw e.response.data; - } else { - throw e; - } - } -}; - -module.exports.initiate_b2c = async function (amount, msisdn, transaction_ref, thirdparty_ref) { - initialize_api_from_dotenv(); - try { - let response; - response = await axios({ - method: 'post', - url: 'https://' + mpesaConfig.baseUrl + ':18345/ipg/v1x/b2cPayment/', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + _getBearerToken(mpesaConfig.publicKey, mpesaConfig.apiKey), - 'Origin': mpesaConfig.origin - }, - data: { - "input_TransactionReference": transaction_ref, - "input_CustomerMSISDN": msisdn + "", - "input_Amount": amount + "", - "input_ThirdPartyReference": thirdparty_ref, - "input_ServiceProviderCode": mpesaConfig.serviceProviderCode + "" - } - }); - return response.data; - } catch (e) { - if (e.response.data) { - throw e.response.data; - } else { - throw e; - } - } -}; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f6cef2f --- /dev/null +++ b/index.ts @@ -0,0 +1,127 @@ +import dotenv from 'dotenv'; +import axios, { AxiosResponse } from 'axios'; +import crypto from 'crypto'; +import constants from 'constants'; + +dotenv.config(); + +interface MpesaConfig { + baseUrl: string; + apiKey: string; + publicKey: string; + origin: string; + serviceProviderCode: string; +} + +let mpesaConfig: MpesaConfig | undefined; + +function _getBearerToken(mpesa_public_key: string, mpesa_api_key: string): string { + const publicKey = `-----BEGIN PUBLIC KEY-----\n${mpesa_public_key}\n-----END PUBLIC KEY-----`; + const buffer = Buffer.from(mpesa_api_key); + const encrypted = crypto.publicEncrypt({ + key: publicKey, + padding: constants.RSA_PKCS1_PADDING, + }, buffer); + return encrypted.toString("base64"); +} + +function initialize_api_from_dotenv(): void { + if (!mpesaConfig) { + mpesaConfig = { + baseUrl: process.env.MPESA_API_HOST || '', + apiKey: process.env.MPESA_API_KEY || '', + publicKey: process.env.MPESA_PUBLIC_KEY || '', + origin: process.env.MPESA_ORIGIN || '', + serviceProviderCode: process.env.MPESA_SERVICE_PROVIDER_CODE || '' + }; + validateConfig(mpesaConfig); + console.log("Using M-Pesa environment configuration"); + } else { + console.log("Using custom M-Pesa configuration"); + } +} + +function required_config_arg(argName: string): string { + return `Please provide a valid ${argName} in the configuration when calling initializeApi()`; +} + +function validateConfig(configParams: MpesaConfig): void { + if (!configParams.baseUrl) { + throw required_config_arg("baseUrl"); + } + if (!configParams.apiKey) { + throw required_config_arg("apiKey"); + } + if (!configParams.publicKey) { + throw required_config_arg("publicKey"); + } + if (!configParams.origin) { + throw required_config_arg("origin"); + } + if (!configParams.serviceProviderCode) { + throw required_config_arg("serviceProviderCode"); + } +} + +export const initializeApi = (configParams: MpesaConfig): void => { + validateConfig(configParams); + mpesaConfig = configParams; +}; + +export const initiate_c2b = async (amount: number, msisdn: string, transaction_ref: string, thirdparty_ref: string): Promise => { + initialize_api_from_dotenv(); + try { + const response: AxiosResponse = await axios({ + method: 'post', + url: `https://${mpesaConfig?.baseUrl}:18352/ipg/v1x/c2bPayment/singleStage/`, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${_getBearerToken(mpesaConfig?.publicKey || '', mpesaConfig?.apiKey || '')}`, + 'Origin': mpesaConfig?.origin || '' + }, + data: { + "input_TransactionReference": transaction_ref, + "input_CustomerMSISDN": msisdn, + "input_Amount": amount.toString(), + "input_ThirdPartyReference": thirdparty_ref, + "input_ServiceProviderCode": mpesaConfig?.serviceProviderCode || '' + } + }); + return response.data; + } catch (e: any) { + if (e.response?.data) { + throw e.response.data; + } else { + throw e; + } + } +}; + +export const initiate_b2c = async (amount: number, msisdn: string, transaction_ref: string, thirdparty_ref: string): Promise => { + initialize_api_from_dotenv(); + try { + const response: AxiosResponse = await axios({ + method: 'post', + url: `https://${mpesaConfig?.baseUrl}:18345/ipg/v1x/b2cPayment/`, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${_getBearerToken(mpesaConfig?.publicKey || '', mpesaConfig?.apiKey || '')}`, + 'Origin': mpesaConfig?.origin || '' + }, + data: { + "input_TransactionReference": transaction_ref, + "input_CustomerMSISDN": msisdn, + "input_Amount": amount.toString(), + "input_ThirdPartyReference": thirdparty_ref, + "input_ServiceProviderCode": mpesaConfig?.serviceProviderCode || '' + } + }); + return response.data; + } catch (e: any) { + if (e.response?.data) { + throw e.response.data; + } else { + throw e; + } + } +}; diff --git a/package.json b/package.json index db0b127..5efb77c 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,18 @@ "name": "mpesa-node-api", "version": "1.1.0", "description": "Node.js library for M-Pesa API (Mozambique)", - "main": "index.js", + "main": "dist/index.js", + "types": "types/index.d.ts", "scripts": { - "test": "mocha" + "test": "mocha", + "build": "tsc" + }, + "mocha": { + "require": "ts-node/register", + "extension": [ + "ts" + ], + "spec": "test/**/*.ts" }, "repository": { "type": "git", @@ -20,13 +29,19 @@ }, "homepage": "https://github.com/rosariopfernandes/mpesa-node-api", "dependencies": { - "axios": "^0.21.1", + "axios": "^1.8.4", "constants": "0.0.2", "crypto": "^1.0.1", "dotenv": "^8.2.0" }, "devDependencies": { + "@types/chai": "^5.2.1", + "@types/dotenv": "^6.1.1", + "@types/mocha": "^10.0.10", + "@types/node": "^22.13.14", "chai": "^4.2.0", - "mocha": "^8.0.1" + "mocha": "^11.1.0", + "ts-node": "^9.1.1", + "typescript": "^5.8.2" } } diff --git a/test/test.js b/test/test.ts similarity index 69% rename from test/test.js rename to test/test.ts index db62f76..72e7fcd 100644 --- a/test/test.js +++ b/test/test.ts @@ -1,15 +1,16 @@ -const mpesa = require('../index'); -const expect = require('chai').expect; +import * as mpesa from '../index'; +import { expect } from 'chai'; describe('config', function () { it('initializeApi throws an error when required config is missing', function () { const actualConfig = { baseUrl: "api.mpesa.co.mz" - }; + } as any; + try { - mpesa.initializeApi(actualConfig) + mpesa.initializeApi(actualConfig); } catch (e) { - expect(e).to.equal('Please provide a valid apiKey in the configuration when calling initializeApi()') + expect(e).to.equal('Please provide a valid apiKey in the configuration when calling initializeApi()'); } }); @@ -19,10 +20,11 @@ describe('config', function () { apiKey: "apiKey", publicKey: "key", origin: "developer.mpesa.co.mz", - serviceProviderCode: 171717 + serviceProviderCode: "171717" }; + expect(function () { - mpesa.initializeApi(actualConfig) + mpesa.initializeApi(actualConfig); }).to.not.throw(); }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..94e0849 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "esnext", + "declaration": true, + "declarationDir": "./types", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "sourceMap": true, + "types": ["mocha", "chai"], + }, + "include": ["**/*.ts"], + "exclude": [ + "node_modules" + ] +}