RecurlyWrapper.coffee 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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. logger.err err:error, options:options, "error returned from recurly"
  18. error = "Recurly API returned with status code: #{response.statusCode}"
  19. callback(error, response, body)
  20. sign : (parameters, callback) ->
  21. nestAttributesForQueryString = (attributes, base) ->
  22. newAttributes = {}
  23. for key, value of attributes
  24. if base?
  25. newKey = "#{base}[#{key}]"
  26. else
  27. newKey = key
  28. if typeof value == "object"
  29. for key, value of nestAttributesForQueryString(value, newKey)
  30. newAttributes[key] = value
  31. else
  32. newAttributes[newKey] = value
  33. return newAttributes
  34. crypto.randomBytes 32, (error, buffer) ->
  35. return callback error if error?
  36. parameters.nonce = buffer.toString "base64"
  37. parameters.timestamp = Math.round((new Date()).getTime() / 1000)
  38. unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters)
  39. signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex")
  40. signature = "#{signed}|#{unsignedQuery}"
  41. callback null, signature
  42. getSubscription: (subscriptionId, options, callback) ->
  43. callback = options unless callback?
  44. options ||= {}
  45. if options.recurlyJsResult
  46. url = "recurly_js/result/#{subscriptionId}"
  47. else
  48. url = "subscriptions/#{subscriptionId}"
  49. @apiRequest({
  50. url: url
  51. }, (error, response, body) =>
  52. return callback(error) if error?
  53. @_parseSubscriptionXml body, (error, recurlySubscription) =>
  54. return callback(error) if error?
  55. if options.includeAccount
  56. if recurlySubscription.account? and recurlySubscription.account.url?
  57. accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
  58. else
  59. return callback "I don't understand the response from Recurly"
  60. @getAccount accountId, (error, account) ->
  61. return callback(error) if error?
  62. recurlySubscription.account = account
  63. callback null, recurlySubscription
  64. else
  65. callback null, recurlySubscription
  66. )
  67. getAccount: (accountId, callback) ->
  68. @apiRequest({
  69. url: "accounts/#{accountId}"
  70. }, (error, response, body) =>
  71. return callback(error) if error?
  72. @_parseAccountXml body, callback
  73. )
  74. updateSubscription: (subscriptionId, options, callback) ->
  75. logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
  76. requestBody = """
  77. <subscription>
  78. <plan_code>#{options.plan_code}</plan_code>
  79. <timeframe>#{options.timeframe}</timeframe>
  80. </subscription>
  81. """
  82. @apiRequest({
  83. url : "subscriptions/#{subscriptionId}"
  84. method : "put"
  85. body : requestBody
  86. }, (error, response, responseBody) =>
  87. return callback(error) if error?
  88. @_parseSubscriptionXml responseBody, callback
  89. )
  90. cancelSubscription: (subscriptionId, callback) ->
  91. logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
  92. @apiRequest({
  93. url: "subscriptions/#{subscriptionId}/cancel",
  94. method: "put"
  95. }, (error, response, body) ->
  96. callback(error)
  97. )
  98. reactivateSubscription: (subscriptionId, callback) ->
  99. logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
  100. @apiRequest({
  101. url: "subscriptions/#{subscriptionId}/reactivate",
  102. method: "put"
  103. }, (error, response, body) ->
  104. callback(error)
  105. )
  106. redeemCoupon: (account_code, coupon_code, callback)->
  107. requestBody = """
  108. <redemption>
  109. <account_code>#{account_code}</account_code>
  110. <currency>USD</currency>
  111. </redemption>
  112. """
  113. logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
  114. @apiRequest({
  115. url : "coupons/#{coupon_code}/redeem"
  116. method : "post"
  117. body : requestBody
  118. }, (error, response, responseBody) =>
  119. if error?
  120. logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon"
  121. callback(error)
  122. )
  123. _parseSubscriptionXml: (xml, callback) ->
  124. @_parseXml xml, (error, data) ->
  125. return callback(error) if error?
  126. if data? and data.subscription?
  127. recurlySubscription = data.subscription
  128. else
  129. return callback "I don't understand the response from Recurly"
  130. callback null, recurlySubscription
  131. _parseAccountXml: (xml, callback) ->
  132. @_parseXml xml, (error, data) ->
  133. return callback(error) if error?
  134. if data? and data.account?
  135. account = data.account
  136. else
  137. return callback "I don't understand the response from Recurly"
  138. callback null, account
  139. _parseXml: (xml, callback) ->
  140. convertDataTypes = (data) ->
  141. if data? and data["$"]?
  142. if data["$"]["nil"] == "nil"
  143. data = null
  144. else if data["$"].href?
  145. data.url = data["$"].href
  146. delete data["$"]
  147. else if data["$"]["type"] == "integer"
  148. data = parseInt(data["_"], 10)
  149. else if data["$"]["type"] == "datetime"
  150. data = new Date(data["_"])
  151. else if data["$"]["type"] == "array"
  152. delete data["$"]
  153. array = []
  154. for key, value of data
  155. if value instanceof Array
  156. array = array.concat(convertDataTypes(value))
  157. else
  158. array.push(convertDataTypes(value))
  159. data = array
  160. if data instanceof Array
  161. data = (convertDataTypes(entry) for entry in data)
  162. else if typeof data == "object"
  163. for key, value of data
  164. data[key] = convertDataTypes(value)
  165. return data
  166. parser = new xml2js.Parser(
  167. explicitRoot : true
  168. explicitArray : false
  169. )
  170. parser.parseString xml, (error, data) ->
  171. return callback(error) if error?
  172. result = convertDataTypes(data)
  173. callback null, result