RecurlyWrapper.coffee 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. getSubscriptions: (accountId, callback)->
  43. @apiRequest({
  44. url: "accounts/#{accountId}/subscriptions"
  45. }, (error, response, body) =>
  46. return callback(error) if error?
  47. @_parseXml body, callback
  48. )
  49. getSubscription: (subscriptionId, options, callback) ->
  50. callback = options unless callback?
  51. options ||= {}
  52. if options.recurlyJsResult
  53. url = "recurly_js/result/#{subscriptionId}"
  54. else
  55. url = "subscriptions/#{subscriptionId}"
  56. @apiRequest({
  57. url: url
  58. }, (error, response, body) =>
  59. return callback(error) if error?
  60. @_parseSubscriptionXml body, (error, recurlySubscription) =>
  61. return callback(error) if error?
  62. if options.includeAccount
  63. if recurlySubscription.account? and recurlySubscription.account.url?
  64. accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
  65. else
  66. return callback "I don't understand the response from Recurly"
  67. @getAccount accountId, (error, account) ->
  68. return callback(error) if error?
  69. recurlySubscription.account = account
  70. callback null, recurlySubscription
  71. else
  72. callback null, recurlySubscription
  73. )
  74. getAccounts: (callback)->
  75. @apiRequest({
  76. url: "accounts"
  77. qs:
  78. per_page:2000
  79. }, (error, response, body) =>
  80. return callback(error) if error?
  81. @_parseXml body, callback
  82. )
  83. getAccount: (accountId, callback) ->
  84. @apiRequest({
  85. url: "accounts/#{accountId}"
  86. }, (error, response, body) =>
  87. return callback(error) if error?
  88. @_parseAccountXml body, callback
  89. )
  90. getBillingInfo: (accountId, callback)->
  91. @apiRequest({
  92. url: "accounts/#{accountId}/billing_info"
  93. }, (error, response, body) =>
  94. return callback(error) if error?
  95. @_parseXml body, callback
  96. )
  97. updateSubscription: (subscriptionId, options, callback) ->
  98. logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
  99. requestBody = """
  100. <subscription>
  101. <plan_code>#{options.plan_code}</plan_code>
  102. <timeframe>#{options.timeframe}</timeframe>
  103. </subscription>
  104. """
  105. @apiRequest({
  106. url : "subscriptions/#{subscriptionId}"
  107. method : "put"
  108. body : requestBody
  109. }, (error, response, responseBody) =>
  110. return callback(error) if error?
  111. @_parseSubscriptionXml responseBody, callback
  112. )
  113. createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, callback)->
  114. requestBody = """
  115. <coupon>
  116. <coupon_code>#{coupon_code}</coupon_code>
  117. <name>#{name}</name>
  118. <discount_type>dollars</discount_type>
  119. <discount_in_cents>
  120. <#{currencyCode}>#{discount_in_cents}</#{currencyCode}>
  121. </discount_in_cents>
  122. </coupon>
  123. """
  124. logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
  125. @apiRequest({
  126. url : "coupons"
  127. method : "post"
  128. body : requestBody
  129. }, (error, response, responseBody) =>
  130. if error?
  131. logger.err err:error, coupon_code:coupon_code, "error creating coupon"
  132. callback(error)
  133. )
  134. lookupCoupon: (coupon_code, callback)->
  135. @apiRequest({
  136. url: "coupons/#{coupon_code}"
  137. }, (error, response, body) =>
  138. return callback(error) if error?
  139. @_parseXml body, callback
  140. )
  141. cancelSubscription: (subscriptionId, callback) ->
  142. logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
  143. @apiRequest({
  144. url: "subscriptions/#{subscriptionId}/cancel",
  145. method: "put"
  146. }, (error, response, body) ->
  147. callback(error)
  148. )
  149. reactivateSubscription: (subscriptionId, callback) ->
  150. logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
  151. @apiRequest({
  152. url: "subscriptions/#{subscriptionId}/reactivate",
  153. method: "put"
  154. }, (error, response, body) ->
  155. callback(error)
  156. )
  157. redeemCoupon: (account_code, coupon_code, callback)->
  158. requestBody = """
  159. <redemption>
  160. <account_code>#{account_code}</account_code>
  161. <currency>USD</currency>
  162. </redemption>
  163. """
  164. logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
  165. @apiRequest({
  166. url : "coupons/#{coupon_code}/redeem"
  167. method : "post"
  168. body : requestBody
  169. }, (error, response, responseBody) =>
  170. if error?
  171. logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon"
  172. callback(error)
  173. )
  174. _parseSubscriptionXml: (xml, callback) ->
  175. @_parseXml xml, (error, data) ->
  176. return callback(error) if error?
  177. if data? and data.subscription?
  178. recurlySubscription = data.subscription
  179. else
  180. return callback "I don't understand the response from Recurly"
  181. callback null, recurlySubscription
  182. _parseAccountXml: (xml, callback) ->
  183. @_parseXml xml, (error, data) ->
  184. return callback(error) if error?
  185. if data? and data.account?
  186. account = data.account
  187. else
  188. return callback "I don't understand the response from Recurly"
  189. callback null, account
  190. _parseXml: (xml, callback) ->
  191. convertDataTypes = (data) ->
  192. if data? and data["$"]?
  193. if data["$"]["nil"] == "nil"
  194. data = null
  195. else if data["$"].href?
  196. data.url = data["$"].href
  197. delete data["$"]
  198. else if data["$"]["type"] == "integer"
  199. data = parseInt(data["_"], 10)
  200. else if data["$"]["type"] == "datetime"
  201. data = new Date(data["_"])
  202. else if data["$"]["type"] == "array"
  203. delete data["$"]
  204. array = []
  205. for key, value of data
  206. if value instanceof Array
  207. array = array.concat(convertDataTypes(value))
  208. else
  209. array.push(convertDataTypes(value))
  210. data = array
  211. if data instanceof Array
  212. data = (convertDataTypes(entry) for entry in data)
  213. else if typeof data == "object"
  214. for key, value of data
  215. data[key] = convertDataTypes(value)
  216. return data
  217. parser = new xml2js.Parser(
  218. explicitRoot : true
  219. explicitArray : false
  220. )
  221. parser.parseString xml, (error, data) ->
  222. return callback(error) if error?
  223. result = convertDataTypes(data)
  224. callback null, result