RecurlyWrapper.coffee 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. allAccounts = []
  100. getPageOfAccounts = (cursor = null)=>
  101. opts =
  102. url: "accounts"
  103. qs:
  104. per_page:200
  105. if cursor?
  106. opts.qs.cursor = cursor
  107. @apiRequest opts, (error, response, body) =>
  108. return callback(error) if error?
  109. @_parseXml body, (err, data)->
  110. if err?
  111. logger.err err:err, "could not get accoutns"
  112. callback(err)
  113. allAccounts = allAccounts.concat(data.accounts)
  114. logger.log "got another #{data.accounts.length}, total now #{allAccounts.length}"
  115. cursor = response.headers.link?.match(/cursor=([0-9]+)&/)?[1]
  116. if cursor?
  117. getPageOfAccounts(cursor)
  118. else
  119. callback(err, allAccounts)
  120. getPageOfAccounts()
  121. getAccount: (accountId, callback) ->
  122. @apiRequest({
  123. url: "accounts/#{accountId}"
  124. }, (error, response, body) =>
  125. return callback(error) if error?
  126. @_parseAccountXml body, callback
  127. )
  128. getBillingInfo: (accountId, callback)->
  129. @apiRequest({
  130. url: "accounts/#{accountId}/billing_info"
  131. }, (error, response, body) =>
  132. return callback(error) if error?
  133. @_parseXml body, callback
  134. )
  135. updateSubscription: (subscriptionId, options, callback) ->
  136. logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
  137. requestBody = """
  138. <subscription>
  139. <plan_code>#{options.plan_code}</plan_code>
  140. <timeframe>#{options.timeframe}</timeframe>
  141. </subscription>
  142. """
  143. @apiRequest({
  144. url : "subscriptions/#{subscriptionId}"
  145. method : "put"
  146. body : requestBody
  147. }, (error, response, responseBody) =>
  148. return callback(error) if error?
  149. @_parseSubscriptionXml responseBody, callback
  150. )
  151. createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
  152. requestBody = """
  153. <coupon>
  154. <coupon_code>#{coupon_code}</coupon_code>
  155. <name>#{name}</name>
  156. <discount_type>dollars</discount_type>
  157. <discount_in_cents>
  158. <#{currencyCode}>#{discount_in_cents}</#{currencyCode}>
  159. </discount_in_cents>
  160. <plan_codes>
  161. <plan_code>#{plan_code}</plan_code>
  162. </plan_codes>
  163. <applies_to_all_plans>false</applies_to_all_plans>
  164. </coupon>
  165. """
  166. logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
  167. @apiRequest({
  168. url : "coupons"
  169. method : "post"
  170. body : requestBody
  171. }, (error, response, responseBody) =>
  172. if error?
  173. logger.err err:error, coupon_code:coupon_code, "error creating coupon"
  174. callback(error)
  175. )
  176. lookupCoupon: (coupon_code, callback)->
  177. @apiRequest({
  178. url: "coupons/#{coupon_code}"
  179. }, (error, response, body) =>
  180. return callback(error) if error?
  181. @_parseXml body, callback
  182. )
  183. cancelSubscription: (subscriptionId, callback) ->
  184. logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
  185. @apiRequest({
  186. url: "subscriptions/#{subscriptionId}/cancel",
  187. method: "put"
  188. }, (error, response, body) ->
  189. callback(error)
  190. )
  191. reactivateSubscription: (subscriptionId, callback) ->
  192. logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
  193. @apiRequest({
  194. url: "subscriptions/#{subscriptionId}/reactivate",
  195. method: "put"
  196. }, (error, response, body) ->
  197. callback(error)
  198. )
  199. redeemCoupon: (account_code, coupon_code, callback)->
  200. requestBody = """
  201. <redemption>
  202. <account_code>#{account_code}</account_code>
  203. <currency>USD</currency>
  204. </redemption>
  205. """
  206. logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
  207. @apiRequest({
  208. url : "coupons/#{coupon_code}/redeem"
  209. method : "post"
  210. body : requestBody
  211. }, (error, response, responseBody) =>
  212. if error?
  213. logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon"
  214. callback(error)
  215. )
  216. _parseSubscriptionXml: (xml, callback) ->
  217. @_parseXml xml, (error, data) ->
  218. return callback(error) if error?
  219. if data? and data.subscription?
  220. recurlySubscription = data.subscription
  221. else
  222. return callback "I don't understand the response from Recurly"
  223. callback null, recurlySubscription
  224. _parseAccountXml: (xml, callback) ->
  225. @_parseXml xml, (error, data) ->
  226. return callback(error) if error?
  227. if data? and data.account?
  228. account = data.account
  229. else
  230. return callback "I don't understand the response from Recurly"
  231. callback null, account
  232. _parseXml: (xml, callback) ->
  233. convertDataTypes = (data) ->
  234. if data? and data["$"]?
  235. if data["$"]["nil"] == "nil"
  236. data = null
  237. else if data["$"].href?
  238. data.url = data["$"].href
  239. delete data["$"]
  240. else if data["$"]["type"] == "integer"
  241. data = parseInt(data["_"], 10)
  242. else if data["$"]["type"] == "datetime"
  243. data = new Date(data["_"])
  244. else if data["$"]["type"] == "array"
  245. delete data["$"]
  246. array = []
  247. for key, value of data
  248. if value instanceof Array
  249. array = array.concat(convertDataTypes(value))
  250. else
  251. array.push(convertDataTypes(value))
  252. data = array
  253. if data instanceof Array
  254. data = (convertDataTypes(entry) for entry in data)
  255. else if typeof data == "object"
  256. for key, value of data
  257. data[key] = convertDataTypes(value)
  258. return data
  259. parser = new xml2js.Parser(
  260. explicitRoot : true
  261. explicitArray : false
  262. )
  263. parser.parseString xml, (error, data) ->
  264. return callback(error) if error?
  265. result = convertDataTypes(data)
  266. callback null, result