RecurlyWrapper.js 27 KB

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