RecurlyWrapper.coffee 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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. extendTrial: (subscriptionId, daysUntilExpire = 7, callback)->
  218. next_renewal_date = new Date()
  219. next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
  220. logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
  221. @apiRequest({
  222. url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
  223. method : "put"
  224. }, (error, response, responseBody) =>
  225. if error?
  226. logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial"
  227. callback(error)
  228. )
  229. _parseSubscriptionXml: (xml, callback) ->
  230. @_parseXml xml, (error, data) ->
  231. return callback(error) if error?
  232. if data? and data.subscription?
  233. recurlySubscription = data.subscription
  234. else
  235. return callback "I don't understand the response from Recurly"
  236. callback null, recurlySubscription
  237. _parseAccountXml: (xml, callback) ->
  238. @_parseXml xml, (error, data) ->
  239. return callback(error) if error?
  240. if data? and data.account?
  241. account = data.account
  242. else
  243. return callback "I don't understand the response from Recurly"
  244. callback null, account
  245. _parseXml: (xml, callback) ->
  246. convertDataTypes = (data) ->
  247. if data? and data["$"]?
  248. if data["$"]["nil"] == "nil"
  249. data = null
  250. else if data["$"].href?
  251. data.url = data["$"].href
  252. delete data["$"]
  253. else if data["$"]["type"] == "integer"
  254. data = parseInt(data["_"], 10)
  255. else if data["$"]["type"] == "datetime"
  256. data = new Date(data["_"])
  257. else if data["$"]["type"] == "array"
  258. delete data["$"]
  259. array = []
  260. for key, value of data
  261. if value instanceof Array
  262. array = array.concat(convertDataTypes(value))
  263. else
  264. array.push(convertDataTypes(value))
  265. data = array
  266. if data instanceof Array
  267. data = (convertDataTypes(entry) for entry in data)
  268. else if typeof data == "object"
  269. for key, value of data
  270. data[key] = convertDataTypes(value)
  271. return data
  272. parser = new xml2js.Parser(
  273. explicitRoot : true
  274. explicitArray : false
  275. )
  276. parser.parseString xml, (error, data) ->
  277. return callback(error) if error?
  278. result = convertDataTypes(data)
  279. callback null, result