Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remittances #72

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
51 changes: 51 additions & 0 deletions examples/remittances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const momo = require("../lib");

const { Remittances } = momo.create({ callbackHost: process.env.CALLBACK_HOST });

// initialise collections
const remittances = Remittances({
userSecret: process.env.REMITTANCES_USER_SECRET,
userId: process.env.REMITTANCES_USER_ID,
primaryKey: process.env.REMITTANCES_PRIMARY_KEY
});

const partyId = "256776564739";
const partyIdType = momo.PayerType.MSISDN;
// Transfer
remittances
.isPayerActive(partyId, partyIdType)
.then((isActive) => {
console.log("Is Active ? ", isActive);
if (!isActive) {
return Promise.reject("Party not active");
ernest-okot marked this conversation as resolved.
Show resolved Hide resolved
}
return remittances.remit({
amount: "100",
currency: "EUR",
externalId: "947354",
payee: {
partyIdType,
partyId
},
payerMessage: "testing",
payeeNote: "hello",
callbackUrl: process.env.CALLBACK_URL ? process.env.CALLBACK_URL : "https://75f59b50.ngrok.io"
ernest-okot marked this conversation as resolved.
Show resolved Hide resolved
});
})

.then(transactionId => {
console.log({ transactionId });

// Get transaction status
return remittances.getTransaction(transactionId);
})
.then(transaction => {
console.log({ transaction });

// Get account balance
return remittances.getBalance();
})
.then(accountBalance => console.log({ accountBalance }))
.catch(error => {
console.log(error);
});
14 changes: 14 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ export const authorizeDisbursements: Authorizer = function(
.then(response => response.data);
};

export const authorizeRemittances: Authorizer = function(
config: Config,
client: AxiosInstance = createClient(config)
): Promise<AccessToken> {
const basicAuthToken: string = createBasicAuthToken(config);
return client
.post<AccessToken>("/remittance/token/", null, {
headers: {
Authorization: `Basic ${basicAuthToken}`
}
})
.then(response => response.data);
};

