RecurlyWrapper.js 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  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. logger.warn(
  459. {
  460. err: error,
  461. body,
  462. options,
  463. statusCode: response ? response.statusCode : undefined,
  464. },
  465. 'error returned from recurly'
  466. )
  467. error = new OError(
  468. `Recurly API returned with status code: ${response.statusCode}`,
  469. { statusCode: response.statusCode }
  470. )
  471. }
  472. callback(error, response, body)
  473. })
  474. },
  475. getSubscriptions(accountId, callback) {
  476. RecurlyWrapper.apiRequest(
  477. {
  478. url: `accounts/${accountId}/subscriptions`,
  479. },
  480. (error, response, body) => {
  481. if (error) {
  482. return callback(error)
  483. }
  484. RecurlyWrapper._parseXml(body, callback)
  485. }
  486. )
  487. },
  488. getSubscription(subscriptionId, options, callback) {
  489. let url
  490. if (!callback) {
  491. callback = options
  492. }
  493. if (!options) {
  494. options = {}
  495. }
  496. if (options.recurlyJsResult) {
  497. url = `recurly_js/result/${subscriptionId}`
  498. } else {
  499. url = `subscriptions/${subscriptionId}`
  500. }
  501. RecurlyWrapper.apiRequest(
  502. {
  503. url,
  504. },
  505. (error, response, body) => {
  506. if (error) {
  507. return callback(error)
  508. }
  509. RecurlyWrapper._parseSubscriptionXml(
  510. body,
  511. (error, recurlySubscription) => {
  512. if (error) {
  513. return callback(error)
  514. }
  515. if (options.includeAccount) {
  516. let accountId
  517. if (
  518. recurlySubscription.account &&
  519. recurlySubscription.account.url
  520. ) {
  521. accountId =
  522. recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
  523. } else {
  524. return callback(
  525. new Error("I don't understand the response from Recurly")
  526. )
  527. }
  528. RecurlyWrapper.getAccount(accountId, function (error, account) {
  529. if (error) {
  530. return callback(error)
  531. }
  532. recurlySubscription.account = account
  533. callback(null, recurlySubscription)
  534. })
  535. } else {
  536. callback(null, recurlySubscription)
  537. }
  538. }
  539. )
  540. }
  541. )
  542. },
  543. getPaginatedEndpoint(resource, queryParams, callback) {
  544. queryParams.per_page = queryParams.per_page || 200
  545. let allItems = []
  546. const getPage = (cursor = null) => {
  547. const opts = {
  548. url: resource,
  549. qs: queryParams,
  550. }
  551. if (cursor) {
  552. opts.qs.cursor = cursor
  553. }
  554. return RecurlyWrapper.apiRequest(opts, (error, response, body) => {
  555. if (error) {
  556. return callback(error)
  557. }
  558. return RecurlyWrapper._parseXml(body, function (err, data) {
  559. if (err) {
  560. logger.warn({ err }, 'could not get accoutns')
  561. return callback(err)
  562. }
  563. const items = data[resource]
  564. allItems = allItems.concat(items)
  565. logger.debug(
  566. `got another ${items.length}, total now ${allItems.length}`
  567. )
  568. const match = response.headers.link?.match(
  569. /cursor=([0-9.]+%3A[0-9.]+)&/
  570. )
  571. cursor = match && match[1]
  572. if (cursor) {
  573. cursor = decodeURIComponent(cursor)
  574. return getPage(cursor)
  575. } else {
  576. callback(err, allItems)
  577. }
  578. })
  579. })
  580. }
  581. getPage()
  582. },
  583. getAccount(accountId, callback) {
  584. RecurlyWrapper.apiRequest(
  585. {
  586. url: `accounts/${accountId}`,
  587. },
  588. (error, response, body) => {
  589. if (error) {
  590. return callback(error)
  591. }
  592. RecurlyWrapper._parseAccountXml(body, callback)
  593. }
  594. )
  595. },
  596. updateAccountEmailAddress,
  597. getAccountActiveCoupons(accountId, callback) {
  598. RecurlyWrapper.apiRequest(
  599. {
  600. url: `accounts/${accountId}/redemptions`,
  601. },
  602. (error, response, body) => {
  603. if (error) {
  604. return callback(error)
  605. }
  606. RecurlyWrapper._parseRedemptionsXml(
  607. body,
  608. function (error, redemptions) {
  609. if (error) {
  610. return callback(error)
  611. }
  612. const activeRedemptions = redemptions.filter(
  613. redemption => redemption.state === 'active'
  614. )
  615. const couponCodes = activeRedemptions.map(
  616. redemption => redemption.coupon_code
  617. )
  618. Async.map(
  619. couponCodes,
  620. RecurlyWrapper.getCoupon,
  621. function (error, coupons) {
  622. if (error) {
  623. return callback(error)
  624. }
  625. return callback(null, coupons)
  626. }
  627. )
  628. }
  629. )
  630. }
  631. )
  632. },
  633. getCoupon(couponCode, callback) {
  634. const opts = { url: `coupons/${couponCode}` }
  635. RecurlyWrapper.apiRequest(opts, (error, response, body) => {
  636. if (error) {
  637. return callback(error)
  638. }
  639. RecurlyWrapper._parseCouponXml(body, callback)
  640. })
  641. },
  642. getBillingInfo(accountId, callback) {
  643. RecurlyWrapper.apiRequest(
  644. {
  645. url: `accounts/${accountId}/billing_info`,
  646. },
  647. (error, response, body) => {
  648. if (error) {
  649. return callback(error)
  650. }
  651. RecurlyWrapper._parseXml(body, callback)
  652. }
  653. )
  654. },
  655. getAccountPastDueInvoices(accountId, callback) {
  656. RecurlyWrapper.apiRequest(
  657. {
  658. url: `accounts/${accountId}/invoices?state=past_due`,
  659. },
  660. (error, response, body) => {
  661. if (error) {
  662. return callback(error)
  663. }
  664. RecurlyWrapper._parseInvoicesXml(body, callback)
  665. }
  666. )
  667. },
  668. attemptInvoiceCollection(invoiceId, callback) {
  669. RecurlyWrapper.apiRequest(
  670. {
  671. url: `invoices/${invoiceId}/collect`,
  672. method: 'put',
  673. },
  674. callback
  675. )
  676. },
  677. updateSubscription(subscriptionId, options, callback) {
  678. logger.debug(
  679. { subscriptionId, options },
  680. 'telling recurly to update subscription'
  681. )
  682. const data = {
  683. plan_code: options.plan_code,
  684. timeframe: options.timeframe,
  685. }
  686. let requestBody
  687. try {
  688. requestBody = RecurlyWrapper._buildXml('subscription', data)
  689. } catch (error) {
  690. return callback(
  691. OError.tag(error, 'error building xml', { subscriptionId })
  692. )
  693. }
  694. RecurlyWrapper.apiRequest(
  695. {
  696. url: `subscriptions/${subscriptionId}`,
  697. method: 'put',
  698. body: requestBody,
  699. },
  700. (error, response, responseBody) => {
  701. if (error) {
  702. return callback(error)
  703. }
  704. RecurlyWrapper._parseSubscriptionXml(responseBody, callback)
  705. }
  706. )
  707. },
  708. createFixedAmmountCoupon(
  709. couponCode,
  710. name,
  711. currencyCode,
  712. discountInCents,
  713. planCode,
  714. callback
  715. ) {
  716. const data = {
  717. coupon_code: couponCode,
  718. name,
  719. discount_type: 'dollars',
  720. discount_in_cents: {},
  721. plan_codes: {
  722. plan_code: planCode,
  723. },
  724. applies_to_all_plans: false,
  725. }
  726. data.discount_in_cents[currencyCode] = discountInCents
  727. let requestBody
  728. try {
  729. requestBody = RecurlyWrapper._buildXml('coupon', data)
  730. } catch (error) {
  731. return callback(
  732. OError.tag(error, 'error building xml', {
  733. couponCode,
  734. name,
  735. })
  736. )
  737. }
  738. logger.debug({ couponCode, requestBody }, 'creating coupon')
  739. RecurlyWrapper.apiRequest(
  740. {
  741. url: 'coupons',
  742. method: 'post',
  743. body: requestBody,
  744. },
  745. (error, response, responseBody) => {
  746. if (error) {
  747. logger.warn({ err: error, couponCode }, 'error creating coupon')
  748. }
  749. callback(error)
  750. }
  751. )
  752. },
  753. lookupCoupon(couponCode, callback) {
  754. RecurlyWrapper.apiRequest(
  755. {
  756. url: `coupons/${couponCode}`,
  757. },
  758. (error, response, body) => {
  759. if (error) {
  760. return callback(error)
  761. }
  762. RecurlyWrapper._parseXml(body, callback)
  763. }
  764. )
  765. },
  766. redeemCoupon(accountCode, couponCode, callback) {
  767. const data = {
  768. account_code: accountCode,
  769. currency: 'USD',
  770. }
  771. let requestBody
  772. try {
  773. requestBody = RecurlyWrapper._buildXml('redemption', data)
  774. } catch (error) {
  775. return callback(
  776. OError.tag(error, 'error building xml', {
  777. accountCode,
  778. couponCode,
  779. })
  780. )
  781. }
  782. logger.debug(
  783. { accountCode, couponCode, requestBody },
  784. 'redeeming coupon for user'
  785. )
  786. RecurlyWrapper.apiRequest(
  787. {
  788. url: `coupons/${couponCode}/redeem`,
  789. method: 'post',
  790. body: requestBody,
  791. },
  792. (error, response, responseBody) => {
  793. if (error) {
  794. logger.warn(
  795. { err: error, accountCode, couponCode },
  796. 'error redeeming coupon'
  797. )
  798. }
  799. callback(error)
  800. }
  801. )
  802. },
  803. extendTrial(subscriptionId, daysUntilExpire, callback) {
  804. if (daysUntilExpire == null) {
  805. daysUntilExpire = 7
  806. }
  807. const nextRenewalDate = new Date()
  808. nextRenewalDate.setDate(nextRenewalDate.getDate() + daysUntilExpire)
  809. logger.debug(
  810. { subscriptionId, daysUntilExpire },
  811. 'Exending Free trial for user'
  812. )
  813. RecurlyWrapper.apiRequest(
  814. {
  815. url: `/subscriptions/${subscriptionId}/postpone?next_bill_date=${nextRenewalDate}&bulk=false`,
  816. method: 'put',
  817. },
  818. (error, response, responseBody) => {
  819. if (error) {
  820. logger.warn(
  821. { err: error, subscriptionId, daysUntilExpire },
  822. 'error exending trial'
  823. )
  824. }
  825. callback(error)
  826. }
  827. )
  828. },
  829. listAccountActiveSubscriptions(accountId, callback) {
  830. RecurlyWrapper.apiRequest(
  831. {
  832. url: `accounts/${accountId}/subscriptions`,
  833. qs: {
  834. state: 'active',
  835. },
  836. expect404: true,
  837. },
  838. function (error, response, body) {
  839. if (error) {
  840. return callback(error)
  841. }
  842. if (response.statusCode === 404) {
  843. callback(null, [])
  844. } else {
  845. RecurlyWrapper._parseSubscriptionsXml(body, callback)
  846. }
  847. }
  848. )
  849. },
  850. _handle422Response(body, callback) {
  851. RecurlyWrapper._parseErrorsXml(body, (error, data) => {
  852. if (error) {
  853. return callback(error)
  854. }
  855. let errorData = {}
  856. if (data.transaction_error) {
  857. errorData = {
  858. message: data.transaction_error.merchant_message,
  859. info: {
  860. category: data.transaction_error.error_category,
  861. gatewayCode: data.transaction_error.gateway_error_code,
  862. public: {
  863. code: data.transaction_error.error_code,
  864. message: data.transaction_error.customer_message,
  865. },
  866. },
  867. }
  868. if (data.transaction_error.three_d_secure_action_token_id) {
  869. errorData.info.public.threeDSecureActionTokenId =
  870. data.transaction_error.three_d_secure_action_token_id
  871. }
  872. } else if (data.error && data.error._) {
  873. // fallback for errors that don't have a `transaction_error` field, but
  874. // instead a `error` field with a message (e.g. VATMOSS errors)
  875. errorData = {
  876. info: {
  877. public: {
  878. message: data.error._,
  879. },
  880. },
  881. }
  882. }
  883. callback(new SubscriptionErrors.RecurlyTransactionError(errorData))
  884. })
  885. },
  886. _parseSubscriptionsXml(xml, callback) {
  887. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'subscriptions', callback)
  888. },
  889. _parseSubscriptionXml(xml, callback) {
  890. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'subscription', callback)
  891. },
  892. _parseAccountXml(xml, callback) {
  893. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'account', callback)
  894. },
  895. _parseBillingInfoXml(xml, callback) {
  896. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'billing_info', callback)
  897. },
  898. _parseRedemptionsXml(xml, callback) {
  899. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'redemptions', callback)
  900. },
  901. _parseCouponXml(xml, callback) {
  902. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback)
  903. },
  904. _parseErrorsXml(xml, callback) {
  905. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'errors', callback)
  906. },
  907. _parseInvoicesXml(xml, callback) {
  908. RecurlyWrapper._parseXmlAndGetAttribute(xml, 'invoices', callback)
  909. },
  910. _parseXmlAndGetAttribute(xml, attribute, callback) {
  911. RecurlyWrapper._parseXml(xml, function (error, data) {
  912. if (error) {
  913. return callback(error)
  914. }
  915. if (data && data[attribute] != null) {
  916. callback(null, data[attribute])
  917. } else {
  918. callback(new Error("I don't understand the response from Recurly"))
  919. }
  920. })
  921. },
  922. _parseXml(xml, callback) {
  923. function convertDataTypes(data) {
  924. let key, value
  925. if (data && data.$) {
  926. if (data.$.nil === 'nil') {
  927. data = null
  928. } else if (data.$.href) {
  929. data.url = data.$.href
  930. delete data.$
  931. } else if (data.$.type === 'integer') {
  932. data = parseInt(data._, 10)
  933. } else if (data.$.type === 'datetime') {
  934. data = new Date(data._)
  935. } else if (data.$.type === 'array') {
  936. delete data.$
  937. let array = []
  938. for (key in data) {
  939. value = data[key]
  940. if (value instanceof Array) {
  941. array = array.concat(convertDataTypes(value))
  942. } else {
  943. array.push(convertDataTypes(value))
  944. }
  945. }
  946. data = array
  947. }
  948. }
  949. if (data instanceof Array) {
  950. data = data.map(entry => convertDataTypes(entry))
  951. } else if (typeof data === 'object') {
  952. for (key in data) {
  953. value = data[key]
  954. data[key] = convertDataTypes(value)
  955. }
  956. }
  957. return data
  958. }
  959. const parser = new xml2js.Parser({
  960. explicitRoot: true,
  961. explicitArray: false,
  962. emptyTag: '',
  963. })
  964. parser.parseString(xml, function (error, data) {
  965. if (error) {
  966. return callback(error)
  967. }
  968. const result = convertDataTypes(data)
  969. callback(null, result)
  970. })
  971. },
  972. _buildXml(rootName, data) {
  973. const options = {
  974. headless: true,
  975. renderOpts: {
  976. pretty: true,
  977. indent: '\t',
  978. },
  979. rootName,
  980. }
  981. const builder = new xml2js.Builder(options)
  982. return builder.buildObject(data)
  983. },
  984. }
  985. RecurlyWrapper.promises = {
  986. getSubscription: promisify(RecurlyWrapper.getSubscription),
  987. updateAccountEmailAddress: promisify(updateAccountEmailAddress),
  988. }
  989. module.exports = RecurlyWrapper
  990. function getCustomFieldsFromSubscriptionDetails(subscriptionDetails) {
  991. if (!subscriptionDetails.ITMCampaign) {
  992. return null
  993. }
  994. const customFields = [
  995. {
  996. name: 'itm_campaign',
  997. value: subscriptionDetails.ITMCampaign,
  998. },
  999. ]
  1000. if (subscriptionDetails.ITMContent) {
  1001. customFields.push({
  1002. name: 'itm_content',
  1003. value: subscriptionDetails.ITMContent,
  1004. })
  1005. }
  1006. if (subscriptionDetails.ITMReferrer) {
  1007. customFields.push({
  1008. name: 'itm_referrer',
  1009. value: subscriptionDetails.ITMReferrer,
  1010. })
  1011. }
  1012. return { custom_field: customFields }
  1013. }
  1014. function getAddressFromSubscriptionDetails(
  1015. subscriptionDetails,
  1016. includeCompanyInfo
  1017. ) {
  1018. const { address } = subscriptionDetails
  1019. if (!address || !address.country) {
  1020. throw new Errors.InvalidError({
  1021. message: 'Invalid country',
  1022. info: {
  1023. public: {
  1024. message: 'Invalid country',
  1025. },
  1026. },
  1027. })
  1028. }
  1029. const addressObject = {
  1030. address1: address.address1,
  1031. address2: address.address2 || '',
  1032. city: address.city || '',
  1033. state: address.state || '',
  1034. zip: address.zip || '',
  1035. country: address.country,
  1036. }
  1037. if (
  1038. includeCompanyInfo &&
  1039. subscriptionDetails.billing_info &&
  1040. subscriptionDetails.billing_info.company &&
  1041. subscriptionDetails.billing_info.company !== ''
  1042. ) {
  1043. addressObject.company = subscriptionDetails.billing_info.company
  1044. if (
  1045. subscriptionDetails.billing_info.vat_number &&
  1046. subscriptionDetails.billing_info.vat_number !== ''
  1047. ) {
  1048. addressObject.vat_number = subscriptionDetails.billing_info.vat_number
  1049. }
  1050. }
  1051. return addressObject
  1052. }