RecurlyWrapper.js 28 KB

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