pesapaljs
Version:
Integrate PesaPal into your Node application
338 lines (271 loc) • 11.3 kB
JavaScript
/**
* Copyright (c) 2014 Salama AB
* All rights reserved
* Contact: aksalj@aksalj.me
* Website: http://www.aksalj.me
*
* Project : pesapaljs
* File : requester
* Date : 10/2/14 8:55 AM
* Description :
*
*/
var Promise = require('bluebird');
var OAuthSimple = require('oauthsimple');
var request = require('request');
var URL = require('url');
var models = require('./models');
var PesaPalScraper = require('./scraper');
var DEBUG = false;
/* Data Keys */
var DATA_KEYS = {
type: "pesapal_notification_type", // for IPN only
transaction: "pesapal_transaction_tracking_id",
reference: "pesapal_merchant_reference",
request: "pesapal_request_data", // for direct order only
response: "pesapal_response_data",
redirect_url: "url" // for when submitting payment data
};
exports.SUPPORTED_DATA_KEYS = DATA_KEYS;
/* API Endpoints */
var PAYMENT_STATUS_URL = null;
var PAYMENT_DETAILS_URL = null;
var DIRECT_ORDER_URL = null;
var CONSUMER_KEY = null;
var CONSUMER_SECRET = null;
var CUSTOM_UI_PAYMENT_METHODS = [];
/* Payment Methods */
var EWALLET = new models.PaymentMethod("PESAPAL", "PesaPal", "PesaPal Account", null, null),
MPESA = new models.PaymentMethod("MPESA", "MPesa", "Safaricom mobile payment", "220220", null),
AIRTEL = new models.PaymentMethod("ZAP", "Airtel Money", "Airtel mobile payment", "PESAPAL", null),
VISA = new models.PaymentMethod("VISA", "Visa", "Visa Credit / Debit Card", null, "CREDITCARD"),// PesaPal uses CREDITCARD, CREDITCARD_CS_KE for test cards?
MASTERCARD = new models.PaymentMethod("MASTERCARD", "MasterCard", "MasterCard Credit / Debit Card", null, "CREDITCARDMC"); // PesaPal uses CREDITCARDMC
exports.SUPPORTED_PAYMENT_METHODS = [EWALLET, MPESA, AIRTEL, VISA, MASTERCARD];
var cookieJar = request.jar();
var requestHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36' // Chrome 37
};
/**
* Setup the module
* @param key
* @param secret
* @param debug
*/
exports.setup = function (key, secret, debug) {
DEBUG = debug;
CONSUMER_KEY = key;
CONSUMER_SECRET = secret;
PAYMENT_STATUS_URL = DEBUG ? "https://demo.pesapal.com/api/querypaymentstatus" : "https://www.pesapal.com/API/QueryPaymentStatus";
PAYMENT_DETAILS_URL = DEBUG ? "https://demo.pesapal.com/api/querypaymentdetails" : "https://www.pesapal.com/API/QueryPaymentDetails";
DIRECT_ORDER_URL = DEBUG ? "https://demo.pesapal.com/api/postpesapaldirectorderv4" : "https://www.pesapal.com/API/PostPesapalDirectOrderV4";
CUSTOM_UI_PAYMENT_METHODS = [MPESA.tag, AIRTEL.tag, VISA.tag, MASTERCARD.tag];
if (DEBUG) {
require('request-debug')(request);
}
};
/**
* Prepare xml to be sent as a query parameter
* @param xml
* @returns {*}
*/
var prepareXML = function (xml) {
var php = require('phpjs');
return php.htmlentities(xml, "ENT_NOQUOTES").replace(/"/g, """);
};
/**
*
* @param order
* @param callbackURI
* @returns string
*/
exports.generateSignedPaymentURL = function (order, callbackURI) {
var signer = new OAuthSimple(CONSUMER_KEY, CONSUMER_SECRET);
var rq = signer.sign({
action: "GET",
path: DIRECT_ORDER_URL,
parameters: {
oauth_callback: callbackURI,
pesapal_request_data: prepareXML(order.toXML())
}
});
return rq.signed_url;
};
/**
* Get a payment information
* @param params {{type: string, reference: string, transaction: string, statusOnly: boolean}}
* @returns {bluebird|exports|module.exports}
*/
exports.fetchPayment = function (params) {
params = params || {};
var type = params.type || null;
var reference = params.reference || null;
var transaction = params.transaction || null;
var statusOnly = true;
if (params.statusOnly === false) statusOnly = false;
if (!reference && !transaction) throw new Error("You must specify either a reference or a transaction or both.");
var signParams = {
action: "GET",
path: PAYMENT_STATUS_URL,
parameters: {}
};
if (statusOnly == false) {
signParams.path = PAYMENT_DETAILS_URL;
}
if (type) {
signParams.parameters[DATA_KEYS.type] = type;
}
if (reference) {
signParams.parameters[DATA_KEYS.reference] = reference;
}
if (transaction) {
signParams.parameters[DATA_KEYS.transaction] = transaction;
}
var signer = new OAuthSimple(CONSUMER_KEY, CONSUMER_SECRET);
var req = signer.sign(signParams);
return new Promise(function (resolve, reject) {
request.get({url: req.signed_url}, function (err, resp, body) {
var data = null;
if (!err) {
// TODO: Parse data
if (body.indexOf("Problem:") == 0) {
err = new Error(body)
} else if (resp.statusCode != 200) {
err = new Error(`Something went wrong; Could not fetch payment. (Error ${resp.statusCode})`);
} else {
data = body.replace(DATA_KEYS.response + "=", '');
if (!statusOnly) {
// data=pesapal_transaction_tracking_id,payment_method,payment_status,pesapal_merchant_reference
var segments = data.split(',');
data = {
transaction: segments[0],
method: segments[1],
status: segments[2],
reference: segments[3]
};
}
}
}
if (err || !data) {
reject(err);
} else {
resolve(data);
}
});
});
};
/**
*
* @param order
* @param paymentMethod
* @returns {bluebird|exports|module.exports}
*/
exports.prepareOrder = function (order, paymentMethod) {
if (!order) throw new Error("No order specified");
if (order.items.length == 0 && order.amount == 0) throw new Error("An order must have at least one(1) item");
paymentMethod = paymentMethod || order.getPaymentMethod();
if (!paymentMethod) throw new Error("No payment method specified");
if (CUSTOM_UI_PAYMENT_METHODS.indexOf(paymentMethod.tag) == -1) {
throw new Error("Payment method not supported. Only MPesa, Airtel Money or Bank cards(Visa & MasterCard) are supported");
}
// Add this for possible use by a client
order.setPaymentMethod(paymentMethod);
var signer = new OAuthSimple(CONSUMER_KEY, CONSUMER_SECRET);
var req = signer.sign({
action: "GET",
path: DIRECT_ORDER_URL,
parameters: {
oauth_callback: 'http://dummysite.se:8528/callback/',
pesapal_request_data: prepareXML(order.toXML())
}
});
return new Promise(function (resolve, reject) {
request.get({url: req.signed_url, jar: cookieJar, headers: requestHeaders}, function (err, resp, body) {
if (err) {
reject(err);
} else {
try { // TODO: Scrap PesaPal payment page
if (body.indexOf("Problem:") == 0) throw body;
order._scraper = new PesaPalScraper(paymentMethod, body, DEBUG);
requestHeaders.Referer = req.signed_url;
} catch (error) {
err = new Error(error);
reject(err);
}
resolve(order);
}
});
});
};
/**
* Send mobile money code / credit card details to PesaPal.
* @param order
* @param paymentData {MobileMoney | Card}
* @returns {bluebird|exports|module.exports}
*/
exports.submitPayment = function (order, paymentData) {
if (!order._scraper) throw new Error("Invalid/Unprepared order");
if (!paymentData) throw new Error("No Card/Transaction Info Supplied");
var params = order._scraper.paymentData;
switch (order._scraper.paymentMethod) {
case "VISA": // Credit / Debit Card
case "MASTERCARD":
params.FirstName = paymentData.firstName;
params.LastName = paymentData.lastName;
params.Email = paymentData.email;
params.Country = paymentData.country;
params.CountryCode = paymentData.countryCode;
params.PhoneNumber = paymentData.phone;
params.CreditCardNumber = paymentData.number;
params.Cvv2Number = paymentData.cvv;
params.ExpDateMonth = paymentData.expirationMonth;
params.ExpDateYear = paymentData.expirationYear;
break;
default: // Mobile Money
params.MobileNumber = paymentData.phone;
params.TransactionCode = paymentData.code;
break;
}
var url = order._scraper.url;
switch (order._scraper.httpMethod.toUpperCase()) {
case "POST":
return new Promise(function (resolve, reject) {
var parseResponse = function (err, resp, body) {
if (err) {
reject(err);
} else if (resp.statusCode == 500) {
reject(new Error("Failed to submit payment"));
} else if (resp.statusCode == 200) { // Some error occurred, probably invalid code/card info
reject(new Error("Check Payment Details"));
} else if (resp.statusCode == 302) { // Do they send another status for redirect?
// HUH: No need to redirect, they seem to include the reference and transaction id in the redirect URI
try {
var uri = URL.parse(resp.headers.location, true);
uri = URL.parse(uri.query[DATA_KEYS.redirect_url], true); // http://dummysite.se:8528/callback/?pesapal_transaction_tracking_id=XXXX&pesapal_merchant_reference=XXXX
var reference = uri.query[DATA_KEYS.reference]; // From redirect URI
var transaction = uri.query[DATA_KEYS.transaction];
if (!reference || !transaction) {
err = new Error("Unable to submit payment / No transaction ID returned");
reject(err);
} else {
resolve({
reference: reference,
transaction: transaction
});
}
} catch (err) {
console.error(err);
reject(new Error("Unable to submit payment."));
}
}
};
request.post({
url: url,
jar: cookieJar,
headers: requestHeaders
}, parseResponse).form(params);
});
case "GET": // HUH: Not likely to happen!
default:
throw new Error("WTF PesaPal!");
}
};