RecurlyWrapper.coffee 8.5 KB

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