🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more →
Sign In

paynow

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

paynow - npm Package Compare versions

Comparing version
2.2.1
to
2.2.2
+38
.github/ISSUE_TEMPLATE/bug_report.md
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Paynow-NodeJS-SDK.iml" filepath="$PROJECT_DIR$/.idea/Paynow-NodeJS-SDK.iml" />
</modules>
</component>
</project>

Sorry, the diff of this file is not supported yet

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
language: node_js
node_js:
- stable
- '6'
deploy:
provider: npm
email: projects@webdevworld.com
api_key:
secure: DWwoL8O6ZXWZjJjoDfEwsLa9f9DKTSJJkPguB99YcJXOa0tI2IRW7HpCxcPIxs6peCPcghLcVDsgoUayhxWy3DPW1sO0BIMcw08ganYlPw/Vs/p5z/Hsvt1hKVJQls3J/RuOO5R1omEeUBHklySSYuoUUb3UBiDVexjoW3VuZdozCnpzLyCws5UutAkCCFPfdP3vrnYTIXrHNKK+/azY/ZMQYWCG5UIZ9qHZRmtvjyjJZbDhT0s81b73G3bADRjzmMsyXCMSPBzMB94SVrarV/T6Hcgnw/6x7zQuXR+UzzHYTlSUZAcHjm/jUypYJfJoYLR/XgOy56bqG2wy2oo86AhcBLAjVZiyReIvARjqwTeGPv4C0ox6b4YOk9gy+5BJp1PdkIWu4Ta2gsupgc8l3aMCxncvqElO3E3zfzv8GBSQr3KeHZFyU2I+2uoL2Mfz9ADsqCLksPphyQtQF7dU6jbK6yoJ+zuDF7ywh6m8pyW9Cy0NpX5LrGq436PNmoaeQ6sCEfJXhyZ0w+FVYFlXAtHzlWY9SLr8S0IHfPVN+r3hJYy+1fHSFGX1nazfNQxawjIqLjdnGCtX+N9WK5G684+/Zux9ROlPzFl0/lrTHm0XlTeswThlz3XZlcdecyuEI/Xh/b0GaOrYjy9a6vWpvQnNniukiLoSHz6xGVxeSiI=
on:
tags: true
repo: paynow/Paynow-NodeJS-SDK
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
/**
* Success response from Paynow
*/
export const RESPONSE_OK : string = "ok";
/**
* Error response from Paynow
*/
export const RESPONSE_ERROR: string = "error";
/**
* API endpoint for initiating normal web-based transactions
*/
export const URL_INITIATE_TRANSACTION : string = "https://www.paynow.co.zw/interface/initiatetransaction";
/**
* API endpoint for initiating mobile based transactions
*/
export const URL_INITIATE_MOBILE_TRANSACTION: string = "https://www.paynow.co.zw/interface/remotetransaction";
export const INNBUCKS_DEEPLINK_PREFIX = "schinn.wbpycode://innbucks.co.zw?pymInnCode=";
export const GOOGLE_QR_PREFIX = "https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=";
export { default as Payment } from "./types/payment"
export { CartItem, default as Cart } from "./types/cart"
export { Paynow, StatusResponse, InitResponse} from './paynow';
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");
}
}
}
//#region CartItem Class
/**
* @param title the name of the cart item
*
* @param amount the cost of a single unit of the item
*
* @param quantity the number of units of the item
*/
export class CartItem {
constructor(public title: string, public amount: number, public quantity: number = 1 ) {}
}
//#endregion
//#region
export default class Cart {
public items : CartItem[] = [];
constructor(_items?: CartItem[] ){
if (_items){
_items.forEach( thing => { this.items.push(thing) });
}
}
length() : number {
return this.items.length;
}
add(item:CartItem){
this.items.push(item);
return this.items.length
}
getTotal(): number {
let cartTotal: number = 0;
this.items.forEach((item: CartItem) => {
cartTotal += item.amount * item.quantity;
});
return cartTotal;
}
summary(): string{
let summary = "";
this.items.forEach(function(item, index) {
summary = summary.concat(item.title + ", ");
})
// console.log(summary, this.items);
summary = summary.substr(0, summary.length - 3);
return summary;
}
}
//#endregion
import Cart, { CartItem } from './cart'
//#region Payment Class
/**
* @param reference unique identifier for the transaction.
* @param authEmail customer's email address.
* @param items items inthe user's Cart
*/
export default class Payment {
constructor( public reference: string, public authEmail: string, public items: Cart = new Cart() ) {}
/**
* Adds an item to the 'shopping cart'
* @param title
* @param amount
*/
add(title: string, amount: number, quantity? : number): Payment {
this.items.add(new CartItem(title, amount, quantity))
return this;
}
info(): string {
return this.items.summary();
}
/**
* Get the total of the items in the cart
* @returns {*|number}
*/
total(): number {
return this.items.getTotal();
}
}
//#endregion
import Cart, { CartItem } from '../src/types/cart';
test('CartItem is 1 if quantity is not added', () => {
let cartItem = new CartItem("Item", 10);
expect(cartItem.quantity).toBe(1);
});
test('CartItem takes value given in constructor if available', () =>{
let cartItem = new CartItem("Item", 10, 5);
expect(cartItem.quantity).toBe(5);
});
test("Cart Instantiation", () => {
let cart = new Cart();
expect(cart.items.length).toBe(0);
});
test("Adding Items to Cart", () => {
let cart = new Cart();
cart.add( new CartItem("Item", 10));
cart.add( new CartItem("item2", 6));
expect(cart.items.length).toBe(2);
});
test(" Cart Total Calculation", () => {
let cart = new Cart();
cart.add( new CartItem("Item", 10, 5));
cart.add( new CartItem("item2", 6));
expect(cart.getTotal()).toBe(56);
});
test("Cart Summary is a string", () => {
let cart = new Cart();
cart.add( new CartItem("Item", 10, 5));
cart.add( new CartItem("item2", 6));
expect(cart.summary()).toBeTruthy;
});
import Payment from '../src/types/payment'
let payment = new Payment("test-reference", "pay@abc.xyz");
test('Payment Type Instantiation', () => {
expect(payment).toBeInstanceOf(Payment);
});
test('Cart Instatiation on Payment Type', () =>{
expect(payment.items).toBeDefined();
})
test('If you can add items to the the cart', () => {
let initialCartLength = payment.items.length();
payment.add("Test Item", 10, 1);
let cartLength = payment.items.length();
expect(initialCartLength).toBeLessThan(cartLength);
})
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "*": ["types/*"] },
"target": "es5",
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"outDir": "dist/",
"sourceMap": true
},
"include": [
"src/*"
],
"exclude": [
"node_modules",
]
}
+3
-7
{
"name": "paynow",
"version": "2.2.1",
"version": "2.2.2",
"description": "Node.JS SDK for Zimbabwe's Leading Payment Gateway, Paynow",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "jest",
"build:tsc": "tsc",
"build:tsc": "tsc --removeComments --sourceMap",
"build": "npm run build:tsc"

@@ -36,6 +35,3 @@ },

"typescript": "^5.7.3"
},
"files": [
"dist/**/*"
]
}
}