SubscriptionController.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. // @ts-check
  2. const SessionManager = require('../Authentication/SessionManager')
  3. const SubscriptionHandler = require('./SubscriptionHandler')
  4. const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
  5. const LimitationsManager = require('./LimitationsManager')
  6. const RecurlyWrapper = require('./RecurlyWrapper')
  7. const Settings = require('@overleaf/settings')
  8. const logger = require('@overleaf/logger')
  9. const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
  10. const FeaturesUpdater = require('./FeaturesUpdater')
  11. const GroupPlansData = require('./GroupPlansData')
  12. const V1SubscriptionManager = require('./V1SubscriptionManager')
  13. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  14. const RecurlyEventHandler = require('./RecurlyEventHandler')
  15. const { expressify } = require('@overleaf/promise-utils')
  16. const OError = require('@overleaf/o-error')
  17. const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors')
  18. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  19. const AuthorizationManager = require('../Authorization/AuthorizationManager')
  20. const Modules = require('../../infrastructure/Modules')
  21. const async = require('async')
  22. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  23. const RecurlyClient = require('./RecurlyClient')
  24. const { AI_ADD_ON_CODE } = require('./RecurlyEntities')
  25. const PlansLocator = require('./PlansLocator')
  26. /**
  27. * @import { SubscriptionChangeDescription } from '../../../../types/subscription/subscription-change-preview'
  28. * @import { SubscriptionChangePreview } from '../../../../types/subscription/subscription-change-preview'
  29. * @import { RecurlySubscriptionChange } from './RecurlyEntities'
  30. * @import { PaymentMethod } from './types'
  31. */
  32. const groupPlanModalOptions = Settings.groupPlanModalOptions
  33. function formatGroupPlansDataForDash() {
  34. return {
  35. plans: [...groupPlanModalOptions.plan_codes],
  36. sizes: [...groupPlanModalOptions.sizes],
  37. sizesForHighDenominationCurrencies: [
  38. ...groupPlanModalOptions.sizesForHighDenominationCurrencies,
  39. ],
  40. usages: [...groupPlanModalOptions.usages],
  41. priceByUsageTypeAndSize: JSON.parse(JSON.stringify(GroupPlansData)),
  42. }
  43. }
  44. async function userSubscriptionPage(req, res) {
  45. const user = SessionManager.getSessionUser(req.session)
  46. await SplitTestHandler.promises.getAssignment(req, res, 'ai-add-on')
  47. await SplitTestHandler.promises.getAssignment(req, res, 'pause-subscription')
  48. // Populates splitTestVariants with a value for the split test name and allows
  49. // Pug to read it
  50. await SplitTestHandler.promises.getAssignment(
  51. req,
  52. res,
  53. 'bootstrap-5-subscription'
  54. )
  55. const results =
  56. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  57. user,
  58. req.i18n.language
  59. )
  60. const {
  61. personalSubscription,
  62. memberGroupSubscriptions,
  63. managedGroupSubscriptions,
  64. currentInstitutionsWithLicence,
  65. managedInstitutions,
  66. managedPublishers,
  67. } = results
  68. const { hasSubscription } =
  69. await LimitationsManager.promises.userHasSubscription(user)
  70. const userCanExtendTrial = (
  71. await Modules.promises.hooks.fire('userCanExtendTrial', user)
  72. )?.[0]
  73. const fromPlansPage = req.query.hasSubscription
  74. const plansData =
  75. SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
  76. personalSubscription?.plan
  77. )
  78. AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
  79. const groupPlansDataForDash = formatGroupPlansDataForDash()
  80. // display the Group Settings button only to admins of group subscriptions with either/or the Managed Users or Group SSO feature available
  81. let groupSettingsEnabledFor
  82. try {
  83. const managedGroups = await async.filter(
  84. managedGroupSubscriptions || [],
  85. async subscription => {
  86. const managedUsersResults = await Modules.promises.hooks.fire(
  87. 'hasManagedUsersFeature',
  88. subscription
  89. )
  90. const groupSSOResults = await Modules.promises.hooks.fire(
  91. 'hasGroupSSOFeature',
  92. subscription
  93. )
  94. const isGroupAdmin =
  95. (subscription.admin_id._id || subscription.admin_id).toString() ===
  96. user._id.toString()
  97. return (
  98. (managedUsersResults?.[0] === true ||
  99. groupSSOResults?.[0] === true) &&
  100. isGroupAdmin
  101. )
  102. }
  103. )
  104. groupSettingsEnabledFor = managedGroups.map(subscription =>
  105. subscription._id.toString()
  106. )
  107. } catch (error) {
  108. logger.error(
  109. { err: error },
  110. 'Failed to list groups with group settings enabled'
  111. )
  112. }
  113. const data = {
  114. title: 'your_subscription',
  115. plans: plansData?.plans,
  116. planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
  117. user,
  118. hasSubscription,
  119. fromPlansPage,
  120. personalSubscription,
  121. userCanExtendTrial,
  122. memberGroupSubscriptions,
  123. managedGroupSubscriptions,
  124. managedInstitutions,
  125. managedPublishers,
  126. currentInstitutionsWithLicence,
  127. groupPlans: groupPlansDataForDash,
  128. groupSettingsEnabledFor,
  129. isManagedAccount: !!req.managedBy,
  130. userRestrictions: Array.from(req.userRestrictions || []),
  131. }
  132. res.render('subscriptions/dashboard-react', data)
  133. }
  134. async function successfulSubscription(req, res) {
  135. const user = SessionManager.getSessionUser(req.session)
  136. const { personalSubscription } =
  137. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  138. user,
  139. req.i18n.language
  140. )
  141. const postCheckoutRedirect = req.session?.postCheckoutRedirect
  142. if (!personalSubscription) {
  143. res.redirect('/user/subscription/plans')
  144. } else {
  145. await SplitTestHandler.promises.getAssignment(
  146. req,
  147. res,
  148. 'bootstrap-5-subscription'
  149. )
  150. res.render('subscriptions/successful-subscription-react', {
  151. title: 'thank_you',
  152. personalSubscription,
  153. postCheckoutRedirect,
  154. user,
  155. })
  156. }
  157. }
  158. async function pauseSubscription(req, res, next) {
  159. const user = SessionManager.getSessionUser(req.session)
  160. const pauseCycles = req.params.pauseCycles
  161. if (!('pauseCycles' in req.params)) {
  162. return HttpErrorHandler.badRequest(
  163. req,
  164. res,
  165. `Pausing subscription requires a 'pauseCycles' argument with number of billing cycles to pause for`
  166. )
  167. }
  168. if (pauseCycles < 0) {
  169. return HttpErrorHandler.badRequest(
  170. req,
  171. res,
  172. `'pauseCycles' should be a number of billing cycles to pause for, or 0 to cancel a pending pause`
  173. )
  174. }
  175. logger.debug(
  176. { userId: user._id },
  177. `pausing subscription for ${pauseCycles} billing cycles`
  178. )
  179. try {
  180. await SubscriptionHandler.promises.pauseSubscription(user, pauseCycles)
  181. return res.sendStatus(200)
  182. } catch (err) {
  183. if (err instanceof Error) {
  184. OError.tag(err, 'something went wrong pausing subscription', {
  185. user_id: user._id,
  186. })
  187. }
  188. return next(err)
  189. }
  190. }
  191. async function resumeSubscription(req, res, next) {
  192. const user = SessionManager.getSessionUser(req.session)
  193. logger.debug({ userId: user._id }, `resuming subscription`)
  194. try {
  195. await SubscriptionHandler.promises.resumeSubscription(user)
  196. return res.sendStatus(200)
  197. } catch (err) {
  198. if (err instanceof Error) {
  199. OError.tag(err, 'something went wrong resuming subscription', {
  200. user_id: user._id,
  201. })
  202. }
  203. return next(err)
  204. }
  205. }
  206. function cancelSubscription(req, res, next) {
  207. const user = SessionManager.getSessionUser(req.session)
  208. logger.debug({ userId: user._id }, 'canceling subscription')
  209. SubscriptionHandler.cancelSubscription(user, function (err) {
  210. if (err) {
  211. OError.tag(err, 'something went wrong canceling subscription', {
  212. user_id: user._id,
  213. })
  214. return next(err)
  215. }
  216. // Note: this redirect isn't used in the main flow as the redirection is
  217. // handled by Angular
  218. res.redirect('/user/subscription/canceled')
  219. })
  220. }
  221. /**
  222. * @returns {Promise<void>}
  223. */
  224. async function canceledSubscription(req, res, next) {
  225. await SplitTestHandler.promises.getAssignment(
  226. req,
  227. res,
  228. 'bootstrap-5-subscription'
  229. )
  230. return res.render('subscriptions/canceled-subscription-react', {
  231. title: 'subscription_canceled',
  232. user: SessionManager.getSessionUser(req.session),
  233. })
  234. }
  235. function cancelV1Subscription(req, res, next) {
  236. const userId = SessionManager.getLoggedInUserId(req.session)
  237. logger.debug({ userId }, 'canceling v1 subscription')
  238. V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
  239. if (err) {
  240. OError.tag(err, 'something went wrong canceling v1 subscription', {
  241. userId,
  242. })
  243. return next(err)
  244. }
  245. res.redirect('/user/subscription')
  246. })
  247. }
  248. async function previewAddonPurchase(req, res) {
  249. const userId = SessionManager.getLoggedInUserId(req.session)
  250. const addOnCode = req.params.addOnCode
  251. if (addOnCode !== AI_ADD_ON_CODE) {
  252. return HttpErrorHandler.notFound(req, res, `Unknown add-on: ${addOnCode}`)
  253. }
  254. const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
  255. let subscriptionChange
  256. try {
  257. subscriptionChange =
  258. await SubscriptionHandler.promises.previewAddonPurchase(userId, addOnCode)
  259. } catch (err) {
  260. if (err instanceof DuplicateAddOnError) {
  261. return HttpErrorHandler.badRequest(
  262. req,
  263. res,
  264. `Subscription already has add-on "${addOnCode}"`
  265. )
  266. }
  267. throw err
  268. }
  269. const subscription = subscriptionChange.subscription
  270. const addOn = await RecurlyClient.promises.getAddOn(
  271. subscription.planCode,
  272. addOnCode
  273. )
  274. /** @type {SubscriptionChangePreview} */
  275. const changePreview = makeChangePreview(
  276. {
  277. type: 'add-on-purchase',
  278. addOn: {
  279. code: addOn.code,
  280. name: addOn.name,
  281. },
  282. },
  283. subscriptionChange,
  284. paymentMethod
  285. )
  286. res.render('subscriptions/preview-change', { changePreview })
  287. }
  288. async function purchaseAddon(req, res, next) {
  289. const user = SessionManager.getSessionUser(req.session)
  290. const addOnCode = req.params.addOnCode
  291. // currently we only support having a quantity of 1
  292. const quantity = 1
  293. // currently we only support one add-on, the Ai add-on
  294. if (addOnCode !== AI_ADD_ON_CODE) {
  295. return res.sendStatus(404)
  296. }
  297. logger.debug({ userId: user._id, addOnCode }, 'purchasing add-ons')
  298. try {
  299. await SubscriptionHandler.promises.purchaseAddon(
  300. user._id,
  301. addOnCode,
  302. quantity
  303. )
  304. return res.sendStatus(200)
  305. } catch (err) {
  306. if (err instanceof DuplicateAddOnError) {
  307. HttpErrorHandler.badRequest(
  308. req,
  309. res,
  310. 'Your subscription already includes this add-on',
  311. { addon: addOnCode }
  312. )
  313. } else {
  314. if (err instanceof Error) {
  315. OError.tag(err, 'something went wrong purchasing add-ons', {
  316. user_id: user._id,
  317. addOnCode,
  318. })
  319. }
  320. return next(err)
  321. }
  322. }
  323. }
  324. async function removeAddon(req, res, next) {
  325. const user = SessionManager.getSessionUser(req.session)
  326. const addOnCode = req.params.addOnCode
  327. if (addOnCode !== AI_ADD_ON_CODE) {
  328. return res.sendStatus(404)
  329. }
  330. logger.debug({ userId: user._id, addOnCode }, 'removing add-ons')
  331. try {
  332. await SubscriptionHandler.promises.removeAddon(user._id, addOnCode)
  333. res.sendStatus(200)
  334. } catch (err) {
  335. if (err instanceof AddOnNotPresentError) {
  336. HttpErrorHandler.badRequest(
  337. req,
  338. res,
  339. 'Your subscription does not contain the requested add-on',
  340. { addon: addOnCode }
  341. )
  342. } else {
  343. if (err instanceof Error) {
  344. OError.tag(err, 'something went wrong removing add-ons', {
  345. user_id: user._id,
  346. addOnCode,
  347. })
  348. }
  349. return next(err)
  350. }
  351. }
  352. }
  353. async function previewSubscription(req, res, next) {
  354. const planCode = req.query.planCode
  355. if (!planCode) {
  356. return HttpErrorHandler.notFound(req, res, 'Missing plan code')
  357. }
  358. const plan = await RecurlyClient.promises.getPlan(planCode)
  359. const userId = SessionManager.getLoggedInUserId(req.session)
  360. const subscriptionChange =
  361. await SubscriptionHandler.promises.previewSubscriptionChange(
  362. userId,
  363. planCode
  364. )
  365. const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
  366. const changePreview = makeChangePreview(
  367. {
  368. type: 'premium-subscription',
  369. plan: { code: plan.code, name: plan.name },
  370. },
  371. subscriptionChange,
  372. paymentMethod
  373. )
  374. res.render('subscriptions/preview-change', { changePreview })
  375. }
  376. function updateSubscription(req, res, next) {
  377. const origin = req && req.query ? req.query.origin : null
  378. const user = SessionManager.getSessionUser(req.session)
  379. const planCode = req.body.plan_code
  380. if (planCode == null) {
  381. const err = new Error('plan_code is not defined')
  382. logger.warn(
  383. { userId: user._id, err, planCode, origin, body: req.body },
  384. '[Subscription] error in updateSubscription form'
  385. )
  386. return next(err)
  387. }
  388. logger.debug({ planCode, userId: user._id }, 'updating subscription')
  389. SubscriptionHandler.updateSubscription(user, planCode, null, function (err) {
  390. if (err) {
  391. OError.tag(err, 'something went wrong updating subscription', {
  392. user_id: user._id,
  393. })
  394. return next(err)
  395. }
  396. res.redirect('/user/subscription')
  397. })
  398. }
  399. function cancelPendingSubscriptionChange(req, res, next) {
  400. const user = SessionManager.getSessionUser(req.session)
  401. logger.debug({ userId: user._id }, 'canceling pending subscription change')
  402. SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
  403. if (err) {
  404. OError.tag(
  405. err,
  406. 'something went wrong canceling pending subscription change',
  407. {
  408. user_id: user._id,
  409. }
  410. )
  411. return next(err)
  412. }
  413. res.redirect('/user/subscription')
  414. })
  415. }
  416. function updateAccountEmailAddress(req, res, next) {
  417. const user = SessionManager.getSessionUser(req.session)
  418. RecurlyWrapper.updateAccountEmailAddress(
  419. user._id,
  420. user.email,
  421. function (error) {
  422. if (error) {
  423. return next(error)
  424. }
  425. res.sendStatus(200)
  426. }
  427. )
  428. }
  429. function reactivateSubscription(req, res, next) {
  430. const user = SessionManager.getSessionUser(req.session)
  431. logger.debug({ userId: user._id }, 'reactivating subscription')
  432. try {
  433. if (req.isManagedGroupAdmin) {
  434. // allow admins to reactivate subscriptions
  435. } else {
  436. // otherwise require the user to have the reactivate-subscription permission
  437. req.assertPermission('reactivate-subscription')
  438. }
  439. } catch (error) {
  440. return next(error)
  441. }
  442. SubscriptionHandler.reactivateSubscription(user, function (err) {
  443. if (err) {
  444. OError.tag(err, 'something went wrong reactivating subscription', {
  445. user_id: user._id,
  446. })
  447. return next(err)
  448. }
  449. res.redirect('/user/subscription')
  450. })
  451. }
  452. function recurlyCallback(req, res, next) {
  453. logger.debug({ data: req.body }, 'received recurly callback')
  454. const event = Object.keys(req.body)[0]
  455. const eventData = req.body[event]
  456. RecurlyEventHandler.sendRecurlyAnalyticsEvent(event, eventData).catch(error =>
  457. logger.error(
  458. { err: error },
  459. 'Failed to process analytics event on Recurly webhook'
  460. )
  461. )
  462. if (
  463. [
  464. 'new_subscription_notification',
  465. 'updated_subscription_notification',
  466. 'expired_subscription_notification',
  467. 'subscription_paused_notification',
  468. 'subscription_resumed_notification',
  469. ].includes(event)
  470. ) {
  471. const recurlySubscription = eventData.subscription
  472. SubscriptionHandler.syncSubscription(
  473. recurlySubscription,
  474. { ip: req.ip },
  475. function (err) {
  476. if (err) {
  477. return next(err)
  478. }
  479. res.sendStatus(200)
  480. }
  481. )
  482. } else if (event === 'billing_info_updated_notification') {
  483. const recurlyAccountCode = eventData.account.account_code
  484. SubscriptionHandler.attemptPaypalInvoiceCollection(
  485. recurlyAccountCode,
  486. function (err) {
  487. if (err) {
  488. return next(err)
  489. }
  490. res.sendStatus(200)
  491. }
  492. )
  493. } else {
  494. res.sendStatus(200)
  495. }
  496. }
  497. async function extendTrial(req, res) {
  498. const user = SessionManager.getSessionUser(req.session)
  499. const { subscription } =
  500. await LimitationsManager.promises.userHasSubscription(user)
  501. const allowed = (
  502. await Modules.promises.hooks.fire('userCanExtendTrial', user)
  503. )?.[0]
  504. if (!allowed) {
  505. logger.warn({ userId: user._id }, 'user can not extend trial')
  506. return res.sendStatus(403)
  507. }
  508. try {
  509. await SubscriptionHandler.promises.extendTrial(subscription, 14)
  510. AnalyticsManager.recordEventForSession(
  511. req.session,
  512. 'subscription-trial-extended'
  513. )
  514. } catch (error) {
  515. return res.sendStatus(500)
  516. }
  517. res.sendStatus(200)
  518. }
  519. function recurlyNotificationParser(req, res, next) {
  520. let xml = ''
  521. req.on('data', chunk => (xml += chunk))
  522. req.on('end', () =>
  523. RecurlyWrapper._parseXml(xml, function (error, body) {
  524. if (error) {
  525. return next(error)
  526. }
  527. req.body = body
  528. next()
  529. })
  530. )
  531. }
  532. async function refreshUserFeatures(req, res) {
  533. const { user_id: userId } = req.params
  534. await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test')
  535. res.sendStatus(200)
  536. }
  537. async function redirectToHostedPage(req, res) {
  538. const userId = SessionManager.getLoggedInUserId(req.session)
  539. const { pageType } = req.params
  540. const url =
  541. await SubscriptionViewModelBuilder.promises.getRedirectToHostedPage(
  542. userId,
  543. pageType
  544. )
  545. logger.warn({ userId, pageType }, 'redirecting to recurly hosted page')
  546. res.redirect(url)
  547. }
  548. async function getRecommendedCurrency(req, res) {
  549. const userId = SessionManager.getLoggedInUserId(req.session)
  550. let ip = req.ip
  551. if (
  552. req.query?.ip &&
  553. (await AuthorizationManager.promises.isUserSiteAdmin(userId))
  554. ) {
  555. ip = req.query.ip
  556. }
  557. const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
  558. const countryCode = currencyLookup.countryCode
  559. const recommendedCurrency = currencyLookup.currencyCode
  560. let currency = null
  561. const queryCurrency = req.query.currency?.toUpperCase()
  562. if (queryCurrency && GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  563. currency = queryCurrency
  564. } else if (recommendedCurrency) {
  565. currency = recommendedCurrency
  566. }
  567. return {
  568. currency,
  569. recommendedCurrency,
  570. countryCode,
  571. }
  572. }
  573. async function getLatamCountryBannerDetails(req, res) {
  574. const userId = SessionManager.getLoggedInUserId(req.session)
  575. let ip = req.ip
  576. if (
  577. req.query?.ip &&
  578. (await AuthorizationManager.promises.isUserSiteAdmin(userId))
  579. ) {
  580. ip = req.query.ip
  581. }
  582. const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
  583. const countryCode = currencyLookup.countryCode
  584. const latamCountryBannerDetails = {}
  585. switch (countryCode) {
  586. case `MX`:
  587. latamCountryBannerDetails.latamCountryFlag = '🇲🇽'
  588. latamCountryBannerDetails.country = 'Mexico'
  589. latamCountryBannerDetails.discount = '25%'
  590. latamCountryBannerDetails.currency = 'Mexican Pesos'
  591. break
  592. case `CO`:
  593. latamCountryBannerDetails.latamCountryFlag = '🇨🇴'
  594. latamCountryBannerDetails.country = 'Colombia'
  595. latamCountryBannerDetails.discount = '60%'
  596. latamCountryBannerDetails.currency = 'Colombian Pesos'
  597. break
  598. case `CL`:
  599. latamCountryBannerDetails.latamCountryFlag = '🇨🇱'
  600. latamCountryBannerDetails.country = 'Chile'
  601. latamCountryBannerDetails.discount = '30%'
  602. latamCountryBannerDetails.currency = 'Chilean Pesos'
  603. break
  604. case `PE`:
  605. latamCountryBannerDetails.latamCountryFlag = '🇵🇪'
  606. latamCountryBannerDetails.country = 'Peru'
  607. latamCountryBannerDetails.currency = 'Peruvian Soles'
  608. latamCountryBannerDetails.discount = '40%'
  609. break
  610. }
  611. return latamCountryBannerDetails
  612. }
  613. /**
  614. * Build a subscription change preview for display purposes
  615. *
  616. * @param {SubscriptionChangeDescription} subscriptionChangeDescription A description of the change for the frontend
  617. * @param {RecurlySubscriptionChange} subscriptionChange The subscription change object coming from Recurly
  618. * @param {PaymentMethod} paymentMethod The payment method associated to the user
  619. * @return {SubscriptionChangePreview}
  620. */
  621. function makeChangePreview(
  622. subscriptionChangeDescription,
  623. subscriptionChange,
  624. paymentMethod
  625. ) {
  626. const subscription = subscriptionChange.subscription
  627. const nextPlan = PlansLocator.findLocalPlanInSettings(
  628. subscriptionChange.nextPlanCode
  629. )
  630. return {
  631. change: subscriptionChangeDescription,
  632. currency: subscription.currency,
  633. immediateCharge: { ...subscriptionChange.immediateCharge },
  634. paymentMethod: paymentMethod.toString(),
  635. nextPlan: {
  636. annual: nextPlan.annual ?? false,
  637. },
  638. nextInvoice: {
  639. date: subscription.periodEnd.toISOString(),
  640. plan: {
  641. name: subscriptionChange.nextPlanName,
  642. amount: subscriptionChange.nextPlanPrice,
  643. },
  644. addOns: subscriptionChange.nextAddOns.map(addOn => ({
  645. code: addOn.code,
  646. name: addOn.name,
  647. quantity: addOn.quantity,
  648. unitAmount: addOn.unitPrice,
  649. amount: addOn.preTaxTotal,
  650. })),
  651. subtotal: subscriptionChange.subtotal,
  652. tax: {
  653. rate: subscription.taxRate,
  654. amount: subscriptionChange.tax,
  655. },
  656. total: subscriptionChange.total,
  657. },
  658. }
  659. }
  660. module.exports = {
  661. userSubscriptionPage: expressify(userSubscriptionPage),
  662. successfulSubscription: expressify(successfulSubscription),
  663. cancelSubscription,
  664. pauseSubscription,
  665. resumeSubscription,
  666. canceledSubscription: expressify(canceledSubscription),
  667. cancelV1Subscription,
  668. previewSubscription: expressify(previewSubscription),
  669. updateSubscription,
  670. cancelPendingSubscriptionChange,
  671. updateAccountEmailAddress,
  672. reactivateSubscription,
  673. recurlyCallback,
  674. extendTrial: expressify(extendTrial),
  675. recurlyNotificationParser,
  676. refreshUserFeatures: expressify(refreshUserFeatures),
  677. redirectToHostedPage: expressify(redirectToHostedPage),
  678. previewAddonPurchase: expressify(previewAddonPurchase),
  679. purchaseAddon,
  680. removeAddon,
  681. makeChangePreview,
  682. getRecommendedCurrency,
  683. getLatamCountryBannerDetails,
  684. }