SubscriptionController.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. const SessionManager = require('../Authentication/SessionManager')
  2. const SubscriptionHandler = require('./SubscriptionHandler')
  3. const PlansLocator = require('./PlansLocator')
  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 planFeatures = require('./planFeatures')
  12. const noPersonalPlansConfig = require('./st-personal-off-variant/plansConfig')
  13. const hasPersonalPlansConfig = require('./st-personal-off-default/plansConfig')
  14. const noPersonalInterstitialPaymentConfig = require('./st-personal-off-variant/interstitialPaymentConfig')
  15. const hasPersonalInterstitialPaymentConfig = require('./st-personal-off-default/interstitialPaymentConfig')
  16. const GroupPlansData = require('./GroupPlansData')
  17. const V1SubscriptionManager = require('./V1SubscriptionManager')
  18. const Errors = require('../Errors/Errors')
  19. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  20. const SubscriptionErrors = require('./Errors')
  21. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  22. const RecurlyEventHandler = require('./RecurlyEventHandler')
  23. const { expressify } = require('../../util/promises')
  24. const OError = require('@overleaf/o-error')
  25. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  26. const SubscriptionHelper = require('./SubscriptionHelper')
  27. const groupPlanModalOptions = Settings.groupPlanModalOptions
  28. const validGroupPlanModalOptions = {
  29. plan_code: groupPlanModalOptions.plan_codes.map(item => item.code),
  30. currency: groupPlanModalOptions.currencies.map(item => item.code),
  31. size: groupPlanModalOptions.sizes,
  32. usage: groupPlanModalOptions.usages.map(item => item.code),
  33. }
  34. function getPlansSplitOptions(assignment) {
  35. if (assignment?.variant === 'personal-off') {
  36. return {
  37. directory: 'st-personal-off-variant',
  38. plansConfig: noPersonalPlansConfig,
  39. interstitialPaymentConfig: noPersonalInterstitialPaymentConfig,
  40. }
  41. }
  42. return {
  43. directory: 'st-personal-off-default',
  44. plansConfig: hasPersonalPlansConfig,
  45. interstitialPaymentConfig: hasPersonalInterstitialPaymentConfig,
  46. }
  47. }
  48. async function plansPage(req, res) {
  49. const plans = SubscriptionViewModelBuilder.buildPlansList()
  50. let currency = null
  51. const queryCurrency = req.query.currency?.toUpperCase()
  52. if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  53. currency = queryCurrency
  54. }
  55. const { recommendedCurrency, countryCode, geoPricingTestVariant } =
  56. await _getRecommendedCurrency(req, res)
  57. if (recommendedCurrency && currency == null) {
  58. currency = recommendedCurrency
  59. }
  60. function getDefault(param, category, defaultValue) {
  61. const v = req.query && req.query[param]
  62. if (v && validGroupPlanModalOptions[category].includes(v)) {
  63. return v
  64. }
  65. return defaultValue
  66. }
  67. const currentView = 'annual'
  68. let defaultGroupPlanModalCurrency = 'USD'
  69. if (validGroupPlanModalOptions.currency.includes(currency)) {
  70. defaultGroupPlanModalCurrency = currency
  71. }
  72. const groupPlanModalDefaults = {
  73. plan_code: getDefault('plan', 'plan_code', 'collaborator'),
  74. size: getDefault('number', 'size', '2'),
  75. currency: getDefault('currency', 'currency', defaultGroupPlanModalCurrency),
  76. usage: getDefault('usage', 'usage', 'enterprise'),
  77. }
  78. let removePersonalPlanAssingment = { variant: 'default' }
  79. try {
  80. removePersonalPlanAssingment =
  81. await SplitTestHandler.promises.getAssignment(
  82. req,
  83. res,
  84. 'remove-personal-plan'
  85. )
  86. } catch (error) {
  87. logger.error(
  88. { err: error },
  89. 'Failed to get assignment for remove-personal-plan test'
  90. )
  91. }
  92. const { plansConfig, directory } = getPlansSplitOptions(
  93. removePersonalPlanAssingment
  94. )
  95. AnalyticsManager.recordEventForSession(req.session, 'plans-page-view', {
  96. currency: recommendedCurrency,
  97. 'remove-personal-plan-page': removePersonalPlanAssingment?.variant,
  98. countryCode,
  99. 'geo-pricing-inr-group': geoPricingTestVariant,
  100. 'geo-pricing-inr-page': currency === 'INR' ? 'inr' : 'default',
  101. })
  102. res.render(`subscriptions/plans-marketing/${directory}/plans-marketing-v2`, {
  103. title: 'plans_and_pricing',
  104. currentView,
  105. plans,
  106. itm_content: req.query?.itm_content,
  107. itm_referrer: req.query?.itm_referrer,
  108. itm_campaign: 'plans',
  109. recommendedCurrency: currency,
  110. planFeatures,
  111. plansConfig,
  112. groupPlans: GroupPlansData,
  113. groupPlanModalOptions,
  114. groupPlanModalDefaults,
  115. initialLocalizedGroupPrice:
  116. SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
  117. })
  118. }
  119. /**
  120. * @param {import("express").Request} req
  121. * @param {import("express").Response} res
  122. * @returns {Promise<void>}
  123. */
  124. async function paymentPage(req, res) {
  125. const user = SessionManager.getSessionUser(req.session)
  126. const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
  127. if (!plan) {
  128. return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
  129. }
  130. const hasSubscription =
  131. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  132. if (hasSubscription) {
  133. res.redirect('/user/subscription?hasSubscription=true')
  134. } else {
  135. // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
  136. // Recurly as well at this point (we don't do this most places for speed).
  137. const valid =
  138. await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
  139. user._id
  140. )
  141. if (!valid) {
  142. res.redirect('/user/subscription?hasSubscription=true')
  143. } else {
  144. let currency = null
  145. if (req.query.currency) {
  146. const queryCurrency = req.query.currency.toUpperCase()
  147. if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  148. currency = queryCurrency
  149. }
  150. }
  151. const { recommendedCurrency, countryCode } =
  152. await _getRecommendedCurrency(req, res)
  153. if (recommendedCurrency && currency == null) {
  154. currency = recommendedCurrency
  155. }
  156. await SplitTestHandler.promises.getAssignment(
  157. req,
  158. res,
  159. 'student-check-modal'
  160. )
  161. res.render('subscriptions/new-react', {
  162. title: 'subscribe',
  163. currency,
  164. countryCode,
  165. plan,
  166. planCode: req.query.planCode,
  167. couponCode: req.query.cc,
  168. showCouponField: !!req.query.scf,
  169. itm_campaign: req.query.itm_campaign,
  170. itm_content: req.query.itm_content,
  171. itm_referrer: req.query.itm_referrer,
  172. })
  173. }
  174. }
  175. }
  176. function formatGroupPlansDataForDash() {
  177. return {
  178. plans: [...groupPlanModalOptions.plan_codes],
  179. sizes: [...groupPlanModalOptions.sizes],
  180. usages: [...groupPlanModalOptions.usages],
  181. priceByUsageTypeAndSize: JSON.parse(JSON.stringify(GroupPlansData)),
  182. }
  183. }
  184. /**
  185. * @param {import("express").Request} req
  186. * @param {import("express").Response} res
  187. * @returns {Promise<void>}
  188. */
  189. async function userSubscriptionPage(req, res) {
  190. const user = SessionManager.getSessionUser(req.session)
  191. const results =
  192. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  193. user
  194. )
  195. const {
  196. personalSubscription,
  197. memberGroupSubscriptions,
  198. managedGroupSubscriptions,
  199. currentInstitutionsWithLicence,
  200. managedInstitutions,
  201. managedPublishers,
  202. v1SubscriptionStatus,
  203. } = results
  204. const hasSubscription =
  205. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  206. const fromPlansPage = req.query.hasSubscription
  207. const plansData =
  208. SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
  209. personalSubscription?.plan
  210. )
  211. AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
  212. const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment(
  213. req,
  214. res,
  215. 'subscription-cancel-button'
  216. )
  217. const cancelButtonNewCopy = cancelButtonAssignment?.variant === 'new-copy'
  218. const groupPlansDataForDash = formatGroupPlansDataForDash()
  219. const data = {
  220. title: 'your_subscription',
  221. plans: plansData?.plans,
  222. planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
  223. user,
  224. hasSubscription,
  225. fromPlansPage,
  226. personalSubscription,
  227. memberGroupSubscriptions,
  228. managedGroupSubscriptions,
  229. managedInstitutions,
  230. managedPublishers,
  231. v1SubscriptionStatus,
  232. currentInstitutionsWithLicence,
  233. cancelButtonNewCopy,
  234. groupPlans: groupPlansDataForDash,
  235. }
  236. res.render('subscriptions/dashboard-react', data)
  237. }
  238. async function interstitialPaymentPage(req, res) {
  239. const user = SessionManager.getSessionUser(req.session)
  240. const { recommendedCurrency, countryCode, geoPricingTestVariant } =
  241. await _getRecommendedCurrency(req, res)
  242. const hasSubscription =
  243. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  244. const showSkipLink = req.query?.skipLink === 'true'
  245. let removePersonalPlanAssingment = { variant: 'default' }
  246. try {
  247. removePersonalPlanAssingment =
  248. await SplitTestHandler.promises.getAssignment(
  249. req,
  250. res,
  251. 'remove-personal-plan'
  252. )
  253. } catch (error) {
  254. logger.error(
  255. { err: error },
  256. 'Failed to get assignment for remove-personal-plan test'
  257. )
  258. }
  259. const { interstitialPaymentConfig, directory } = getPlansSplitOptions(
  260. removePersonalPlanAssingment
  261. )
  262. if (hasSubscription) {
  263. res.redirect('/user/subscription?hasSubscription=true')
  264. } else {
  265. AnalyticsManager.recordEventForSession(
  266. req.session,
  267. 'paywall-plans-page-view',
  268. {
  269. currency: recommendedCurrency,
  270. countryCode,
  271. 'geo-pricing-inr-group': geoPricingTestVariant,
  272. 'geo-pricing-inr-page':
  273. recommendedCurrency === 'INR' ? 'inr' : 'default',
  274. 'remove-personal-plan-page': removePersonalPlanAssingment?.variant,
  275. }
  276. )
  277. res.render(
  278. `subscriptions/plans-marketing/${directory}/interstitial-payment`,
  279. {
  280. title: 'subscribe',
  281. itm_content: req.query?.itm_content,
  282. itm_campaign: req.query?.itm_campaign,
  283. itm_referrer: req.query?.itm_referrer,
  284. recommendedCurrency,
  285. interstitialPaymentConfig,
  286. showSkipLink,
  287. }
  288. )
  289. }
  290. }
  291. async function createSubscription(req, res) {
  292. const user = SessionManager.getSessionUser(req.session)
  293. const recurlyTokenIds = {
  294. billing: req.body.recurly_token_id,
  295. threeDSecureActionResult:
  296. req.body.recurly_three_d_secure_action_result_token_id,
  297. }
  298. const { subscriptionDetails } = req.body
  299. const hasSubscription =
  300. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  301. if (hasSubscription) {
  302. logger.warn({ userId: user._id }, 'user already has subscription')
  303. return res.sendStatus(409) // conflict
  304. }
  305. try {
  306. await SubscriptionHandler.promises.createSubscription(
  307. user,
  308. subscriptionDetails,
  309. recurlyTokenIds
  310. )
  311. res.sendStatus(201)
  312. } catch (err) {
  313. if (
  314. err instanceof SubscriptionErrors.RecurlyTransactionError ||
  315. err instanceof Errors.InvalidError
  316. ) {
  317. logger.error({ err }, 'recurly transaction error, potential 422')
  318. HttpErrorHandler.unprocessableEntity(
  319. req,
  320. res,
  321. err.message,
  322. OError.getFullInfo(err).public
  323. )
  324. } else {
  325. logger.warn(
  326. { err, userId: user._id },
  327. 'something went wrong creating subscription'
  328. )
  329. throw err
  330. }
  331. }
  332. }
  333. /**
  334. * @param {import("express").Request} req
  335. * @param {import("express").Response} res
  336. * @returns {Promise<void>}
  337. */
  338. async function successfulSubscription(req, res) {
  339. const user = SessionManager.getSessionUser(req.session)
  340. const { personalSubscription } =
  341. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  342. user
  343. )
  344. const postCheckoutRedirect = req.session?.postCheckoutRedirect
  345. if (!personalSubscription) {
  346. res.redirect('/user/subscription/plans')
  347. } else {
  348. res.render('subscriptions/successful-subscription-react', {
  349. title: 'thank_you',
  350. personalSubscription,
  351. postCheckoutRedirect,
  352. })
  353. }
  354. }
  355. function cancelSubscription(req, res, next) {
  356. const user = SessionManager.getSessionUser(req.session)
  357. logger.debug({ userId: user._id }, 'canceling subscription')
  358. SubscriptionHandler.cancelSubscription(user, function (err) {
  359. if (err) {
  360. OError.tag(err, 'something went wrong canceling subscription', {
  361. user_id: user._id,
  362. })
  363. return next(err)
  364. }
  365. // Note: this redirect isn't used in the main flow as the redirection is
  366. // handled by Angular
  367. res.redirect('/user/subscription/canceled')
  368. })
  369. }
  370. /**
  371. * @param {import("express").Request} req
  372. * @param {import("express").Response} res
  373. * @param {import("express").NextFunction} next
  374. * @returns {Promise<void>}
  375. */
  376. function canceledSubscription(req, res, next) {
  377. return res.render('subscriptions/canceled-subscription-react', {
  378. title: 'subscription_canceled',
  379. })
  380. }
  381. function cancelV1Subscription(req, res, next) {
  382. const userId = SessionManager.getLoggedInUserId(req.session)
  383. logger.debug({ userId }, 'canceling v1 subscription')
  384. V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
  385. if (err) {
  386. OError.tag(err, 'something went wrong canceling v1 subscription', {
  387. userId,
  388. })
  389. return next(err)
  390. }
  391. res.redirect('/user/subscription')
  392. })
  393. }
  394. function updateSubscription(req, res, next) {
  395. const origin = req && req.query ? req.query.origin : null
  396. const user = SessionManager.getSessionUser(req.session)
  397. const planCode = req.body.plan_code
  398. if (planCode == null) {
  399. const err = new Error('plan_code is not defined')
  400. logger.warn(
  401. { userId: user._id, err, planCode, origin, body: req.body },
  402. '[Subscription] error in updateSubscription form'
  403. )
  404. return next(err)
  405. }
  406. logger.debug({ planCode, userId: user._id }, 'updating subscription')
  407. SubscriptionHandler.updateSubscription(user, planCode, null, function (err) {
  408. if (err) {
  409. OError.tag(err, 'something went wrong updating subscription', {
  410. user_id: user._id,
  411. })
  412. return next(err)
  413. }
  414. res.redirect('/user/subscription')
  415. })
  416. }
  417. function cancelPendingSubscriptionChange(req, res, next) {
  418. const user = SessionManager.getSessionUser(req.session)
  419. logger.debug({ userId: user._id }, 'canceling pending subscription change')
  420. SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
  421. if (err) {
  422. OError.tag(
  423. err,
  424. 'something went wrong canceling pending subscription change',
  425. {
  426. user_id: user._id,
  427. }
  428. )
  429. return next(err)
  430. }
  431. res.redirect('/user/subscription')
  432. })
  433. }
  434. function updateAccountEmailAddress(req, res, next) {
  435. const user = SessionManager.getSessionUser(req.session)
  436. RecurlyWrapper.updateAccountEmailAddress(
  437. user._id,
  438. user.email,
  439. function (error) {
  440. if (error) {
  441. return next(error)
  442. }
  443. res.sendStatus(200)
  444. }
  445. )
  446. }
  447. function reactivateSubscription(req, res, next) {
  448. const user = SessionManager.getSessionUser(req.session)
  449. logger.debug({ userId: user._id }, 'reactivating subscription')
  450. SubscriptionHandler.reactivateSubscription(user, function (err) {
  451. if (err) {
  452. OError.tag(err, 'something went wrong reactivating subscription', {
  453. user_id: user._id,
  454. })
  455. return next(err)
  456. }
  457. res.redirect('/user/subscription')
  458. })
  459. }
  460. function recurlyCallback(req, res, next) {
  461. logger.debug({ data: req.body }, 'received recurly callback')
  462. const event = Object.keys(req.body)[0]
  463. const eventData = req.body[event]
  464. RecurlyEventHandler.sendRecurlyAnalyticsEvent(event, eventData).catch(error =>
  465. logger.error(
  466. { err: error },
  467. 'Failed to process analytics event on Recurly webhook'
  468. )
  469. )
  470. if (
  471. [
  472. 'new_subscription_notification',
  473. 'updated_subscription_notification',
  474. 'expired_subscription_notification',
  475. ].includes(event)
  476. ) {
  477. const recurlySubscription = eventData.subscription
  478. SubscriptionHandler.syncSubscription(
  479. recurlySubscription,
  480. { ip: req.ip },
  481. function (err) {
  482. if (err) {
  483. return next(err)
  484. }
  485. res.sendStatus(200)
  486. }
  487. )
  488. } else if (event === 'billing_info_updated_notification') {
  489. const recurlyAccountCode = eventData.account.account_code
  490. SubscriptionHandler.attemptPaypalInvoiceCollection(
  491. recurlyAccountCode,
  492. function (err) {
  493. if (err) {
  494. return next(err)
  495. }
  496. res.sendStatus(200)
  497. }
  498. )
  499. } else {
  500. res.sendStatus(200)
  501. }
  502. }
  503. function renderUpgradeToAnnualPlanPage(req, res, next) {
  504. const user = SessionManager.getSessionUser(req.session)
  505. LimitationsManager.userHasV2Subscription(
  506. user,
  507. function (err, hasSubscription, subscription) {
  508. let planName
  509. if (err) {
  510. return next(err)
  511. }
  512. const planCode = subscription
  513. ? subscription.planCode.toLowerCase()
  514. : undefined
  515. if ((planCode ? planCode.indexOf('annual') : undefined) !== -1) {
  516. planName = 'annual'
  517. } else if ((planCode ? planCode.indexOf('student') : undefined) !== -1) {
  518. planName = 'student'
  519. } else if (
  520. (planCode ? planCode.indexOf('collaborator') : undefined) !== -1
  521. ) {
  522. planName = 'collaborator'
  523. }
  524. if (hasSubscription) {
  525. res.render('subscriptions/upgradeToAnnual', {
  526. title: 'Upgrade to annual',
  527. planName,
  528. })
  529. } else {
  530. res.redirect('/user/subscription/plans')
  531. }
  532. }
  533. )
  534. }
  535. function processUpgradeToAnnualPlan(req, res, next) {
  536. const user = SessionManager.getSessionUser(req.session)
  537. const { planName } = req.body
  538. const couponCode = Settings.coupon_codes.upgradeToAnnualPromo[planName]
  539. const annualPlanName = `${planName}-annual`
  540. logger.debug(
  541. { userId: user._id, planName: annualPlanName },
  542. 'user is upgrading to annual billing with discount'
  543. )
  544. return SubscriptionHandler.updateSubscription(
  545. user,
  546. annualPlanName,
  547. couponCode,
  548. function (err) {
  549. if (err) {
  550. OError.tag(err, 'error updating subscription', {
  551. user_id: user._id,
  552. })
  553. return next(err)
  554. }
  555. res.sendStatus(200)
  556. }
  557. )
  558. }
  559. async function extendTrial(req, res) {
  560. const user = SessionManager.getSessionUser(req.session)
  561. const { subscription } =
  562. await LimitationsManager.promises.userHasV2Subscription(user)
  563. try {
  564. await SubscriptionHandler.promises.extendTrial(subscription, 14)
  565. AnalyticsManager.recordEventForSession(
  566. req.session,
  567. 'subscription-trial-extended'
  568. )
  569. } catch (error) {
  570. return res.sendStatus(500)
  571. }
  572. res.sendStatus(200)
  573. }
  574. function recurlyNotificationParser(req, res, next) {
  575. let xml = ''
  576. req.on('data', chunk => (xml += chunk))
  577. req.on('end', () =>
  578. RecurlyWrapper._parseXml(xml, function (error, body) {
  579. if (error) {
  580. return next(error)
  581. }
  582. req.body = body
  583. next()
  584. })
  585. )
  586. }
  587. async function refreshUserFeatures(req, res) {
  588. const { user_id: userId } = req.params
  589. await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test')
  590. res.sendStatus(200)
  591. }
  592. async function redirectToHostedPage(req, res) {
  593. const userId = SessionManager.getLoggedInUserId(req.session)
  594. const { pageType } = req.params
  595. const url =
  596. await SubscriptionViewModelBuilder.promises.getRedirectToHostedPage(
  597. userId,
  598. pageType
  599. )
  600. logger.warn({ userId, pageType }, 'redirecting to recurly hosted page')
  601. res.redirect(url)
  602. }
  603. async function _getRecommendedCurrency(req, res) {
  604. const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(
  605. req.query?.ip || req.ip
  606. )
  607. const countryCode = currencyLookup.countryCode
  608. let recommendedCurrency = currencyLookup.currencyCode
  609. let assignment
  610. // for #12703
  611. try {
  612. assignment = await SplitTestHandler.promises.getAssignment(
  613. req,
  614. res,
  615. 'geo-pricing-inr'
  616. )
  617. } catch (error) {
  618. logger.error(
  619. { err: error },
  620. 'Failed to get assignment for geo-pricing-inr test'
  621. )
  622. }
  623. // if the user has been detected as located in India (thus recommended INR as currency)
  624. // but is not part of the geo pricing test, we fall back to the default currency instead
  625. if (recommendedCurrency === 'INR' && assignment?.variant !== 'inr') {
  626. recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
  627. }
  628. return {
  629. recommendedCurrency,
  630. countryCode,
  631. geoPricingTestVariant: assignment?.variant,
  632. }
  633. }
  634. module.exports = {
  635. plansPage: expressify(plansPage),
  636. paymentPage: expressify(paymentPage),
  637. userSubscriptionPage: expressify(userSubscriptionPage),
  638. interstitialPaymentPage: expressify(interstitialPaymentPage),
  639. createSubscription: expressify(createSubscription),
  640. successfulSubscription: expressify(successfulSubscription),
  641. cancelSubscription,
  642. canceledSubscription,
  643. cancelV1Subscription,
  644. updateSubscription,
  645. cancelPendingSubscriptionChange,
  646. updateAccountEmailAddress,
  647. reactivateSubscription,
  648. recurlyCallback,
  649. renderUpgradeToAnnualPlanPage,
  650. processUpgradeToAnnualPlan,
  651. extendTrial: expressify(extendTrial),
  652. recurlyNotificationParser,
  653. refreshUserFeatures: expressify(refreshUserFeatures),
  654. redirectToHostedPage: expressify(redirectToHostedPage),
  655. }