paynow
Version:
Node.JS SDK for Zimbabwe's Leading Payment Gateway, Paynow
434 lines (384 loc) • 12.5 kB
text/typescript
import Payment from "./types/payment";
import {
URL_INITIATE_MOBILE_TRANSACTION,
URL_INITIATE_TRANSACTION,
RESPONSE_ERROR,
RESPONSE_OK,
GOOGLE_QR_PREFIX,
INNBUCKS_DEEPLINK_PREFIX,
} from "./constants";
import axios, {AxiosResponse} from "axios";
//#region StatusResponse Class
/**
*
* @property {String} reference - merchant transaction reference .
* @property {String} amount - original amount for the transaction.
* @property {String} paynowReference - the Paynow transaction reference.
* @property {String} pollUrl - the URL on Paynow the merchant can poll to confirm the transaction’s status.
* @property {String} status - transaction status returned from paynow.
* @property {String} error - error message sent from Paynow (if any).
*
* @param data data from the status response
*/
export class StatusResponse {
reference: String;
amount: String;
paynowReference: String;
pollUrl: String;
status: String;
error: String;
constructor(data: any) {
if (data.status.toLowerCase() === RESPONSE_ERROR) {
this.error = data.error;
} else {
this.reference = data.reference;
this.amount = data.amount;
this.paynowReference = data.paynowreference;
this.pollUrl = data.pollurl;
this.status = data.status;
}
}
}
//#endregion
//#region InitResponse Class
/**
*
* @property {boolean} success - indicates if initiate request was successful or not.
* @property {boolean} hasRedirect - indicates if the response has a URL to redirect to.
* @property {String} redirectUrl - the URL the user should be redirected to so they can make a payment.
* @property {String} error - error message sent from Paynow (if any).
* @property {String} pollUrl - pollUrl sent from Paynow that can be used to check transaction status.
* @property {String} instructions - instructions for USSD push for customers to dial incase of mobile money payments.
* @property {String} status - status from Paynow.
*
* @param data - data from the Response.
*
*/
export class InitResponse {
success: boolean;
hasRedirect: boolean;
redirectUrl: String;
error: String;
pollUrl: String;
instructions: String;
status: String;
innbucks_info: Array<any>;
isInnbucks: boolean;
constructor(data: any) {
this.status = data.status.toLowerCase();
this.success = this.status === RESPONSE_OK;
this.hasRedirect = typeof data.browserurl !== "undefined";
this.isInnbucks = false;
if (!this.success) {
this.error = data.error;
} else {
this.pollUrl = data.pollurl;
if (this.hasRedirect) {
this.redirectUrl = data.browserurl;
}
if (typeof data.instructions !== "undefined") {
this.instructions = data.instructions;
}
if (typeof data.authorizationcode !== "undefined") {
this.isInnbucks = true;
this.innbucks_info = [];
this.innbucks_info.push({
authorizationcode: data.authorizationcode,
deep_link_url : INNBUCKS_DEEPLINK_PREFIX + data.authorizationcode,
qr_code: GOOGLE_QR_PREFIX + data.authorizationcode,
expires_at: data.authorizationexpires,
});
}
}
}
}
//#endregion
/**
* Paynow Class
*
* @param integrationId {String} Merchant's integration id
* @param integrationKey {String} Merchant's integration key
* @param resultUrl {String} Url where where transaction status will be sent
* @param returnUrl {String} Url to redirect the user after payment
**/
export class Paynow {
constructor(
public integrationId: string,
public integrationKey: string,
public resultUrl: string,
public returnUrl: string
) {}
/**
* Send a payment to paynow
* @param payment
*/
send(payment: Payment) {
return this.init(payment);
}
/**
* Send a mobile money payment to paynow
* @param payment
*/
sendMobile(payment: Payment, phone: string, method: string) {
return this.initMobile(payment, phone, method);
}
/**
* Create a new Paynow payment
* @param {String} reference This is the unique reference of the transaction
* @param {String} authEmail This is the email address of the person making payment. Required for mobile transactions
* @returns {Payment}
*/
createPayment(reference: string, authEmail: string): Payment {
return new Payment(reference, authEmail);
}
/**
* Throw an exception with the given message
* @param message*
* @returns void
*/
fail(message: string): Error {
throw new Error(message);
}
/**
* Initialize a new transaction with PayNow
* @param payment
* @returns {PromiseLike<InitResponse> | Promise<InitResponse>}
*/
init(payment: Payment) {
this.validate(payment);
let data = this.build(payment);
return axios({
method: "POST",
url: URL_INITIATE_TRANSACTION,
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}).then((response) => {
return this.parse(response.data);
})
.catch(function (err) {
console.log("An error occured while initiating transaction", err);
});
}
/**
* Initialize a new mobile transaction with PayNow
* @param {Payment} payment
* @param phone - The phone number to be used for the payment
* @param method - The express checkout method.
* @returns {PromiseLike<InitResponse> | Promise<InitResponse>} the response from the initiation of the transaction
*/
async initMobile(payment: Payment, phone: string, method: string): Promise<PromiseLike<InitResponse> | Promise<InitResponse>> {
this.validate(payment);
if (!this.isValidEmail(payment.authEmail))
this.fail("Invalid email. Please ensure that you pass a valid email address when initiating a mobile payment");
let data = this.buildMobile(payment, phone, method);
try {
const response = await axios({
method: "POST",
url: URL_INITIATE_MOBILE_TRANSACTION,
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
return this.parse(response.data);
} catch (err) {
console.log("An error occured while initiating transaction", err);
}
}
/**
* Validates whether an email address is valid or not
*
* @param {string} emailAddress The email address to validate
*
* @returns {boolean} A value indicating an email is valid or not
*/
isValidEmail(emailAddress: string) {
if (!emailAddress || emailAddress.length === 0) return false;
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(emailAddress);
}
/**
* Parses the response from Paynow
* @param response
* @returns {InitResponse}
*/
parse(response: Response) {
if (typeof response === "undefined") {
return null;
}
if (response) {
let parsedResponseURL = this.parseQuery(response as unknown as string);
if (
parsedResponseURL.status.toString().toLowerCase() !== RESPONSE_ERROR &&
!this.verifyHash(parsedResponseURL)
) {
throw new Error("Hashes do not match!");
}
return new InitResponse(parsedResponseURL);
} else {
throw new Error("An unknown error occurred");
}
}
/**
* Creates a SHA512 hash of the transactions
* @param values
* @param integrationKey
* @returns {string}
*/
generateHash(values: { [key: string]: string }, integrationKey: String) {
let sha512 = require("js-sha512").sha512;
let string: string = "";
for (const key of Object.keys(values)) {
if (key !== "hash") {
string += values[key];
}
}
string += integrationKey.toLowerCase();
return sha512(string).toUpperCase();
}
/**
* Verify hashes at all interactions with server
* @param {*} values
*/
verifyHash(values: { [key: string]: string }) {
if (typeof values["hash"] === "undefined") {
return false;
} else {
return values["hash"] === this.generateHash(values, this.integrationKey);
}
}
/**
* URL encodes the given string
* @param str {String}
* @returns {String}
*/
urlEncode(url: string) {
return encodeURI(url);
}
/**
* URL decodes the given string
* @param str {String}
* @returns {String}
*/
urlDecode(url: string) {
return decodeURIComponent(
(url + "")
.replace(/%(?![\da-f]{2})/gi, function () {
return "%25";
})
.replace(/\+/g, "%20")
);
}
/**
* Parse responses from Paynow
* @param queryString
*/
parseQuery(queryString: string) {
let query: { [key: string]: string } = {};
let pairs = (
queryString[0] === "?" ? queryString.substr(1) : queryString
).split("&");
for (let i = 0; i < pairs.length; i++) {
let pair = pairs[i].split("=");
query[this.urlDecode(pair[0])] = this.urlDecode(pair[1] || "");
}
// if(!this.verifyHash(query))
// throw new Error("Hash mismatch");
return query;
}
/**
* Build up a payment into the format required by Paynow
* @param payment
* @returns {{resulturl: String, returnurl: String, reference: String, amount: number, id: String, additionalinfo: String, authemail: String, status: String}}
*/
build(payment: Payment) {
let data: { [key: string]: string } = {
resulturl: this.resultUrl,
returnurl: this.returnUrl,
reference: payment.reference,
amount: payment.total().toString(),
id: this.integrationId,
additionalinfo: payment.info(),
authemail:
typeof payment.authEmail === "undefined" ? "" : payment.authEmail,
status: "Message",
};
for (const key of Object.keys(data)) {
if (key === "hash") continue;
data[key] = this.urlEncode(data[key]);
}
data["hash"] = this.generateHash(data, this.integrationKey);
return data;
}
/**
* Build up a mobile payment into the format required by Paynow
* @param payment
* @returns {{resulturl: String, returnurl: String, reference: String, amount: number, id: String, additionalinfo: String, authemail: String, status: String}}
*/
buildMobile(
payment: Payment,
phone: string,
method: string
): Error | { [key: string]: string } {
let data: { [key: string]: string } = {
resulturl: this.resultUrl,
returnurl: this.returnUrl,
reference: payment.reference,
amount: payment.total().toString(),
id: this.integrationId,
additionalinfo: payment.info(),
authemail: payment.authEmail,
phone: phone,
method: method,
status: "Message",
};
for (const key of Object.keys(data)) {
if (key === "hash") continue;
data[key] = this.urlEncode(data[key]);
}
data["hash"] = this.generateHash(data, this.integrationKey);
return data;
}
/**
* Check the status of a transaction
* @param url
* @returns {PromiseLike<InitResponse> | Promise<InitResponse>}
*/
public async pollTransaction(url: string): Promise<PromiseLike<InitResponse> | Promise<InitResponse>> {
let response = await axios({
method: "POST",
url: url,
data: null,
});
return this.parse(response.data);
}
/**
* Parses the response from Paynow
* @param response
* @returns {StatusResponse}
*/
parseStatusUpdate(response: any): StatusResponse {
if (response.length > 0) {
response = this.parseQuery(response);
if (!this.verifyHash(response)) {
throw new Error("Hashes do not match!");
}
return new StatusResponse(response);
} else {
throw new Error("An unknown error occurred");
}
}
/**
* Validates an outgoing request before sending it to Paynow (data sanity checks)
* @param payment
*/
validate(payment: Payment) {
if (payment.items.length() <= 0) {
this.fail("You need to have at least one item in cart");
}
if (payment.total() <= 0) {
this.fail("The total should be greater than zero");
}
}
}