export function createBasicAuthToken(config: UserConfig): string {
return Buffer.from(`${config.userId}:${config.userSecret}`).toString(
"base64"
Expand Down
5 changes: 3 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { AxiosError } from "axios";
import { Payment } from "./collections";
import { FailureReason } from "./common";
import { Transfer } from "./disbursements";
import {Remit} from "./remittances";

interface ErrorBody {
code: FailureReason;
message: string;
}

export class MtnMoMoError extends Error {
public transaction?: Payment | Transfer;
public transaction?: Payment | Transfer | Remit;

constructor(message?: string) {
super(message);
Expand Down Expand Up @@ -171,7 +172,7 @@ export function getError(code?: FailureReason, message?: string) {
return new UnspecifiedError(message);
}

export function getTransactionError(transaction: Payment | Transfer) {
export function getTransactionError(transaction: Payment | Transfer | Remit) {
const error: MtnMoMoError = getError(transaction.reason as FailureReason);
ernest-okot marked this conversation as resolved.
Show resolved Hide resolved
error.transaction = transaction;

Expand Down
20 changes: 20 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { Payment, PaymentRequest } from "./collections";
export { Transfer, TransferRequest } from "./disbursements";
export { Remit, RemittanceRequest } from "./remittances";

export * from "./errors";
export {
PartyIdType as PayerType,
Expand All @@ -16,11 +18,13 @@ import { AxiosInstance } from "axios";

import Collections from "./collections";
import Disbursements from "./disbursements";
import Remittances from "./remittances";
import Users from "./users";

import {
authorizeCollections,
authorizeDisbursements,
authorizeRemittances,
createTokenRefresher
} from "./auth";
import { createAuthClient, createClient } from "./client";
Expand All @@ -41,6 +45,7 @@ import {
export interface MomoClient {
Collections(productConfig: ProductConfig): Collections;
Disbursements(productConfig: ProductConfig): Disbursements;
Remittances(productConfig: ProductConfig): Remittances;
Users(subscription: SubscriptionConfig): Users;
}

Expand Down Expand Up @@ -89,6 +94,21 @@ export function create(globalConfig: GlobalConfig): MomoClient {
return new Disbursements(client);
},

Remittances(productConfig: ProductConfig): Remittances {
const config: Config = {
...defaultGlobalConfig,
...globalConfig,
...productConfig,
};

const client: AxiosInstance = createAuthClient(
createTokenRefresher(authorizeRemittances, config),
createClient(config)
);

return new Remittances(client);
},

Users(subscriptionConfig: SubscriptionConfig): Users {
validateSubscriptionConfig(subscriptionConfig);

Expand Down
185 changes: 185 additions & 0 deletions src/remittances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { AxiosInstance } from "axios";
import {v4 as uuid} from "uuid";

import { getTransactionError } from "./errors";
import { validateRemittance } from "./validate";

import { Balance,
FailureReason,
PartyIdType,
TransactionStatus
} from "./common";

export interface RemittanceRequest {
/**
* Unique Transfer Reference (UUID v4), will be automatically generated if not explicitly supplied
*/

referenceId?: string;
/**
* Amount that will be debited from the payer account.
*/
amount: string;

/**
* ISO4217 Currency
*/
currency: string;

/**
* External id is used as a reference to the transaction.
* External id is used for reconciliation.
* The external id will be included in transaction history report.
* External id is not required to be unique.
*/
externalId?: string;

/**
* Party identifies an account holder in the wallet platform.
* Party consists of two parameters, type and partyId.
* Each type have its own validation of the partyId
* MSISDN - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
* EMAIL - Validated to be a valid e-mail format. Validated with IsEmail
* PARTY_CODE - UUID of the party. Validated with IsUuid
*/
payee: {
partyIdType: PartyIdType;
partyId: string;
};

/**
* Message that will be written in the payer transaction history message field.
*/
payerMessage?: string;
/**
* Message that will be written in the payee transaction history note field.
*/
payeeNote?: string;
/**
* URL to the server where the callback should be sent.
*/
callbackUrl?: string;
}

export interface Remit {
/**
* Amount that will be debited from the payer account.
*/
amount: string;

/**
* Financial transactionIdd from mobile money manager.
* Used to connect to the specific financial transaction made in the account
*/
financialTransactionId: string;
/**
* ISO4217 Currency
*/
currency: string;

/**
* External id is used as a reference to the transaction.
* External id is used for reconciliation.
* The external id will be included in transaction history report.
* External id is not required to be unique.
*/
externalId: string;

/**
* Party identifies a account holder in the wallet platform.
* Party consists of two parameters, type and partyId.
* Each type have its own validation of the partyId
* MSISDN - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
* EMAIL - Validated to be a valid e-mail format. Validated with IsEmail
* PARTY_CODE - UUID of the party. Validated with IsUuid
*/
payee: {
partyIdType: "MSISDN";
partyId: string;
};

status: TransactionStatus;
reason?: FailureReason;

}

export default class Remittances {
private client: AxiosInstance;

constructor(client: AxiosInstance) {
this.client = client;
}

/**
* Remit operation to send funds to local recipients from the diaspora
* from the owner's account to the payee's account
*
* @param remittanceRequest
*/
public remit({
callbackUrl,
referenceId= uuid(),
...remittanceRequest
}: RemittanceRequest): Promise<string> {

return validateRemittance({referenceId, ...remittanceRequest}).then(() => {
return this.client
.post<void>("/remittance/v1_0/transfer", remittanceRequest, {
headers: {
"X-Reference-Id": referenceId,
...(callbackUrl ? { "X-Callback-Url": callbackUrl } : {})
}
})
.then(() => referenceId);
});
}

/**
* This method is used to retrieve the transaction. You can invoke this method
* to at intervals until your 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
*
* @param referenceId the value returned from `remit`
*/
public getTransaction( referenceId: string): Promise<Remit> {
return this.client.get<Remit>(`/remittance/v1_0/transfer/${referenceId}`)
.then(response => response.data)
.then(transaction => {
if (transaction.status === TransactionStatus.FAILED) {
return Promise.reject(getTransactionError(transaction));
}

return Promise.resolve(transaction);
});
}

/**
* Get the balance of the account.
*/
public getBalance(): Promise<Balance> {
return this.client
.get<Balance>("/remittance/v1_0/account/balance")
.then(response => response.data);
}

/**
* This method is used to check if an account holder is registered and active in the system.
*
* @param id Specifies the type of the party ID. Allowed values [msisdn, email, party_code].
* accountHolderId should explicitly be in small letters.
*
* @param type The party number. Validated according to the party ID type (case Sensitive).
* msisdn - Mobile Number validated according to ITU-T E.164. Validated with IsMSISDN
* email - Validated to be a valid e-mail format. Validated with IsEmail
* party_code - UUID of the party. Validated with IsUuid
*/
public isPayerActive(id: string, type: PartyIdType = PartyIdType.MSISDN): Promise<boolean> {
return this.client
.get<{result: boolean}>(`/remittance/v1_0/accountholder/${String(type).toLowerCase()}/${id}/active`)
.then(response => response.data)
.then(data => data.result ? data.result : false);
ernest-okot marked this conversation as resolved.
Show resolved Hide resolved
}
}
21 changes: 21 additions & 0 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UserConfig
} from "./common";
import { TransferRequest } from "./disbursements";
import {RemittanceRequest} from "./remittances";

export function validateRequestToPay(
paymentRequest: PaymentRequest
Expand Down Expand Up @@ -50,6 +51,26 @@ export function validateTransfer(
});
}

export function validateRemittance( remittanceRequest: RemittanceRequest
): Promise<void> {
const { amount, currency, payee, referenceId }: RemittanceRequest = remittanceRequest || {};
return Promise.resolve().then(() => {
strictEqual(isTruthy(referenceId), true, "referenceId is required");
strictEqual(isUuid4(referenceId as string), true, "referenceId must be a valid uuid v4");
ernest-okot marked this conversation as resolved.
Show resolved Hide resolved
strictEqual(isTruthy(amount), true, "amount is required");
strictEqual(isNumeric(amount), true, "amount must be a number");
strictEqual(isTruthy(currency), true, "currency is required");
strictEqual(isTruthy(payee), true, "payee is required");
strictEqual(isTruthy(payee.partyId), true, "payee.partyId is required");
strictEqual(
isTruthy(payee.partyIdType),
true,
"payee.partyIdType is required"
);
strictEqual(isString(currency), true, "amount must be a string");
});
}

export function validateGlobalConfig(config: GlobalConfig): void {
const { callbackHost, baseUrl, environment } = config;
strictEqual(isTruthy(callbackHost), true, "callbackHost is required");
Expand Down
Loading