RecurlyWrapper.coffee 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. <coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
  15. <account>
  16. <account_code>#{user._id}</account_code>
  17. <email>#{user.email}</email>
  18. <first_name>#{user.first_name}</first_name>
  19. <last_name>#{user.last_name}</last_name>
  20. <billing_info>
  21. <token_id>#{recurly_token_id}</token_id>
  22. </billing_info>
  23. </account>
  24. </subscription>
  25. """
  26. @apiRequest({
  27. url : "subscriptions"
  28. method : "POST"
  29. body : requestBody
  30. }, (error, response, responseBody) =>
  31. return callback(error) if error?
  32. @_parseSubscriptionXml responseBody, callback
  33. )
  34. apiRequest : (options, callback) ->
  35. options.url = @apiUrl + "/" + options.url
  36. options.headers =
  37. "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
  38. "Accept" : "application/xml"
  39. "Content-Type" : "application/xml; charset=utf-8"
  40. request options, (error, response, body) ->
  41. unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
  42. logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
  43. error = "Recurly API returned with status code: #{response.statusCode}"
  44. callback(error, response, body)
  45. sign : (parameters, callback) ->
  46. nestAttributesForQueryString = (attributes, base) ->
  47. newAttributes = {}
  48. for key, value of attributes
  49. if base?
  50. newKey = "#{base}[#{key}]"
  51. else
  52. newKey = key
  53. if typeof value == "object"
  54. for key, value of nestAttributesForQueryString(value, newKey)
  55. newAttributes[key] = value
  56. else
  57. newAttributes[newKey] = value
  58. return newAttributes
  59. crypto.randomBytes 32, (error, buffer) ->
  60. return callback error if error?
  61. parameters.nonce = buffer.toString "base64"
  62. parameters.timestamp = Math.round((new Date()).getTime() / 1000)
  63. unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters)
  64. signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex")
  65. signature = "#{signed}|#{unsignedQuery}"
  66. callback null, signature
  67. getSubscriptions: (accountId, callback)->
  68. @apiRequest({
  69. url: "accounts/#{accountId}/subscriptions"
  70. }, (error, response, body) =>
  71. return callback(error) if error?
  72. @_parseXml body, callback
  73. )
  74. getSubscription: (subscriptionId, options, callback) ->
  75. callback = options unless callback?
  76. options ||= {}
  77. if options.recurlyJsResult
  78. url = "recurly_js/result/#{subscriptionId}"
  79. else
  80. url = "subscriptions/#{subscriptionId}"
  81. @apiRequest({
  82. url: url
  83. }, (error, response, body) =>
  84. return callback(error) if error?
  85. @_parseSubscriptionXml body, (error, recurlySubscription) =>
  86. return callback(error) if error?
  87. if options.includeAccount
  88. if recurlySubscription.account? and recurlySubscription.account.url?
  89. accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
  90. else
  91. return callback "I don't understand the response from Recurly"
  92. @getAccount accountId, (error, account) ->
  93. return callback(error) if error?
  94. recurlySubscription.account = account
  95. callback null, recurlySubscription
  96. else
  97. callback null, recurlySubscription
  98. )
  99. getAccounts: (callback)->
  100. allAccounts = []
  101. getPageOfAccounts = (cursor = null)=>
  102. opts =
  103. url: "accounts"
  104. qs:
  105. per_page:200
  106. if cursor?
  107. opts.qs.cursor = cursor
  108. @apiRequest opts, (error, response, body) =>
  109. return callback(error) if error?
  110. @_parseXml body, (err, data)->
  111. if err?
  112. logger.err err:err, "could not get accoutns"
  113. callback(err)
  114. allAccounts = allAccounts.concat(data.accounts)
  115. logger.log "got another #{data.accounts.length}, total now #{allAccounts.length}"
  116. cursor = response.headers.link?.match(/cursor=([0-9]+)&/)?[1]
  117. if cursor?
  118. getPageOfAccounts(cursor)
  119. else
  120. callback(err, allAccounts)
  121. getPageOfAccounts()
  122. getAccount: (accountId, callback) ->
  123. @apiRequest({
  124. url: "accounts/#{accountId}"
  125. }, (error, response, body) =>
  126. return callback(error) if error?
  127. @_parseAccountXml body, callback
  128. )
  129. getBillingInfo: (accountId, callback)->
  130. @apiRequest({
  131. url: "accounts/#{accountId}/billing_info"
  132. }, (error, response, body) =>
  133. return callback(error) if error?
  134. @_parseXml body, callback
  135. )
  136. updateSubscription: (subscriptionId, options, callback) ->
  137. logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
  138. requestBody = """
  139. <subscription>
  140. <plan_code>#{options.plan_code}</plan_code>
  141. <timeframe>#{options.timeframe}</timeframe>
  142. </subscription>
  143. """
  144. @apiRequest({
  145. url : "subscriptions/#{subscriptionId}"
  146. method : "put"
  147. body : requestBody
  148. }, (error, response, responseBody) =>
  149. return callback(error) if error?
  150. @_parseSubscriptionXml responseBody, callback
  151. )
  152. createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
  153. requestBody = """
  154. <coupon>
  155. <coupon_code>#{coupon_code}</coupon_code>
  156. <name>#{name}</name>
  157. <discount_type>dollars</discount_type>
  158. <discount_in_cents>
  159. <#{currencyCode}>#{discount_in_cents}</#{currencyCode}>
  160. </discount_in_cents>
  161. <plan_codes>
  162. <plan_code>#{plan_code}</plan_code>
  163. </plan_codes>
  164. <applies_to_all_plans>false</applies_to_all_plans>
  165. </coupon>
  166. """
  167. logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
  168. @apiRequest({
  169. url : "coupons"
  170. method : "post"
  171. body : requestBody
  172. }, (error, response, responseBody) =>
  173. if error?
  174. logger.err err:error, coupon_code:coupon_code, "error creating coupon"
  175. callback(error)
  176. )
  177. lookupCoupon: (coupon_code, callback)->
  178. @apiRequest({
  179. url: "coupons/#{coupon_code}"
  180. }, (error, response, body) =>
  181. return callback(error) if error?
  182. @_parseXml body, callback
  183. )
  184. cancelSubscription: (subscriptionId, callback) ->
  185. logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
  186. @apiRequest({
  187. url: "subscriptions/#{subscriptionId}/cancel",
  188. method: "put"
  189. }, (error, response, body) ->
  190. callback(error)
  191. )
  192. reactivateSubscription: (subscriptionId, callback) ->
  193. logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
  194. @apiRequest({
  195. url: "subscriptions/#{subscriptionId}/reactivate",
  196. method: "put"
  197. }, (error, response, body) ->
  198. callback(error)
  199. )
  200. redeemCoupon: (account_code, coupon_code, callback)->
  201. requestBody = """
  202. <redemption>
  203. <account_code>#{account_code}</account_code>
  204. <currency>USD</currency>
  205. </redemption>
  206. """
  207. logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
  208. @apiRequest({
  209. url : "coupons/#{coupon_code}/redeem"
  210. method : "post"
  211. body : requestBody
  212. }, (error, response, responseBody) =>
  213. if error?
  214. logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon"
  215. callback(error)
  216. )
  217. _parseSubscriptionXml: (xml, callback) ->
  218. @_parseXml xml, (error, data) ->
  219. return callback(error) if error?
  220. if data? and data.subscription?
  221. recurlySubscription = data.subscription
  222. else
  223. return callback "I don't understand the response from Recurly"
  224. callback null, recurlySubscription
  225. _parseAccountXml: (xml, callback) ->
  226. @_parseXml xml, (error, data) ->
  227. return callback(error) if error?
  228. if data? and data.account?
  229. account = data.account
  230. else
  231. return callback "I don't understand the response from Recurly"
  232. callback null, account
  233. _parseXml: (xml, callback) ->
  234. convertDataTypes = (data) ->
  235. if data? and data["$"]?
  236. if data["$"]["nil"] == "nil"
  237. data = null
  238. else if data["$"].href?
  239. data.url = data["$"].href
  240. delete data["$"]
  241. else if data["$"]["type"] == "integer"
  242. data = parseInt(data["_"], 10)
  243. else if data["$"]["type"] == "datetime"
  244. data = new Date(data["_"])
  245. else if data["$"]["type"] == "array"
  246. delete data["$"]
  247. array = []
  248. for key, value of data
  249. if value instanceof Array
  250. array = array.concat(convertDataTypes(value))
  251. else
  252. array.push(convertDataTypes(value))
  253. data = array
  254. if data instanceof Array
  255. data = (convertDataTypes(entry) for entry in data)
  256. else if typeof data == "object"
  257. for key, value of data
  258. data[key] = convertDataTypes(value)
  259. return data
  260. parser = new xml2js.Parser(
  261. explicitRoot : true
  262. explicitArray : false
  263. )
  264. parser.parseString xml, (error, data) ->
  265. return callback(error) if error?
  266. result = convertDataTypes(data)
  267. callback null, result