SubscriptionController.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  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. // Populates splitTestVariants with a value for the split test name and allows
  210. // Pug to read it
  211. await SplitTestHandler.promises.getAssignment(
  212. req,
  213. res,
  214. 'bootstrap-5-subscription'
  215. )
  216. const results =
  217. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  218. user,
  219. req.i18n.language,
  220. localCcyAssignment.variant === 'enabled'
  221. ? SubscriptionFormatters.formatPriceLocalized
  222. : SubscriptionFormatters.formatPriceDefault
  223. )
  224. const {
  225. personalSubscription,
  226. memberGroupSubscriptions,
  227. managedGroupSubscriptions,
  228. currentInstitutionsWithLicence,
  229. managedInstitutions,
  230. managedPublishers,
  231. } = results
  232. const hasSubscription =
  233. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  234. const userCanExtendTrial = (
  235. await Modules.promises.hooks.fire('userCanExtendTrial', user)
  236. )?.[0]
  237. const fromPlansPage = req.query.hasSubscription
  238. const plansData =
  239. SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
  240. personalSubscription?.plan
  241. )
  242. AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
  243. const groupPlansDataForDash = formatGroupPlansDataForDash()
  244. // display the Group Settings button only to admins of group subscriptions with either/or the Managed Users or Group SSO feature available
  245. let groupSettingsEnabledFor
  246. try {
  247. const managedGroups = await async.filter(
  248. managedGroupSubscriptions || [],
  249. async subscription => {
  250. const managedUsersResults = await Modules.promises.hooks.fire(
  251. 'hasManagedUsersFeature',
  252. subscription
  253. )
  254. const groupSSOResults = await Modules.promises.hooks.fire(
  255. 'hasGroupSSOFeature',
  256. subscription
  257. )
  258. const isGroupAdmin =
  259. (subscription.admin_id._id || subscription.admin_id).toString() ===
  260. user._id.toString()
  261. return (
  262. (managedUsersResults?.[0] === true ||
  263. groupSSOResults?.[0] === true) &&
  264. isGroupAdmin
  265. )
  266. }
  267. )
  268. groupSettingsEnabledFor = managedGroups.map(subscription =>
  269. subscription._id.toString()
  270. )
  271. } catch (error) {
  272. logger.error(
  273. { err: error },
  274. 'Failed to list groups with group settings enabled'
  275. )
  276. }
  277. const data = {
  278. title: 'your_subscription',
  279. plans: plansData?.plans,
  280. planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
  281. user,
  282. hasSubscription,
  283. fromPlansPage,
  284. personalSubscription,
  285. userCanExtendTrial,
  286. memberGroupSubscriptions,
  287. managedGroupSubscriptions,
  288. managedInstitutions,
  289. managedPublishers,
  290. currentInstitutionsWithLicence,
  291. groupPlans: groupPlansDataForDash,
  292. groupSettingsEnabledFor,
  293. isManagedAccount: !!req.managedBy,
  294. userRestrictions: Array.from(req.userRestrictions || []),
  295. }
  296. res.render('subscriptions/dashboard-react', data)
  297. }
  298. async function interstitialPaymentPage(req, res) {
  299. const websiteRedesignPlansAssignment =
  300. await SplitTestHandler.promises.getAssignment(
  301. req,
  302. res,
  303. 'website-redesign-plans'
  304. )
  305. let template = 'subscriptions/interstitial-payment'
  306. if (websiteRedesignPlansAssignment.variant === 'new-design') {
  307. return await Modules.promises.hooks.fire(
  308. 'interstitialPaymentPageNewDesign',
  309. req,
  310. res
  311. )
  312. } else if (websiteRedesignPlansAssignment.variant === 'light-design') {
  313. template = 'subscriptions/interstitial-payment-light-design'
  314. }
  315. const user = SessionManager.getSessionUser(req.session)
  316. const { recommendedCurrency, countryCode, geoPricingLATAMTestVariant } =
  317. await _getRecommendedCurrency(req, res)
  318. const latamCountryBannerDetails = await getLatamCountryBannerDetails(req, res)
  319. const hasSubscription =
  320. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  321. const showSkipLink = req.query?.skipLink === 'true'
  322. if (hasSubscription) {
  323. res.redirect('/user/subscription?hasSubscription=true')
  324. } else {
  325. const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } =
  326. _plansBanners({
  327. geoPricingLATAMTestVariant,
  328. countryCode,
  329. })
  330. const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
  331. req,
  332. res,
  333. 'local-ccy-format-v2'
  334. )
  335. res.render(template, {
  336. title: 'subscribe',
  337. itm_content: req.query?.itm_content,
  338. itm_campaign: req.query?.itm_campaign,
  339. itm_referrer: req.query?.itm_referrer,
  340. recommendedCurrency,
  341. interstitialPaymentConfig,
  342. showSkipLink,
  343. formatCurrency:
  344. localCcyAssignment.variant === 'enabled'
  345. ? formatCurrencyLocalized
  346. : SubscriptionHelper.formatCurrencyDefault,
  347. showCurrencyAndPaymentMethods: localCcyAssignment.variant === 'enabled',
  348. showInrGeoBanner,
  349. showBrlGeoBanner,
  350. showLATAMBanner,
  351. latamCountryBannerDetails,
  352. skipLinkTarget: req.session?.postCheckoutRedirect || '/project',
  353. websiteRedesignPlansVariant: websiteRedesignPlansAssignment.variant,
  354. })
  355. }
  356. }
  357. /**
  358. * @param {import('express').Request} req
  359. * @param {import('express').Response} res
  360. * @returns {Promise<void>}
  361. */
  362. async function successfulSubscription(req, res) {
  363. const user = SessionManager.getSessionUser(req.session)
  364. const localCcyAssignment = await SplitTestHandler.promises.getAssignment(
  365. req,
  366. res,
  367. 'local-ccy-format-v2'
  368. )
  369. const { personalSubscription } =
  370. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  371. user,
  372. req.i18n.language,
  373. localCcyAssignment.variant === 'enabled'
  374. ? SubscriptionFormatters.formatPriceLocalized
  375. : SubscriptionFormatters.formatPriceDefault
  376. )
  377. const postCheckoutRedirect = req.session?.postCheckoutRedirect
  378. if (!personalSubscription) {
  379. res.redirect('/user/subscription/plans')
  380. } else {
  381. await SplitTestHandler.promises.getAssignment(
  382. req,
  383. res,
  384. 'bootstrap-5-subscription'
  385. )
  386. res.render('subscriptions/successful-subscription-react', {
  387. title: 'thank_you',
  388. personalSubscription,
  389. postCheckoutRedirect,
  390. })
  391. }
  392. }
  393. function cancelSubscription(req, res, next) {
  394. const user = SessionManager.getSessionUser(req.session)
  395. logger.debug({ userId: user._id }, 'canceling subscription')
  396. SubscriptionHandler.cancelSubscription(user, function (err) {
  397. if (err) {
  398. OError.tag(err, 'something went wrong canceling subscription', {
  399. user_id: user._id,
  400. })
  401. return next(err)
  402. }
  403. // Note: this redirect isn't used in the main flow as the redirection is
  404. // handled by Angular
  405. res.redirect('/user/subscription/canceled')
  406. })
  407. }
  408. /**
  409. * @param {import('express').Request} req
  410. * @param {import('express').Response} res
  411. * @param {import('express').NextFunction} next
  412. * @returns {Promise<void>}
  413. */
  414. function canceledSubscription(req, res, next) {
  415. return res.render('subscriptions/canceled-subscription-react', {
  416. title: 'subscription_canceled',
  417. })
  418. }
  419. function cancelV1Subscription(req, res, next) {
  420. const userId = SessionManager.getLoggedInUserId(req.session)
  421. logger.debug({ userId }, 'canceling v1 subscription')
  422. V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
  423. if (err) {
  424. OError.tag(err, 'something went wrong canceling v1 subscription', {
  425. userId,
  426. })
  427. return next(err)
  428. }
  429. res.redirect('/user/subscription')
  430. })
  431. }
  432. function updateSubscription(req, res, next) {
  433. const origin = req && req.query ? req.query.origin : null
  434. const user = SessionManager.getSessionUser(req.session)
  435. const planCode = req.body.plan_code
  436. if (planCode == null) {
  437. const err = new Error('plan_code is not defined')
  438. logger.warn(
  439. { userId: user._id, err, planCode, origin, body: req.body },
  440. '[Subscription] error in updateSubscription form'
  441. )
  442. return next(err)
  443. }
  444. logger.debug({ planCode, userId: user._id }, 'updating subscription')
  445. SubscriptionHandler.updateSubscription(user, planCode, null, function (err) {
  446. if (err) {
  447. OError.tag(err, 'something went wrong updating subscription', {
  448. user_id: user._id,
  449. })
  450. return next(err)
  451. }
  452. res.redirect('/user/subscription')
  453. })
  454. }
  455. function cancelPendingSubscriptionChange(req, res, next) {
  456. const user = SessionManager.getSessionUser(req.session)
  457. logger.debug({ userId: user._id }, 'canceling pending subscription change')
  458. SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
  459. if (err) {
  460. OError.tag(
  461. err,
  462. 'something went wrong canceling pending subscription change',
  463. {
  464. user_id: user._id,
  465. }
  466. )
  467. return next(err)
  468. }
  469. res.redirect('/user/subscription')
  470. })
  471. }
  472. function updateAccountEmailAddress(req, res, next) {
  473. const user = SessionManager.getSessionUser(req.session)
  474. RecurlyWrapper.updateAccountEmailAddress(
  475. user._id,
  476. user.email,
  477. function (error) {
  478. if (error) {
  479. return next(error)
  480. }
  481. res.sendStatus(200)
  482. }
  483. )
  484. }
  485. function reactivateSubscription(req, res, next) {
  486. const user = SessionManager.getSessionUser(req.session)
  487. logger.debug({ userId: user._id }, 'reactivating subscription')
  488. try {
  489. if (req.isManagedGroupAdmin) {
  490. // allow admins to reactivate subscriptions
  491. } else {
  492. // otherwise require the user to have the reactivate-subscription permission
  493. req.assertPermission('reactivate-subscription')
  494. }
  495. } catch (error) {
  496. return next(error)
  497. }
  498. SubscriptionHandler.reactivateSubscription(user, function (err) {
  499. if (err) {
  500. OError.tag(err, 'something went wrong reactivating subscription', {
  501. user_id: user._id,
  502. })
  503. return next(err)
  504. }
  505. res.redirect('/user/subscription')
  506. })
  507. }
  508. function recurlyCallback(req, res, next) {
  509. logger.debug({ data: req.body }, 'received recurly callback')
  510. const event = Object.keys(req.body)[0]
  511. const eventData = req.body[event]
  512. RecurlyEventHandler.sendRecurlyAnalyticsEvent(event, eventData).catch(error =>
  513. logger.error(
  514. { err: error },
  515. 'Failed to process analytics event on Recurly webhook'
  516. )
  517. )
  518. if (
  519. [
  520. 'new_subscription_notification',
  521. 'updated_subscription_notification',
  522. 'expired_subscription_notification',
  523. ].includes(event)
  524. ) {
  525. const recurlySubscription = eventData.subscription
  526. SubscriptionHandler.syncSubscription(
  527. recurlySubscription,
  528. { ip: req.ip },
  529. function (err) {
  530. if (err) {
  531. return next(err)
  532. }
  533. res.sendStatus(200)
  534. }
  535. )
  536. } else if (event === 'billing_info_updated_notification') {
  537. const recurlyAccountCode = eventData.account.account_code
  538. SubscriptionHandler.attemptPaypalInvoiceCollection(
  539. recurlyAccountCode,
  540. function (err) {
  541. if (err) {
  542. return next(err)
  543. }
  544. res.sendStatus(200)
  545. }
  546. )
  547. } else {
  548. res.sendStatus(200)
  549. }
  550. }
  551. async function extendTrial(req, res) {
  552. const user = SessionManager.getSessionUser(req.session)
  553. const { subscription } =
  554. await LimitationsManager.promises.userHasV2Subscription(user)
  555. const allowed = (
  556. await Modules.promises.hooks.fire('userCanExtendTrial', user)
  557. )?.[0]
  558. if (!allowed) {
  559. logger.warn({ userId: user._id }, 'user can not extend trial')
  560. return res.sendStatus(403)
  561. }
  562. try {
  563. await SubscriptionHandler.promises.extendTrial(subscription, 14)
  564. AnalyticsManager.recordEventForSession(
  565. req.session,
  566. 'subscription-trial-extended'
  567. )
  568. } catch (error) {
  569. return res.sendStatus(500)
  570. }
  571. res.sendStatus(200)
  572. }
  573. function recurlyNotificationParser(req, res, next) {
  574. let xml = ''
  575. req.on('data', chunk => (xml += chunk))
  576. req.on('end', () =>
  577. RecurlyWrapper._parseXml(xml, function (error, body) {
  578. if (error) {
  579. return next(error)
  580. }
  581. req.body = body
  582. next()
  583. })
  584. )
  585. }
  586. async function refreshUserFeatures(req, res) {
  587. const { user_id: userId } = req.params
  588. await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test')
  589. res.sendStatus(200)
  590. }
  591. async function redirectToHostedPage(req, res) {
  592. const userId = SessionManager.getLoggedInUserId(req.session)
  593. const { pageType } = req.params
  594. const url =
  595. await SubscriptionViewModelBuilder.promises.getRedirectToHostedPage(
  596. userId,
  597. pageType
  598. )
  599. logger.warn({ userId, pageType }, 'redirecting to recurly hosted page')
  600. res.redirect(url)
  601. }
  602. async function _getRecommendedCurrency(req, res) {
  603. const userId = SessionManager.getLoggedInUserId(req.session)
  604. let ip = req.ip
  605. if (
  606. req.query?.ip &&
  607. (await AuthorizationManager.promises.isUserSiteAdmin(userId))
  608. ) {
  609. ip = req.query.ip
  610. }
  611. const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
  612. const countryCode = currencyLookup.countryCode
  613. let recommendedCurrency = currencyLookup.currencyCode
  614. const assignmentLATAM = await SplitTestHandler.promises.getAssignment(
  615. req,
  616. res,
  617. 'geo-pricing-latam-v2'
  618. )
  619. if (
  620. ['MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
  621. assignmentLATAM?.variant === 'default'
  622. ) {
  623. recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
  624. }
  625. let currency = null
  626. const queryCurrency = req.query.currency?.toUpperCase()
  627. if (queryCurrency && GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  628. currency = queryCurrency
  629. } else if (recommendedCurrency) {
  630. currency = recommendedCurrency
  631. }
  632. return {
  633. currency,
  634. recommendedCurrency,
  635. countryCode,
  636. geoPricingLATAMTestVariant: assignmentLATAM?.variant,
  637. }
  638. }
  639. async function getLatamCountryBannerDetails(req, res) {
  640. const userId = SessionManager.getLoggedInUserId(req.session)
  641. let ip = req.ip
  642. if (
  643. req.query?.ip &&
  644. (await AuthorizationManager.promises.isUserSiteAdmin(userId))
  645. ) {
  646. ip = req.query.ip
  647. }
  648. const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
  649. const countryCode = currencyLookup.countryCode
  650. const latamCountryBannerDetails = {}
  651. switch (countryCode) {
  652. case `MX`:
  653. latamCountryBannerDetails.latamCountryFlag = '🇲🇽'
  654. latamCountryBannerDetails.country = 'Mexico'
  655. latamCountryBannerDetails.discount = '25%'
  656. latamCountryBannerDetails.currency = 'Mexican Pesos'
  657. break
  658. case `CO`:
  659. latamCountryBannerDetails.latamCountryFlag = '🇨🇴'
  660. latamCountryBannerDetails.country = 'Colombia'
  661. latamCountryBannerDetails.discount = '60%'
  662. latamCountryBannerDetails.currency = 'Colombian Pesos'
  663. break
  664. case `CL`:
  665. latamCountryBannerDetails.latamCountryFlag = '🇨🇱'
  666. latamCountryBannerDetails.country = 'Chile'
  667. latamCountryBannerDetails.discount = '30%'
  668. latamCountryBannerDetails.currency = 'Chilean Pesos'
  669. break
  670. case `PE`:
  671. latamCountryBannerDetails.latamCountryFlag = '🇵🇪'
  672. latamCountryBannerDetails.country = 'Peru'
  673. latamCountryBannerDetails.currency = 'Peruvian Soles'
  674. latamCountryBannerDetails.discount = '40%'
  675. break
  676. }
  677. return latamCountryBannerDetails
  678. }
  679. module.exports = {
  680. plansPage: expressify(plansPage),
  681. plansPageLightDesign: expressify(plansPageLightDesign),
  682. userSubscriptionPage: expressify(userSubscriptionPage),
  683. interstitialPaymentPage: expressify(interstitialPaymentPage),
  684. successfulSubscription: expressify(successfulSubscription),
  685. cancelSubscription,
  686. canceledSubscription,
  687. cancelV1Subscription,
  688. updateSubscription,
  689. cancelPendingSubscriptionChange,
  690. updateAccountEmailAddress,
  691. reactivateSubscription,
  692. recurlyCallback,
  693. extendTrial: expressify(extendTrial),
  694. recurlyNotificationParser,
  695. refreshUserFeatures: expressify(refreshUserFeatures),
  696. redirectToHostedPage: expressify(redirectToHostedPage),
  697. plansBanners: _plansBanners,
  698. promises: {
  699. getRecommendedCurrency: _getRecommendedCurrency,
  700. getLatamCountryBannerDetails,
  701. },
  702. }