UNPKG

pesapaljs

Version:

Integrate PesaPal into your Node application

338 lines (271 loc) 11.3 kB
/** * 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!"); } };