RecurlyWrapper.coffee 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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. Async = require('async')
  8. module.exports = RecurlyWrapper =
  9. apiUrl : "https://api.recurly.com/v2"
  10. _addressToXml: (address) ->
  11. allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code']
  12. resultString = "<billing_info>\n"
  13. for k, v of address
  14. if k == 'postal_code'
  15. k = 'zip'
  16. if v and (k in allowedKeys)
  17. resultString += "<#{k}#{if k == 'address2' then ' nil="nil"' else ''}>#{v || ''}</#{k}>\n"
  18. resultString += "</billing_info>\n"
  19. return resultString
  20. _paypal:
  21. checkAccountExists: (cache, next) ->
  22. user = cache.user
  23. recurly_token_id = cache.recurly_token_id
  24. subscriptionDetails = cache.subscriptionDetails
  25. logger.log {user_id: user._id, recurly_token_id}, "checking if recurly account exists for user"
  26. RecurlyWrapper.apiRequest({
  27. url: "accounts/#{user._id}"
  28. method: "GET"
  29. }, (error, response, responseBody) ->
  30. if error
  31. if response.statusCode == 404 # actually not an error in this case, just no existing account
  32. cache.userExists = false
  33. return next(null, cache)
  34. logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
  35. return next(error)
  36. logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
  37. RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
  38. if err
  39. logger.error {err, user_id: user._id, recurly_token_id}, "error parsing account"
  40. return next(err)
  41. cache.userExists = true
  42. cache.account = account
  43. return next(null, cache)
  44. )
  45. createAccount: (cache, next) ->
  46. user = cache.user
  47. recurly_token_id = cache.recurly_token_id
  48. subscriptionDetails = cache.subscriptionDetails
  49. address = subscriptionDetails.address
  50. if !address
  51. return next(new Error('no address in subscriptionDetails at createAccount stage'))
  52. if cache.userExists
  53. logger.log {user_id: user._id, recurly_token_id}, "user already exists in recurly"
  54. return next(null, cache)
  55. logger.log {user_id: user._id, recurly_token_id}, "creating user in recurly"
  56. requestBody = """
  57. <account>
  58. <account_code>#{user._id}</account_code>
  59. <email>#{user.email}</email>
  60. <first_name>#{user.first_name}</first_name>
  61. <last_name>#{user.last_name}</last_name>
  62. <address>
  63. <address1>#{address.address1}</address1>
  64. <address2>#{address.address2}</address2>
  65. <city>#{address.city || ''}</city>
  66. <state>#{address.state || ''}</state>
  67. <zip>#{address.zip || ''}</zip>
  68. <country>#{address.country}</country>
  69. </address>
  70. </account>
  71. """
  72. RecurlyWrapper.apiRequest({
  73. url : "accounts"
  74. method : "POST"
  75. body : requestBody
  76. }, (error, response, responseBody) =>
  77. if error
  78. logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating account"
  79. return next(error)
  80. RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
  81. if err
  82. logger.error {err, user_id: user._id, recurly_token_id}, "error creating account"
  83. return next(err)
  84. cache.account = account
  85. return next(null, cache)
  86. )
  87. createBillingInfo: (cache, next) ->
  88. user = cache.user
  89. recurly_token_id = cache.recurly_token_id
  90. subscriptionDetails = cache.subscriptionDetails
  91. logger.log {user_id: user._id, recurly_token_id}, "creating billing info in recurly"
  92. accountCode = cache?.account?.account_code
  93. if !accountCode
  94. return next(new Error('no account code at createBillingInfo stage'))
  95. requestBody = """
  96. <billing_info>
  97. <token_id>#{recurly_token_id}</token_id>
  98. </billing_info>
  99. """
  100. RecurlyWrapper.apiRequest({
  101. url: "accounts/#{accountCode}/billing_info"
  102. method: "POST"
  103. body: requestBody
  104. }, (error, response, responseBody) =>
  105. if error
  106. logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating billing info"
  107. return next(error)
  108. RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
  109. if err
  110. logger.error {err, user_id: user._id, accountCode, recurly_token_id}, "error creating billing info"
  111. return next(err)
  112. cache.billingInfo = billingInfo
  113. return next(null, cache)
  114. )
  115. setAddress: (cache, next) ->
  116. user = cache.user
  117. recurly_token_id = cache.recurly_token_id
  118. subscriptionDetails = cache.subscriptionDetails
  119. logger.log {user_id: user._id, recurly_token_id}, "setting billing address in recurly"
  120. accountCode = cache?.account?.account_code
  121. if !accountCode
  122. return next(new Error('no account code at setAddress stage'))
  123. address = subscriptionDetails.address
  124. if !address
  125. return next(new Error('no address in subscriptionDetails at setAddress stage'))
  126. requestBody = RecurlyWrapper._addressToXml(address)
  127. RecurlyWrapper.apiRequest({
  128. url: "accounts/#{accountCode}/billing_info"
  129. method: "PUT"
  130. body: requestBody
  131. }, (error, response, responseBody) =>
  132. if error
  133. logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while setting address"
  134. return next(error)
  135. RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
  136. if err
  137. logger.error {err, user_id: user._id, recurly_token_id}, "error updating billing info"
  138. return next(err)
  139. cache.billingInfo = billingInfo
  140. return next(null, cache)
  141. )
  142. createSubscription: (cache, next) ->
  143. user = cache.user
  144. recurly_token_id = cache.recurly_token_id
  145. subscriptionDetails = cache.subscriptionDetails
  146. logger.log {user_id: user._id, recurly_token_id}, "creating subscription in recurly"
  147. requestBody = """
  148. <subscription>
  149. <plan_code>#{subscriptionDetails.plan_code}</plan_code>
  150. <currency>#{subscriptionDetails.currencyCode}</currency>
  151. <coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
  152. <account>
  153. <account_code>#{user._id}</account_code>
  154. </account>
  155. </subscription>
  156. """ # TODO: check account details and billing
  157. RecurlyWrapper.apiRequest({
  158. url : "subscriptions"
  159. method : "POST"
  160. body : requestBody
  161. }, (error, response, responseBody) =>
  162. if error
  163. logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating subscription"
  164. return next(error)
  165. RecurlyWrapper._parseSubscriptionXml responseBody, (err, subscription) ->
  166. if err
  167. logger.error {err, user_id: user._id, recurly_token_id}, "error creating subscription"
  168. return next(err)
  169. cache.subscription = subscription
  170. return next(null, cache)
  171. )
  172. _createPaypalSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
  173. logger.log {user_id: user._id, recurly_token_id}, "starting process of creating paypal subscription"
  174. # We use `async.waterfall` to run each of these actions in sequence
  175. # passing a `cache` object along the way. The cache is initialized
  176. # with required data, and `async.apply` to pass the cache to the first function
  177. cache = {user, recurly_token_id, subscriptionDetails}
  178. Async.waterfall([
  179. Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
  180. RecurlyWrapper._paypal.createAccount,
  181. RecurlyWrapper._paypal.createBillingInfo,
  182. RecurlyWrapper._paypal.setAddress,
  183. RecurlyWrapper._paypal.createSubscription,
  184. ], (err, result) ->
  185. if err
  186. logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
  187. return callback(err)
  188. if !result.subscription
  189. err = new Error('no subscription object in result')
  190. logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
  191. return callback(err)
  192. logger.log {user_id: user._id, recurly_token_id}, "done creating paypal subscription for user"
  193. callback(null, result.subscription)
  194. )
  195. _createCreditCardSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
  196. requestBody = """
  197. <subscription>
  198. <plan_code>#{subscriptionDetails.plan_code}</plan_code>
  199. <currency>#{subscriptionDetails.currencyCode}</currency>
  200. <coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
  201. <account>
  202. <account_code>#{user._id}</account_code>
  203. <email>#{user.email}</email>
  204. <first_name>#{user.first_name}</first_name>
  205. <last_name>#{user.last_name}</last_name>
  206. <billing_info>
  207. <token_id>#{recurly_token_id}</token_id>
  208. </billing_info>
  209. </account>
  210. </subscription>
  211. """
  212. RecurlyWrapper.apiRequest({
  213. url : "subscriptions"
  214. method : "POST"
  215. body : requestBody
  216. }, (error, response, responseBody) =>
  217. return callback(error) if error?
  218. RecurlyWrapper._parseSubscriptionXml responseBody, callback
  219. )
  220. createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
  221. isPaypal = subscriptionDetails.isPaypal
  222. logger.log {user_id: user._id, isPaypal, recurly_token_id}, "setting up subscription in recurly"
  223. fn = if isPaypal then RecurlyWrapper._createPaypalSubscription else RecurlyWrapper._createCreditCardSubscription
  224. fn user, subscriptionDetails, recurly_token_id, callback
  225. apiRequest : (options, callback) ->
  226. options.url = RecurlyWrapper.apiUrl + "/" + options.url
  227. options.headers =
  228. "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
  229. "Accept" : "application/xml"
  230. "Content-Type" : "application/xml; charset=utf-8"
  231. request options, (error, response, body) ->
  232. unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
  233. logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
  234. error = "Recurly API returned with status code: #{response.statusCode}"
  235. callback(error, response, body)
  236. sign : (parameters, callback) ->
  237. nestAttributesForQueryString = (attributes, base) ->
  238. newAttributes = {}
  239. for key, value of attributes
  240. if base?
  241. newKey = "#{base}[#{key}]"
  242. else
  243. newKey = key
  244. if typeof value == "object"
  245. for key, value of nestAttributesForQueryString(value, newKey)
  246. newAttributes[key] = value
  247. else
  248. newAttributes[newKey] = value
  249. return newAttributes
  250. crypto.randomBytes 32, (error, buffer) ->
  251. return callback error if error?
  252. parameters.nonce = buffer.toString "base64"
  253. parameters.timestamp = Math.round((new Date()).getTime() / 1000)
  254. unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters)
  255. signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex")
  256. signature = "#{signed}|#{unsignedQuery}"
  257. callback null, signature
  258. getSubscriptions: (accountId, callback)->
  259. RecurlyWrapper.apiRequest({
  260. url: "accounts/#{accountId}/subscriptions"
  261. }, (error, response, body) =>
  262. return callback(error) if error?
  263. RecurlyWrapper._parseXml body, callback
  264. )
  265. getSubscription: (subscriptionId, options, callback) ->
  266. callback = options unless callback?
  267. options ||= {}
  268. if options.recurlyJsResult
  269. url = "recurly_js/result/#{subscriptionId}"
  270. else
  271. url = "subscriptions/#{subscriptionId}"
  272. RecurlyWrapper.apiRequest({
  273. url: url
  274. }, (error, response, body) =>
  275. return callback(error) if error?
  276. RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) =>
  277. return callback(error) if error?
  278. if options.includeAccount
  279. if recurlySubscription.account? and recurlySubscription.account.url?
  280. accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
  281. else
  282. return callback "I don't understand the response from Recurly"
  283. RecurlyWrapper.getAccount accountId, (error, account) ->
  284. return callback(error) if error?
  285. recurlySubscription.account = account
  286. callback null, recurlySubscription
  287. else
  288. callback null, recurlySubscription
  289. )
  290. getAccounts: (callback)->
  291. allAccounts = []
  292. getPageOfAccounts = (cursor = null)=>
  293. opts =
  294. url: "accounts"
  295. qs:
  296. per_page:200
  297. if cursor?
  298. opts.qs.cursor = cursor
  299. RecurlyWrapper.apiRequest opts, (error, response, body) =>
  300. return callback(error) if error?
  301. RecurlyWrapper._parseXml body, (err, data)->
  302. if err?
  303. logger.err err:err, "could not get accoutns"
  304. callback(err)
  305. allAccounts = allAccounts.concat(data.accounts)
  306. logger.log "got another #{data.accounts.length}, total now #{allAccounts.length}"
  307. cursor = response.headers.link?.match(/cursor=([0-9]+)&/)?[1]
  308. if cursor?
  309. getPageOfAccounts(cursor)
  310. else
  311. callback(err, allAccounts)
  312. getPageOfAccounts()
  313. getAccount: (accountId, callback) ->
  314. RecurlyWrapper.apiRequest({
  315. url: "accounts/#{accountId}"
  316. }, (error, response, body) =>
  317. return callback(error) if error?
  318. RecurlyWrapper._parseAccountXml body, callback
  319. )
  320. getBillingInfo: (accountId, callback)->
  321. RecurlyWrapper.apiRequest({
  322. url: "accounts/#{accountId}/billing_info"
  323. }, (error, response, body) =>
  324. return callback(error) if error?
  325. RecurlyWrapper._parseXml body, callback
  326. )
  327. updateSubscription: (subscriptionId, options, callback) ->
  328. logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
  329. requestBody = """
  330. <subscription>
  331. <plan_code>#{options.plan_code}</plan_code>
  332. <timeframe>#{options.timeframe}</timeframe>
  333. </subscription>
  334. """
  335. RecurlyWrapper.apiRequest({
  336. url : "subscriptions/#{subscriptionId}"
  337. method : "put"
  338. body : requestBody
  339. }, (error, response, responseBody) =>
  340. return callback(error) if error?
  341. RecurlyWrapper._parseSubscriptionXml responseBody, callback
  342. )
  343. createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
  344. requestBody = """
  345. <coupon>
  346. <coupon_code>#{coupon_code}</coupon_code>
  347. <name>#{name}</name>
  348. <discount_type>dollars</discount_type>
  349. <discount_in_cents>
  350. <#{currencyCode}>#{discount_in_cents}</#{currencyCode}>
  351. </discount_in_cents>
  352. <plan_codes>
  353. <plan_code>#{plan_code}</plan_code>
  354. </plan_codes>
  355. <applies_to_all_plans>false</applies_to_all_plans>
  356. </coupon>
  357. """
  358. logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
  359. RecurlyWrapper.apiRequest({
  360. url : "coupons"
  361. method : "post"
  362. body : requestBody
  363. }, (error, response, responseBody) =>
  364. if error?
  365. logger.err err:error, coupon_code:coupon_code, "error creating coupon"
  366. callback(error)
  367. )
  368. lookupCoupon: (coupon_code, callback)->
  369. RecurlyWrapper.apiRequest({
  370. url: "coupons/#{coupon_code}"
  371. }, (error, response, body) =>
  372. return callback(error) if error?
  373. RecurlyWrapper._parseXml body, callback
  374. )
  375. cancelSubscription: (subscriptionId, callback) ->
  376. logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
  377. RecurlyWrapper.apiRequest({
  378. url: "subscriptions/#{subscriptionId}/cancel",
  379. method: "put"
  380. }, (error, response, body) ->
  381. callback(error)
  382. )
  383. reactivateSubscription: (subscriptionId, callback) ->
  384. logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
  385. RecurlyWrapper.apiRequest({
  386. url: "subscriptions/#{subscriptionId}/reactivate",
  387. method: "put"
  388. }, (error, response, body) ->
  389. callback(error)
  390. )
  391. redeemCoupon: (account_code, coupon_code, callback)->
  392. requestBody = """
  393. <redemption>
  394. <account_code>#{account_code}</account_code>
  395. <currency>USD</currency>
  396. </redemption>
  397. """
  398. logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
  399. RecurlyWrapper.apiRequest({
  400. url : "coupons/#{coupon_code}/redeem"
  401. method : "post"
  402. body : requestBody
  403. }, (error, response, responseBody) =>
  404. if error?
  405. logger.err err:error, account_code:account_code, coupon_code:coupon_code, "error redeeming coupon"
  406. callback(error)
  407. )
  408. extendTrial: (subscriptionId, daysUntilExpire = 7, callback)->
  409. next_renewal_date = new Date()
  410. next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
  411. logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
  412. RecurlyWrapper.apiRequest({
  413. url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
  414. method : "put"
  415. }, (error, response, responseBody) =>
  416. if error?
  417. logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial"
  418. callback(error)
  419. )
  420. _parseSubscriptionXml: (xml, callback) ->
  421. RecurlyWrapper._parseXml xml, (error, data) ->
  422. return callback(error) if error?
  423. if data? and data.subscription?
  424. recurlySubscription = data.subscription
  425. else
  426. return callback "I don't understand the response from Recurly"
  427. callback null, recurlySubscription
  428. _parseAccountXml: (xml, callback) ->
  429. RecurlyWrapper._parseXml xml, (error, data) ->
  430. return callback(error) if error?
  431. if data? and data.account?
  432. account = data.account
  433. else
  434. return callback "I don't understand the response from Recurly"
  435. callback null, account
  436. _parseBillingInfoXml: (xml, callback) ->
  437. RecurlyWrapper._parseXml xml, (error, data) ->
  438. return callback(error) if error?
  439. if data? and data.billing_info?
  440. billingInfo = data.billing_info
  441. else
  442. return callback "I don't understand the response from Recurly"
  443. callback null, billingInfo
  444. _parseXml: (xml, callback) ->
  445. convertDataTypes = (data) ->
  446. if data? and data["$"]?
  447. if data["$"]["nil"] == "nil"
  448. data = null
  449. else if data["$"].href?
  450. data.url = data["$"].href
  451. delete data["$"]
  452. else if data["$"]["type"] == "integer"
  453. data = parseInt(data["_"], 10)
  454. else if data["$"]["type"] == "datetime"
  455. data = new Date(data["_"])
  456. else if data["$"]["type"] == "array"
  457. delete data["$"]
  458. array = []
  459. for key, value of data
  460. if value instanceof Array
  461. array = array.concat(convertDataTypes(value))
  462. else
  463. array.push(convertDataTypes(value))
  464. data = array
  465. if data instanceof Array
  466. data = (convertDataTypes(entry) for entry in data)
  467. else if typeof data == "object"
  468. for key, value of data
  469. data[key] = convertDataTypes(value)
  470. return data
  471. parser = new xml2js.Parser(
  472. explicitRoot : true
  473. explicitArray : false
  474. )
  475. parser.parseString xml, (error, data) ->
  476. return callback(error) if error?
  477. result = convertDataTypes(data)
  478. callback null, result