RecurlyWrapper.js 30 KB

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