RecurlyWrapper.coffee 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. querystring = require 'querystring'
  2. crypto = require 'crypto'
  3. request = require 'request'
  4. Settings = require "settings-sharelatex"
  5. xml2js = require "xml2js"
  6. logger = require("logger-sharelatex")
  7. module.exports = RecurlyWrapper =
  8. apiUrl : "https://api.recurly.com/v2"
  9. apiRequest : (options, callback) ->
  10. options.url = @apiUrl + "/" + options.url
  11. options.headers =
  12. "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
  13. "Accept" : "application/xml"
  14. "Content-Type" : "application/xml; charset=utf-8"
  15. request options, (error, response, body) ->
  16. unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
  17. error = "Recurly API returned with status code: #{response.statusCode}"
  18. callback(error, response, body)
  19. sign : (parameters, callback) ->
  20. nestAttributesForQueryString = (attributes, base) ->
  21. newAttributes = {}
  22. for key, value of attributes
  23. if base?
  24. newKey = "#{base}[#{key}]"
  25. else
  26. newKey = key
  27. if typeof value == "object"
  28. for key, value of nestAttributesForQueryString(value, newKey)
  29. newAttributes[key] = value
  30. else
  31. newAttributes[newKey] = value
  32. return newAttributes
  33. crypto.randomBytes 32, (error, buffer) ->
  34. return callback error if error?
  35. parameters.nonce = buffer.toString "base64"
  36. parameters.timestamp = Math.round((new Date()).getTime() / 1000)
  37. unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters)
  38. signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex")
  39. signature = "#{signed}|#{unsignedQuery}"
  40. callback null, signature
  41. getSubscription: (subscriptionId, options, callback) ->
  42. callback = options unless callback?
  43. options ||= {}
  44. if options.recurlyJsResult
  45. url = "recurly_js/result/#{subscriptionId}"
  46. else
  47. url = "subscriptions/#{subscriptionId}"
  48. @apiRequest({
  49. url: url
  50. }, (error, response, body) =>
  51. return callback(error) if error?
  52. @_parseSubscriptionXml body, (error, recurlySubscription) =>
  53. return callback(error) if error?
  54. if options.includeAccount
  55. if recurlySubscription.account? and recurlySubscription.account.url?
  56. accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
  57. else
  58. return callback "I don't understand the response from Recurly"
  59. @getAccount accountId, (error, account) ->
  60. return callback(error) if error?
  61. recurlySubscription.account = account
  62. callback null, recurlySubscription
  63. else
  64. callback null, recurlySubscription
  65. )
  66. getAccount: (accountId, callback) ->
  67. @apiRequest({
  68. url: "accounts/#{accountId}"
  69. }, (error, response, body) =>
  70. return callback(error) if error?
  71. @_parseAccountXml body, callback
  72. )
  73. updateSubscription: (subscriptionId, options, callback) ->
  74. logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
  75. requestBody = """
  76. <subscription>
  77. <plan_code>#{options.plan_code}</plan_code>
  78. <timeframe>#{options.timeframe}</timeframe>
  79. </subscription>
  80. """
  81. @apiRequest({
  82. url : "subscriptions/#{subscriptionId}"
  83. method : "put"
  84. body : requestBody
  85. }, (error, response, responseBody) =>
  86. return callback(error) if error?
  87. @_parseSubscriptionXml responseBody, callback
  88. )
  89. cancelSubscription: (subscriptionId, callback) ->
  90. logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
  91. @apiRequest({
  92. url: "subscriptions/#{subscriptionId}/cancel",
  93. method: "put"
  94. }, (error, response, body) ->
  95. callback(error)
  96. )
  97. reactivateSubscription: (subscriptionId, callback) ->
  98. logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
  99. @apiRequest({
  100. url: "subscriptions/#{subscriptionId}/reactivate",
  101. method: "put"
  102. }, (error, response, body) ->
  103. callback(error)
  104. )
  105. _parseSubscriptionXml: (xml, callback) ->
  106. @_parseXml xml, (error, data) ->
  107. return callback(error) if error?
  108. if data? and data.subscription?
  109. recurlySubscription = data.subscription
  110. else
  111. return callback "I don't understand the response from Recurly"
  112. callback null, recurlySubscription
  113. _parseAccountXml: (xml, callback) ->
  114. @_parseXml xml, (error, data) ->
  115. return callback(error) if error?
  116. if data? and data.account?
  117. account = data.account
  118. else
  119. return callback "I don't understand the response from Recurly"
  120. callback null, account
  121. _parseXml: (xml, callback) ->
  122. convertDataTypes = (data) ->
  123. if data? and data["$"]?
  124. if data["$"]["nil"] == "nil"
  125. data = null
  126. else if data["$"].href?
  127. data.url = data["$"].href
  128. delete data["$"]
  129. else if data["$"]["type"] == "integer"
  130. data = parseInt(data["_"], 10)
  131. else if data["$"]["type"] == "datetime"
  132. data = new Date(data["_"])
  133. else if data["$"]["type"] == "array"
  134. delete data["$"]
  135. array = []
  136. for key, value of data
  137. if value instanceof Array
  138. array = array.concat(convertDataTypes(value))
  139. else
  140. array.push(convertDataTypes(value))
  141. data = array
  142. if data instanceof Array
  143. data = (convertDataTypes(entry) for entry in data)
  144. else if typeof data == "object"
  145. for key, value of data
  146. data[key] = convertDataTypes(value)
  147. return data
  148. parser = new xml2js.Parser(
  149. explicitRoot : true
  150. explicitArray : false
  151. )
  152. parser.parseString xml, (error, data) ->
  153. return callback(error) if error?
  154. result = convertDataTypes(data)
  155. callback null, result