SubscriptionController.js 23 KB

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