recurly-js
Version:
Library for accessing the api for the Recurly recurring billing service.
249 lines (223 loc) • 7 kB
JavaScript
;
var crypto = require('crypto');
var Tools = require('./tools');
module.exports = function (config) {
var t = new Tools(config);
var TRANSPARENT_POST_BASE_URL = config.RECURLY_BASE_URL + '/transparent/' + config.SUBDOMAIN;
var BILLING_INFO_URL = TRANSPARENT_POST_BASE_URL + '/billing_info';
var SUBSCRIBE_URL = TRANSPARENT_POST_BASE_URL + '/subscription';
var TRANSACTION_URL = TRANSPARENT_POST_BASE_URL + '/transaction';
t.debug('============================');
t.debug(TRANSPARENT_POST_BASE_URL);
t.debug(BILLING_INFO_URL);
t.debug(SUBSCRIBE_URL);
t.debug(TRANSACTION_URL);
t.debug('============================');
this.billingInfoUrl = function () {
return BILLING_INFO_URL;
};
this.subscribeUrl = function () {
return SUBSCRIBE_URL;
};
this.transactionUrl = function () {
return TRANSACTION_URL;
};
this.hidden_field = function (data) {
return '<input type="hidden" name="data" value="' + t.htmlEscape(encodedData(data)) + '" />';
};
this.getResults = function (confirm, result, status, type, callback) {
validateQueryString(confirm, type, status, result);
t.request('/transparent/results/' + result, 'GET', callback);
};
this.getFormValuesFromResult = function getFormValuesFromResult(result, type) {
var fields = {};
var errors = [];
t.traverse(result.data, function (key, value, parent) {
var shouldprint = false;
var toprint = '';
if (value instanceof Object) {
if (Object.keys(value).length === 0) {
shouldprint = true;
toprint = '';
}
if (Object.hasOwnProperty('@') || Object.hasOwnProperty('#')) {
shouldprint = true;
toprint = value;
}
if (value instanceof Array) {
shouldprint = true;
toprint = value;
}
}
else if (!(value instanceof Object)) {
shouldprint = true;
toprint = value;
if (key === 'error') {
errors.push({field: '_general', reason: value});
shouldprint = false;
}
}
if (key === '@' || key === '#') {
shouldprint = false;
}
if (parent === '@' || parent === '#') {
shouldprint = false;
}
if (!parent) {
switch (type) {
case 'subscribe':
{
parent = 'account';
break;
}
case 'billing_info':
{
parent = 'billing_info';
}
}
}
if (key === 'errors') {
shouldprint = false;
errors = errors.concat(processErrors(value, parent));
}
if (shouldprint) {
var toadd = {};
try {
fields[parent + '[' + key + ']'] = toprint.replace(/'/g, ''');
}
catch (e) {
t.debug('GET FIELDS: could not process: ' + parent + '[' + key + '] : ' + toprint);
}
}
});
errors = handleFuzzyLogicSpecialCases(errors);
return {fields: fields, errors: errors};
};
function processErrors(errors, parent) {
var acc = [];
var processSingleError = function (e) {
try {
acc.push({
field: parent + '[' + e['@'].field + ']',
reason: e['#'].replace(/'/g, ''')
});
}
catch (err) {
t.debug('Could not process listed error: ' + e);
}
};
errors.forEach(function (item) {
if (item instanceof Array) {
item.forEach(processSingleError);
}
else {
try {
processSingleError(item);
}
catch (err) {
t.debug('Could not process single error: ' + item);
}
}
});
return acc;
}
function encodedData(data) {
verifyRequiredFields(data);
var queryString = makeQueryString(data);
var validationString = hash(queryString);
return validationString + '|' + queryString;
}
function verifyRequiredFields(params) {
if (!params.hasOwnProperty('redirect_url')) {
throw 'Missing required parameter: redirect_url';
}
if (!params.hasOwnProperty('account[account_code]')) {
throw 'Missing required parameter: account[account_code]';
}
}
function makeQueryString(params) {
params.time = makeDate();
return buildQueryStringFromSortedObject(makeSortedObject(params, true));
}
function makeDate() {
var d = new Date();
var addleadingzero = function (n) {
return n < 10 ? '0' + n : n.toString();
};
return d.getUTCFullYear() + '-' +
addleadingzero(d.getUTCMonth() + 1) + '-' +
addleadingzero(d.getUTCDate()) + 'T' +
addleadingzero(d.getUTCHours()) + ':' +
addleadingzero(d.getUTCMinutes()) + ':' +
addleadingzero(d.getUTCSeconds()) + 'Z';
}
function hash(data) {
//get the sha1 of the private key in binary
var shakey = crypto.createHash('sha1');
shakey.update(config.PRIVATE_KEY);
shakey = shakey.digest('binary');
//now make an hmac and return it as hex
var hmac = crypto.createHmac('sha1', shakey);
hmac.update(data);
return hmac.digest('hex');
//php: 03021207ad681f2ea9b9e1fc20ac7ae460d8d988 <== Yes this sign is identical to the php version
//node: 03021207ad681f2ea9b9e1fc20ac7ae460d8d988
}
function buildQueryStringFromSortedObject(params) {
return params.map(function (p) {
return escape(p.key) + '=' + t.urlEncode(p.value);
}).join('&');
}
function makeSortedObject(obj, casesensitive) {
return Object.keys(obj).map(function (key) {
return {key: key, value: obj[key]};
}).sort(function (a, b) {
return (casesensitive ? a.key : a.key.toLowerCase()) > (casesensitive ? b.key : b.key.toLowerCase());
});
}
//Used for validating return params from Recurly
function validateQueryString(confirm, type, status, resultKeys) {
var values = {
result: resultKeys,
status: status,
type: type
};
var queryValues = buildQueryStringFromSortedObject(makeSortedObject(values, true));
var hashedValues = hash(queryValues);
if (hashedValues !== confirm) {
throw 'Error: Forged query string';
}
return true;
}
function handleFuzzyLogicSpecialCases(errors) {
var toreturn = [];
errors.forEach(function (e) {
switch (e.field) {
case 'billing_info[verification_value]':
{
toreturn.push(copyWithNewName('billing_info[credit_card][verification_value]', e));
toreturn.push(copyWithNewName('credit_card[verification_value]', e));
break;
}
case 'credit_card[number]':
{
toreturn.push(copyWithNewName('billing_info[credit_card][number]', e));
toreturn.push(e);
break;
}
default:
{
toreturn.push(e);
break;
}
}
});
return toreturn;
}
function copyWithNewName(name, error) {
return {
field: name,
reason: error.reason
};
}
};