node-recurly
Version:
Library for accessing the api for the Recurly recurring billing service.
233 lines (207 loc) • 6.58 kB
JavaScript
(function(){
var qs = require('querystring');
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(encoded_data(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){
//loop through the error list
item.forEach(processSingleError)
}
else{
//its a single error so grab it out
try{
processSingleError(item);
}
catch(err){
t.debug('Could not process single error: ' + item);
}
}
});
return acc;
}
function encoded_data(data){
verify_required_fields(data);
var query_string = make_query_string(data);
var validation_string = hash(query_string);
return validation_string + "|" + query_string;
}
function verify_required_fields(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 make_query_string(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 };
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, result_key)
{
var values = {
result: result_key,
status: status,
type: type
}
var query_values = buildQueryStringFromSortedObject(makeSortedObject(values, true));
hashed_values = hash(query_values);
if(hashed_values !== 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};
}
}//END CLASS
})();