RecurlyWrapper.coffee 20 KB

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