SubscriptionController.js 24 KB

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