RecurlyWrapper.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. no-unused-vars,
  6. node/no-deprecated-api,
  7. standard/no-callback-literal,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS101: Remove unnecessary use of Array.from
  14. * DS102: Remove unnecessary code created because of implicit returns
  15. * DS103: Rewrite code to no longer use __guard__
  16. * DS207: Consider shorter variations of null checks
  17. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  18. */
  19. let RecurlyWrapper
  20. const querystring = require('querystring')
  21. const crypto = require('crypto')
  22. const request = require('request')
  23. const Settings = require('settings-sharelatex')
  24. const xml2js = require('xml2js')
  25. const logger = require('logger-sharelatex')
  26. const Async = require('async')
  27. module.exports = RecurlyWrapper = {
  28. apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2',
  29. _paypal: {
  30. checkAccountExists(cache, next) {
  31. const { user } = cache
  32. const { recurly_token_id } = cache
  33. const { subscriptionDetails } = cache
  34. logger.log(
  35. { user_id: user._id, recurly_token_id },
  36. 'checking if recurly account exists for user'
  37. )
  38. return RecurlyWrapper.apiRequest(
  39. {
  40. url: `accounts/${user._id}`,
  41. method: 'GET',
  42. expect404: true
  43. },
  44. function(error, response, responseBody) {
  45. if (error) {
  46. logger.warn(
  47. { error, user_id: user._id, recurly_token_id },
  48. 'error response from recurly while checking account'
  49. )
  50. return next(error)
  51. }
  52. if (response.statusCode === 404) {
  53. // actually not an error in this case, just no existing account
  54. logger.log(
  55. { user_id: user._id, recurly_token_id },
  56. 'user does not currently exist in recurly, proceed'
  57. )
  58. cache.userExists = false
  59. return next(null, cache)
  60. }
  61. logger.log(
  62. { user_id: user._id, recurly_token_id },
  63. 'user appears to exist in recurly'
  64. )
  65. return RecurlyWrapper._parseAccountXml(responseBody, function(
  66. err,
  67. account
  68. ) {
  69. if (err) {
  70. logger.warn(
  71. { err, user_id: user._id, recurly_token_id },
  72. 'error parsing account'
  73. )
  74. return next(err)
  75. }
  76. cache.userExists = true
  77. cache.account = account
  78. return next(null, cache)
  79. })
  80. }
  81. )
  82. },
  83. createAccount(cache, next) {
  84. const { user } = cache
  85. const { recurly_token_id } = cache
  86. const { subscriptionDetails } = cache
  87. const { address } = subscriptionDetails
  88. if (!address) {
  89. return next(
  90. new Error('no address in subscriptionDetails at createAccount stage')
  91. )
  92. }
  93. if (cache.userExists) {
  94. logger.log(
  95. { user_id: user._id, recurly_token_id },
  96. 'user already exists in recurly'
  97. )
  98. return next(null, cache)
  99. }
  100. logger.log(
  101. { user_id: user._id, recurly_token_id },
  102. 'creating user in recurly'
  103. )
  104. const data = {
  105. account_code: user._id,
  106. email: user.email,
  107. first_name: user.first_name,
  108. last_name: user.last_name,
  109. address: {
  110. address1: address.address1,
  111. address2: address.address2 || '',
  112. city: address.city || '',
  113. state: address.state || '',
  114. zip: address.zip || '',
  115. country: address.country
  116. }
  117. }
  118. const requestBody = RecurlyWrapper._buildXml('account', data)
  119. return RecurlyWrapper.apiRequest(
  120. {
  121. url: 'accounts',
  122. method: 'POST',
  123. body: requestBody
  124. },
  125. (error, response, responseBody) => {
  126. if (error) {
  127. logger.warn(
  128. { error, user_id: user._id, recurly_token_id },
  129. 'error response from recurly while creating account'
  130. )
  131. return next(error)
  132. }
  133. return RecurlyWrapper._parseAccountXml(responseBody, function(
  134. err,
  135. account
  136. ) {
  137. if (err) {
  138. logger.warn(
  139. { err, user_id: user._id, recurly_token_id },
  140. 'error creating account'
  141. )
  142. return next(err)
  143. }
  144. cache.account = account
  145. return next(null, cache)
  146. })
  147. }
  148. )
  149. },
  150. createBillingInfo(cache, next) {
  151. const { user } = cache
  152. const { recurly_token_id } = cache
  153. const { subscriptionDetails } = cache
  154. logger.log(
  155. { user_id: user._id, recurly_token_id },
  156. 'creating billing info in recurly'
  157. )
  158. const accountCode = __guard__(
  159. cache != null ? cache.account : undefined,
  160. x1 => x1.account_code
  161. )
  162. if (!accountCode) {
  163. return next(new Error('no account code at createBillingInfo stage'))
  164. }
  165. const data = { token_id: recurly_token_id }
  166. const requestBody = RecurlyWrapper._buildXml('billing_info', data)
  167. return RecurlyWrapper.apiRequest(
  168. {
  169. url: `accounts/${accountCode}/billing_info`,
  170. method: 'POST',
  171. body: requestBody
  172. },
  173. (error, response, responseBody) => {
  174. if (error) {
  175. logger.warn(
  176. { error, user_id: user._id, recurly_token_id },
  177. 'error response from recurly while creating billing info'
  178. )
  179. return next(error)
  180. }
  181. return RecurlyWrapper._parseBillingInfoXml(responseBody, function(
  182. err,
  183. billingInfo
  184. ) {
  185. if (err) {
  186. logger.warn(
  187. { err, user_id: user._id, accountCode, recurly_token_id },
  188. 'error creating billing info'
  189. )
  190. return next(err)
  191. }
  192. cache.billingInfo = billingInfo
  193. return next(null, cache)
  194. })
  195. }
  196. )
  197. },
  198. setAddress(cache, next) {
  199. const { user } = cache
  200. const { recurly_token_id } = cache
  201. const { subscriptionDetails } = cache
  202. logger.log(
  203. { user_id: user._id, recurly_token_id },
  204. 'setting billing address in recurly'
  205. )
  206. const accountCode = __guard__(
  207. cache != null ? cache.account : undefined,
  208. x1 => x1.account_code
  209. )
  210. if (!accountCode) {
  211. return next(new Error('no account code at setAddress stage'))
  212. }
  213. const { address } = subscriptionDetails
  214. if (!address) {
  215. return next(
  216. new Error('no address in subscriptionDetails at setAddress stage')
  217. )
  218. }
  219. const data = {
  220. address1: address.address1,
  221. address2: address.address2 || '',
  222. city: address.city || '',
  223. state: address.state || '',
  224. zip: address.zip || '',
  225. country: address.country
  226. }
  227. const requestBody = RecurlyWrapper._buildXml('billing_info', data)
  228. return RecurlyWrapper.apiRequest(
  229. {
  230. url: `accounts/${accountCode}/billing_info`,
  231. method: 'PUT',
  232. body: requestBody
  233. },
  234. (error, response, responseBody) => {
  235. if (error) {
  236. logger.warn(
  237. { error, user_id: user._id, recurly_token_id },
  238. 'error response from recurly while setting address'
  239. )
  240. return next(error)
  241. }
  242. return RecurlyWrapper._parseBillingInfoXml(responseBody, function(
  243. err,
  244. billingInfo
  245. ) {
  246. if (err) {
  247. logger.warn(
  248. { err, user_id: user._id, recurly_token_id },
  249. 'error updating billing info'
  250. )
  251. return next(err)
  252. }
  253. cache.billingInfo = billingInfo
  254. return next(null, cache)
  255. })
  256. }
  257. )
  258. },
  259. createSubscription(cache, next) {
  260. const { user } = cache
  261. const { recurly_token_id } = cache
  262. const { subscriptionDetails } = cache
  263. logger.log(
  264. { user_id: user._id, recurly_token_id },
  265. 'creating subscription in recurly'
  266. )
  267. const data = {
  268. plan_code: subscriptionDetails.plan_code,
  269. currency: subscriptionDetails.currencyCode,
  270. coupon_code: subscriptionDetails.coupon_code,
  271. account: {
  272. account_code: user._id
  273. }
  274. }
  275. const requestBody = RecurlyWrapper._buildXml('subscription', data)
  276. return RecurlyWrapper.apiRequest(
  277. {
  278. url: 'subscriptions',
  279. method: 'POST',
  280. body: requestBody
  281. },
  282. (error, response, responseBody) => {
  283. if (error) {
  284. logger.warn(
  285. { error, user_id: user._id, recurly_token_id },
  286. 'error response from recurly while creating subscription'
  287. )
  288. return next(error)
  289. }
  290. return RecurlyWrapper._parseSubscriptionXml(responseBody, function(
  291. err,
  292. subscription
  293. ) {
  294. if (err) {
  295. logger.warn(
  296. { err, user_id: user._id, recurly_token_id },
  297. 'error creating subscription'
  298. )
  299. return next(err)
  300. }
  301. cache.subscription = subscription
  302. return next(null, cache)
  303. })
  304. }
  305. )
  306. }
  307. },
  308. _createPaypalSubscription(
  309. user,
  310. subscriptionDetails,
  311. recurly_token_id,
  312. callback
  313. ) {
  314. logger.log(
  315. { user_id: user._id, recurly_token_id },
  316. 'starting process of creating paypal subscription'
  317. )
  318. // We use `async.waterfall` to run each of these actions in sequence
  319. // passing a `cache` object along the way. The cache is initialized
  320. // with required data, and `async.apply` to pass the cache to the first function
  321. const cache = { user, recurly_token_id, subscriptionDetails }
  322. return Async.waterfall(
  323. [
  324. Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
  325. RecurlyWrapper._paypal.createAccount,
  326. RecurlyWrapper._paypal.createBillingInfo,
  327. RecurlyWrapper._paypal.setAddress,
  328. RecurlyWrapper._paypal.createSubscription
  329. ],
  330. function(err, result) {
  331. if (err) {
  332. logger.warn(
  333. { err, user_id: user._id, recurly_token_id },
  334. 'error in paypal subscription creation process'
  335. )
  336. return callback(err)
  337. }
  338. if (!result.subscription) {
  339. err = new Error('no subscription object in result')
  340. logger.warn(
  341. { err, user_id: user._id, recurly_token_id },
  342. 'error in paypal subscription creation process'
  343. )
  344. return callback(err)
  345. }
  346. logger.log(
  347. { user_id: user._id, recurly_token_id },
  348. 'done creating paypal subscription for user'
  349. )
  350. return callback(null, result.subscription)
  351. }
  352. )
  353. },
  354. _createCreditCardSubscription(
  355. user,
  356. subscriptionDetails,
  357. recurly_token_id,
  358. callback
  359. ) {
  360. const data = {
  361. plan_code: subscriptionDetails.plan_code,
  362. currency: subscriptionDetails.currencyCode,
  363. coupon_code: subscriptionDetails.coupon_code,
  364. account: {
  365. account_code: user._id,
  366. email: user.email,
  367. first_name: subscriptionDetails.first_name || user.first_name,
  368. last_name: subscriptionDetails.last_name || user.last_name,
  369. billing_info: {
  370. token_id: recurly_token_id
  371. }
  372. }
  373. }
  374. const requestBody = RecurlyWrapper._buildXml('subscription', data)
  375. return RecurlyWrapper.apiRequest(
  376. {
  377. url: 'subscriptions',
  378. method: 'POST',
  379. body: requestBody
  380. },
  381. (error, response, responseBody) => {
  382. if (error != null) {
  383. return callback(error)
  384. }
  385. return RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
  386. }
  387. )
  388. },
  389. createSubscription(user, subscriptionDetails, recurly_token_id, callback) {
  390. const { isPaypal } = subscriptionDetails
  391. logger.log(
  392. { user_id: user._id, isPaypal, recurly_token_id },
  393. 'setting up subscription in recurly'
  394. )
  395. const fn = isPaypal
  396. ? RecurlyWrapper._createPaypalSubscription
  397. : RecurlyWrapper._createCreditCardSubscription
  398. return fn(user, subscriptionDetails, recurly_token_id, callback)
  399. },
  400. apiRequest(options, callback) {
  401. options.url = RecurlyWrapper.apiUrl + '/' + options.url
  402. options.headers = {
  403. Authorization: `Basic ${new Buffer(Settings.apis.recurly.apiKey).toString(
  404. 'base64'
  405. )}`,
  406. Accept: 'application/xml',
  407. 'Content-Type': 'application/xml; charset=utf-8',
  408. 'X-Api-Version': Settings.apis.recurly.apiVersion
  409. }
  410. const { expect404 } = options
  411. delete options.expect404
  412. return request(options, function(error, response, body) {
  413. if (
  414. error == null &&
  415. response.statusCode !== 200 &&
  416. response.statusCode !== 201 &&
  417. response.statusCode !== 204 &&
  418. (response.statusCode !== 404 || !expect404)
  419. ) {
  420. logger.warn(
  421. {
  422. err: error,
  423. body,
  424. options,
  425. statusCode: response != null ? response.statusCode : undefined
  426. },
  427. 'error returned from recurly'
  428. )
  429. error = `Recurly API returned with status code: ${response.statusCode}`
  430. }
  431. if (response.statusCode === 404 && expect404) {
  432. logger.log(
  433. { url: options.url, method: options.method },
  434. 'got 404 response from recurly, expected as valid response'
  435. )
  436. }
  437. return callback(error, response, body)
  438. })
  439. },
  440. getSubscriptions(accountId, callback) {
  441. return RecurlyWrapper.apiRequest(
  442. {
  443. url: `accounts/${accountId}/subscriptions`
  444. },
  445. (error, response, body) => {
  446. if (error != null) {
  447. return callback(error)
  448. }
  449. return RecurlyWrapper._parseXml(body, callback)
  450. }
  451. )
  452. },
  453. getSubscription(subscriptionId, options, callback) {
  454. let url
  455. if (callback == null) {
  456. callback = options
  457. }
  458. if (!options) {
  459. options = {}
  460. }
  461. if (options.recurlyJsResult) {
  462. url = `recurly_js/result/${subscriptionId}`
  463. } else {
  464. url = `subscriptions/${subscriptionId}`
  465. }
  466. return RecurlyWrapper.apiRequest(
  467. {
  468. url
  469. },
  470. (error, response, body) => {
  471. if (error != null) {
  472. return callback(error)
  473. }
  474. return RecurlyWrapper._parseSubscriptionXml(
  475. body,
  476. (error, recurlySubscription) => {
  477. if (error != null) {
  478. return callback(error)
  479. }
  480. if (options.includeAccount) {
  481. let accountId
  482. if (
  483. recurlySubscription.account != null &&
  484. recurlySubscription.account.url != null
  485. ) {
  486. accountId = recurlySubscription.account.url.match(
  487. /accounts\/(.*)/
  488. )[1]
  489. } else {
  490. return callback(
  491. new Error("I don't understand the response from Recurly")
  492. )
  493. }
  494. return RecurlyWrapper.getAccount(accountId, function(
  495. error,
  496. account
  497. ) {
  498. if (error != null) {
  499. return callback(error)
  500. }
  501. recurlySubscription.account = account
  502. return callback(null, recurlySubscription)
  503. })
  504. } else {
  505. return callback(null, recurlySubscription)
  506. }
  507. }
  508. )
  509. }
  510. )
  511. },
  512. getAccounts(callback) {
  513. let allAccounts = []
  514. var getPageOfAccounts = (cursor = null) => {
  515. const opts = {
  516. url: 'accounts',
  517. qs: {
  518. per_page: 200
  519. }
  520. }
  521. if (cursor != null) {
  522. opts.qs.cursor = cursor
  523. }
  524. return RecurlyWrapper.apiRequest(opts, (error, response, body) => {
  525. if (error != null) {
  526. return callback(error)
  527. }
  528. return RecurlyWrapper._parseXml(body, function(err, data) {
  529. if (err != null) {
  530. logger.warn({ err }, 'could not get accoutns')
  531. callback(err)
  532. }
  533. allAccounts = allAccounts.concat(data.accounts)
  534. logger.log(
  535. `got another ${data.accounts.length}, total now ${
  536. allAccounts.length
  537. }`
  538. )
  539. cursor = __guard__(
  540. response.headers.link != null
  541. ? response.headers.link.match(/cursor=([0-9]+)&/)
  542. : undefined,
  543. x1 => x1[1]
  544. )
  545. if (cursor != null) {
  546. return getPageOfAccounts(cursor)
  547. } else {
  548. return callback(err, allAccounts)
  549. }
  550. })
  551. })
  552. }
  553. return getPageOfAccounts()
  554. },
  555. getAccount(accountId, callback) {
  556. return RecurlyWrapper.apiRequest(
  557. {
  558. url: `accounts/${accountId}`
  559. },
  560. (error, response, body) => {
  561. if (error != null) {
  562. return callback(error)
  563. }
  564. return RecurlyWrapper._parseAccountXml(body, callback)
  565. }
  566. )
  567. },
  568. getAccountActiveCoupons(accountId, callback) {
  569. return RecurlyWrapper.apiRequest(
  570. {
  571. url: `accounts/${accountId}/redemptions`
  572. },
  573. (error, response, body) => {
  574. if (error != null) {
  575. return callback(error)
  576. }
  577. return RecurlyWrapper._parseRedemptionsXml(body, function(
  578. error,
  579. redemptions
  580. ) {
  581. if (error != null) {
  582. return callback(error)
  583. }
  584. const activeRedemptions = redemptions.filter(
  585. redemption => redemption.state === 'active'
  586. )
  587. const couponCodes = activeRedemptions.map(
  588. redemption => redemption.coupon_code
  589. )
  590. return Async.map(couponCodes, RecurlyWrapper.getCoupon, function(
  591. error,
  592. coupons
  593. ) {
  594. if (error != null) {
  595. return callback(error)
  596. }
  597. return callback(null, coupons)
  598. })
  599. })
  600. }
  601. )
  602. },
  603. getCoupon(couponCode, callback) {
  604. const opts = { url: `coupons/${couponCode}` }
  605. return RecurlyWrapper.apiRequest(opts, (error, response, body) =>
  606. RecurlyWrapper._parseCouponXml(body, callback)
  607. )
  608. },
  609. getBillingInfo(accountId, callback) {
  610. return RecurlyWrapper.apiRequest(
  611. {
  612. url: `accounts/${accountId}/billing_info`
  613. },
  614. (error, response, body) => {
  615. if (error != null) {
  616. return callback(error)
  617. }
  618. return RecurlyWrapper._parseXml(body, callback)
  619. }
  620. )
  621. },
  622. updateSubscription(subscriptionId, options, callback) {
  623. logger.log(
  624. { subscriptionId, options },
  625. 'telling recurly to update subscription'
  626. )
  627. const data = {
  628. plan_code: options.plan_code,
  629. timeframe: options.timeframe
  630. }
  631. const requestBody = RecurlyWrapper._buildXml('subscription', data)
  632. return RecurlyWrapper.apiRequest(
  633. {
  634. url: `subscriptions/${subscriptionId}`,
  635. method: 'put',
  636. body: requestBody
  637. },
  638. (error, response, responseBody) => {
  639. if (error != null) {
  640. return callback(error)
  641. }
  642. return RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
  643. }
  644. )
  645. },
  646. createFixedAmmountCoupon(
  647. coupon_code,
  648. name,
  649. currencyCode,
  650. discount_in_cents,
  651. plan_code,
  652. callback
  653. ) {
  654. const data = {
  655. coupon_code,
  656. name,
  657. discount_type: 'dollars',
  658. discount_in_cents: {},
  659. plan_codes: {
  660. plan_code
  661. },
  662. applies_to_all_plans: false
  663. }
  664. data.discount_in_cents[currencyCode] = discount_in_cents
  665. const requestBody = RecurlyWrapper._buildXml('coupon', data)
  666. logger.log({ coupon_code, requestBody }, 'creating coupon')
  667. return RecurlyWrapper.apiRequest(
  668. {
  669. url: 'coupons',
  670. method: 'post',
  671. body: requestBody
  672. },
  673. (error, response, responseBody) => {
  674. if (error != null) {
  675. logger.warn({ err: error, coupon_code }, 'error creating coupon')
  676. }
  677. return callback(error)
  678. }
  679. )
  680. },
  681. lookupCoupon(coupon_code, callback) {
  682. return RecurlyWrapper.apiRequest(
  683. {
  684. url: `coupons/${coupon_code}`
  685. },
  686. (error, response, body) => {
  687. if (error != null) {
  688. return callback(error)
  689. }
  690. return RecurlyWrapper._parseXml(body, callback)
  691. }
  692. )
  693. },
  694. cancelSubscription(subscriptionId, callback) {
  695. logger.log({ subscriptionId }, 'telling recurly to cancel subscription')
  696. return RecurlyWrapper.apiRequest(
  697. {
  698. url: `subscriptions/${subscriptionId}/cancel`,
  699. method: 'put'
  700. },
  701. function(error, response, body) {
  702. if (error != null) {
  703. return RecurlyWrapper._parseXml(body, function(_err, parsed) {
  704. if (
  705. __guard__(
  706. parsed != null ? parsed.error : undefined,
  707. x1 => x1.description
  708. ) === "A canceled subscription can't transition to canceled"
  709. ) {
  710. logger.log(
  711. { subscriptionId, error, body },
  712. 'subscription already cancelled, not really an error, proceeding'
  713. )
  714. return callback(null)
  715. } else {
  716. return callback(error)
  717. }
  718. })
  719. } else {
  720. return callback(null)
  721. }
  722. }
  723. )
  724. },
  725. reactivateSubscription(subscriptionId, callback) {
  726. logger.log(
  727. { subscriptionId },
  728. 'telling recurly to reactivating subscription'
  729. )
  730. return RecurlyWrapper.apiRequest(
  731. {
  732. url: `subscriptions/${subscriptionId}/reactivate`,
  733. method: 'put'
  734. },
  735. (error, response, body) => callback(error)
  736. )
  737. },
  738. redeemCoupon(account_code, coupon_code, callback) {
  739. const data = {
  740. account_code,
  741. currency: 'USD'
  742. }
  743. const requestBody = RecurlyWrapper._buildXml('redemption', data)
  744. logger.log(
  745. { account_code, coupon_code, requestBody },
  746. 'redeeming coupon for user'
  747. )
  748. return RecurlyWrapper.apiRequest(
  749. {
  750. url: `coupons/${coupon_code}/redeem`,
  751. method: 'post',
  752. body: requestBody
  753. },
  754. (error, response, responseBody) => {
  755. if (error != null) {
  756. logger.warn(
  757. { err: error, account_code, coupon_code },
  758. 'error redeeming coupon'
  759. )
  760. }
  761. return callback(error)
  762. }
  763. )
  764. },
  765. extendTrial(subscriptionId, daysUntilExpire, callback) {
  766. if (daysUntilExpire == null) {
  767. daysUntilExpire = 7
  768. }
  769. const next_renewal_date = new Date()
  770. next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
  771. logger.log(
  772. { subscriptionId, daysUntilExpire },
  773. 'Exending Free trial for user'
  774. )
  775. return RecurlyWrapper.apiRequest(
  776. {
  777. url: `/subscriptions/${subscriptionId}/postpone?next_renewal_date=${next_renewal_date}&bulk=false`,
  778. method: 'put'
  779. },
  780. (error, response, responseBody) => {
  781. if (error != null) {
  782. logger.warn(
  783. { err: error, subscriptionId, daysUntilExpire },
  784. 'error exending trial'
  785. )
  786. }
  787. return callback(error)
  788. }
  789. )
  790. },
  791. listAccountActiveSubscriptions(account_id, callback) {
  792. if (callback == null) {
  793. callback = function(error, subscriptions) {}
  794. }
  795. return RecurlyWrapper.apiRequest(
  796. {
  797. url: `accounts/${account_id}/subscriptions`,
  798. qs: {
  799. state: 'active'
  800. },
  801. expect404: true
  802. },
  803. function(error, response, body) {
  804. if (error != null) {
  805. return callback(error)
  806. }
  807. if (response.statusCode === 404) {
  808. return callback(null, [])
  809. } else {
  810. return RecurlyWrapper._parseSubscriptionsXml(body, callback)
  811. }
  812. }
  813. )
  814. },
  815. _parseSubscriptionsXml(xml, callback) {
  816. return RecurlyWrapper._parseXmlAndGetAttribute(
  817. xml,
  818. 'subscriptions',
  819. callback
  820. )
  821. },
  822. _parseSubscriptionXml(xml, callback) {
  823. return RecurlyWrapper._parseXmlAndGetAttribute(
  824. xml,
  825. 'subscription',
  826. callback
  827. )
  828. },
  829. _parseAccountXml(xml, callback) {
  830. return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'account', callback)
  831. },
  832. _parseBillingInfoXml(xml, callback) {
  833. return RecurlyWrapper._parseXmlAndGetAttribute(
  834. xml,
  835. 'billing_info',
  836. callback
  837. )
  838. },
  839. _parseRedemptionsXml(xml, callback) {
  840. return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'redemptions', callback)
  841. },
  842. _parseCouponXml(xml, callback) {
  843. return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback)
  844. },
  845. _parseXmlAndGetAttribute(xml, attribute, callback) {
  846. return RecurlyWrapper._parseXml(xml, function(error, data) {
  847. if (error != null) {
  848. return callback(error)
  849. }
  850. if (data != null && data[attribute] != null) {
  851. return callback(null, data[attribute])
  852. } else {
  853. return callback(
  854. new Error("I don't understand the response from Recurly")
  855. )
  856. }
  857. })
  858. },
  859. _parseXml(xml, callback) {
  860. var convertDataTypes = function(data) {
  861. let key, value
  862. if (data != null && data['$'] != null) {
  863. if (data['$']['nil'] === 'nil') {
  864. data = null
  865. } else if (data['$'].href != null) {
  866. data.url = data['$'].href
  867. delete data['$']
  868. } else if (data['$']['type'] === 'integer') {
  869. data = parseInt(data['_'], 10)
  870. } else if (data['$']['type'] === 'datetime') {
  871. data = new Date(data['_'])
  872. } else if (data['$']['type'] === 'array') {
  873. delete data['$']
  874. let array = []
  875. for (key in data) {
  876. value = data[key]
  877. if (value instanceof Array) {
  878. array = array.concat(convertDataTypes(value))
  879. } else {
  880. array.push(convertDataTypes(value))
  881. }
  882. }
  883. data = array
  884. }
  885. }
  886. if (data instanceof Array) {
  887. data = Array.from(data).map(entry => convertDataTypes(entry))
  888. } else if (typeof data === 'object') {
  889. for (key in data) {
  890. value = data[key]
  891. data[key] = convertDataTypes(value)
  892. }
  893. }
  894. return data
  895. }
  896. const parser = new xml2js.Parser({
  897. explicitRoot: true,
  898. explicitArray: false,
  899. emptyTag: ''
  900. })
  901. return parser.parseString(xml, function(error, data) {
  902. if (error != null) {
  903. return callback(error)
  904. }
  905. const result = convertDataTypes(data)
  906. return callback(null, result)
  907. })
  908. },
  909. _buildXml(rootName, data) {
  910. const options = {
  911. headless: true,
  912. renderOpts: {
  913. pretty: true,
  914. indent: '\t'
  915. },
  916. rootName
  917. }
  918. const builder = new xml2js.Builder(options)
  919. return builder.buildObject(data)
  920. }
  921. }
  922. function __guard__(value, transform) {
  923. return typeof value !== 'undefined' && value !== null
  924. ? transform(value)
  925. : undefined
  926. }