RecurlyWrapper.js 30 KB

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