RecurlyWrapper.coffee 7.1 KB

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