querystring = require 'querystring' crypto = require 'crypto' request = require 'request' Settings = require "settings-sharelatex" xml2js = require "xml2js" logger = require("logger-sharelatex") module.exports = RecurlyWrapper = apiUrl : "https://api.recurly.com/v2" apiRequest : (options, callback) -> options.url = @apiUrl + "/" + options.url options.headers = "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64") "Accept" : "application/xml" "Content-Type" : "application/xml; charset=utf-8" request options, (error, response, body) -> unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 logger.err err:error, options:options, "error returned from recurly" error = "Recurly API returned with status code: #{response.statusCode}" callback(error, response, body) sign : (parameters, callback) -> nestAttributesForQueryString = (attributes, base) -> newAttributes = {} for key, value of attributes if base? newKey = "#{base}[#{key}]" else newKey = key if typeof value == "object" for key, value of nestAttributesForQueryString(value, newKey) newAttributes[key] = value else newAttributes[newKey] = value return newAttributes crypto.randomBytes 32, (error, buffer) -> return callback error if error? parameters.nonce = buffer.toString "base64" parameters.timestamp = Math.round((new Date()).getTime() / 1000) unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters) signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex") signature = "#{signed}|#{unsignedQuery}" callback null, signature getSubscriptions: (accountId, callback)-> @apiRequest({ url: "accounts/#{accountId}/subscriptions" }, (error, response, body) => return callback(error) if error? @_parseXml body, callback ) getSubscription: (subscriptionId, options, callback) -> callback = options unless callback? options ||= {} if options.recurlyJsResult url = "recurly_js/result/#{subscriptionId}" else url = "subscriptions/#{subscriptionId}" @apiRequest({ url: url }, (error, response, body) => return callback(error) if error? @_parseSubscriptionXml body, (error, recurlySubscription) => return callback(error) if error? if options.includeAccount if recurlySubscription.account? and recurlySubscription.account.url? accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1] else return callback "I don't understand the response from Recurly" @getAccount accountId, (error, account) -> return callback(error) if error? recurlySubscription.account = account callback null, recurlySubscription else callback null, recurlySubscription ) getAccounts: (callback)-> @apiRequest({ url: "accounts" qs: per_page:2000 }, (error, response, body) => return callback(error) if error? @_parseXml body, callback ) getAccount: (accountId, callback) -> @apiRequest({ url: "accounts/#{accountId}" }, (error, response, body) => return callback(error) if error? @_parseAccountXml body, callback ) getBillingInfo: (accountId, callback)-> @apiRequest({ url: "accounts/#{accountId}/billing_info" }, (error, response, body) => return callback(error) if error? @_parseXml body, callback ) updateSubscription: (subscriptionId, options, callback) -> logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription" requestBody = """ #{options.plan_code} #{options.timeframe} """ @apiRequest({ url : "subscriptions/#{subscriptionId}" method : "put" body : requestBody }, (error, response, responseBody) => return callback(error) if error? @_parseSubscriptionXml responseBody, callback ) createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, callback)-> requestBody = """ #{coupon_code} #{name} dollars <#{currencyCode}>#{discount_in_cents} """ logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon" @apiRequest({ url : "coupons" method : "post" body : requestBody }, (error, response, responseBody) => if error? logger.err err:error, coupon_code:coupon_code, "error creating coupon" callback(error) ) lookupCoupon: (coupon_code, callback)-> @apiRequest({ url: "coupons/#{coupon_code}" }, (error, response, body) => return callback(error) if error? @_parseXml body, callback ) cancelSubscription: (subscriptionId, callback) -> logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription" @apiRequest({ url: "subscriptions/#{subscriptionId}/cancel", method: "put" }, (error, response, body) -> callback(error) ) reactivateSubscription: (subscriptionId, callback) -> logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription" @apiRequest({ url: "subscriptions/#{subscriptionId}/reactivate", method: "put" }, (error, response, body) -> callback(error) ) redeemCoupon: (account_code, coupon_code, callback)-> requestBody = """ #{account_code} USD """ logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user" @apiRequest({ url : "coupons/#{coupon_code}/redeem" method : "post" body : requestBody }, (error, response, responseBody) => if error? logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon" callback(error) ) _parseSubscriptionXml: (xml, callback) -> @_parseXml xml, (error, data) -> return callback(error) if error? if data? and data.subscription? recurlySubscription = data.subscription else return callback "I don't understand the response from Recurly" callback null, recurlySubscription _parseAccountXml: (xml, callback) -> @_parseXml xml, (error, data) -> return callback(error) if error? if data? and data.account? account = data.account else return callback "I don't understand the response from Recurly" callback null, account _parseXml: (xml, callback) -> convertDataTypes = (data) -> if data? and data["$"]? if data["$"]["nil"] == "nil" data = null else if data["$"].href? data.url = data["$"].href delete data["$"] else if data["$"]["type"] == "integer" data = parseInt(data["_"], 10) else if data["$"]["type"] == "datetime" data = new Date(data["_"]) else if data["$"]["type"] == "array" delete data["$"] array = [] for key, value of data if value instanceof Array array = array.concat(convertDataTypes(value)) else array.push(convertDataTypes(value)) data = array if data instanceof Array data = (convertDataTypes(entry) for entry in data) else if typeof data == "object" for key, value of data data[key] = convertDataTypes(value) return data parser = new xml2js.Parser( explicitRoot : true explicitArray : false ) parser.parseString xml, (error, data) -> return callback(error) if error? result = convertDataTypes(data) callback null, result