SubscriptionController.js 23 KB

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