SubscriptionController.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  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 plansConfig = require('./plansConfig')
  13. const interstitialPaymentConfig = require('./interstitialPaymentConfig')
  14. const GroupPlansData = require('./GroupPlansData')
  15. const V1SubscriptionManager = require('./V1SubscriptionManager')
  16. const Errors = require('../Errors/Errors')
  17. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  18. const SubscriptionErrors = require('./Errors')
  19. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  20. const RecurlyEventHandler = require('./RecurlyEventHandler')
  21. const { expressify } = require('../../util/promises')
  22. const OError = require('@overleaf/o-error')
  23. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  24. const SubscriptionHelper = require('./SubscriptionHelper')
  25. const ManagedUsersManager = require('../../../../modules/managed-users/app/src/ManagedUsersManager')
  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. async function plansPage(req, res) {
  34. const plans = SubscriptionViewModelBuilder.buildPlansList()
  35. let currency = null
  36. const queryCurrency = req.query.currency?.toUpperCase()
  37. if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  38. currency = queryCurrency
  39. }
  40. const {
  41. recommendedCurrency,
  42. countryCode,
  43. geoPricingINRTestVariant,
  44. geoPricingLATAMTestVariant,
  45. } = await _getRecommendedCurrency(req, res)
  46. if (recommendedCurrency && currency == null) {
  47. currency = recommendedCurrency
  48. }
  49. function getDefault(param, category, defaultValue) {
  50. const v = req.query && req.query[param]
  51. if (v && validGroupPlanModalOptions[category].includes(v)) {
  52. return v
  53. }
  54. return defaultValue
  55. }
  56. const currentView = 'annual'
  57. let defaultGroupPlanModalCurrency = 'USD'
  58. if (validGroupPlanModalOptions.currency.includes(currency)) {
  59. defaultGroupPlanModalCurrency = currency
  60. }
  61. const groupPlanModalDefaults = {
  62. plan_code: getDefault('plan', 'plan_code', 'collaborator'),
  63. size: getDefault('number', 'size', '2'),
  64. currency: getDefault('currency', 'currency', defaultGroupPlanModalCurrency),
  65. usage: getDefault('usage', 'usage', 'enterprise'),
  66. }
  67. let showInrGeoBanner, inrGeoBannerSplitTestName
  68. let inrGeoBannerVariant = 'default'
  69. if (countryCode === 'IN') {
  70. inrGeoBannerSplitTestName =
  71. geoPricingINRTestVariant === 'inr'
  72. ? 'geo-banners-inr-2'
  73. : 'geo-banners-inr-1'
  74. try {
  75. const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
  76. req,
  77. res,
  78. inrGeoBannerSplitTestName
  79. )
  80. inrGeoBannerVariant = geoBannerAssignment.variant
  81. if (inrGeoBannerVariant !== 'default') {
  82. showInrGeoBanner = true
  83. }
  84. } catch (error) {
  85. logger.error(
  86. { err: error },
  87. `Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
  88. )
  89. }
  90. }
  91. const plansPageViewSegmentation = {
  92. currency: recommendedCurrency,
  93. countryCode,
  94. 'geo-pricing-inr-group': geoPricingINRTestVariant,
  95. 'geo-pricing-inr-page': currency === 'INR' ? 'inr' : 'default',
  96. 'geo-pricing-latam-group': geoPricingLATAMTestVariant,
  97. 'geo-pricing-latam-page': ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(
  98. currency
  99. )
  100. ? 'latam'
  101. : 'default',
  102. }
  103. if (inrGeoBannerSplitTestName) {
  104. plansPageViewSegmentation[inrGeoBannerSplitTestName] = inrGeoBannerVariant
  105. }
  106. AnalyticsManager.recordEventForSession(
  107. req.session,
  108. 'plans-page-view',
  109. plansPageViewSegmentation
  110. )
  111. res.render('subscriptions/plans', {
  112. title: 'plans_and_pricing',
  113. currentView,
  114. plans,
  115. itm_content: req.query?.itm_content,
  116. itm_referrer: req.query?.itm_referrer,
  117. itm_campaign: 'plans',
  118. recommendedCurrency: currency,
  119. planFeatures,
  120. plansConfig,
  121. groupPlans: GroupPlansData,
  122. groupPlanModalOptions,
  123. groupPlanModalDefaults,
  124. initialLocalizedGroupPrice:
  125. SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
  126. showInrGeoBanner,
  127. })
  128. }
  129. /**
  130. * @param {import('express').Request} req
  131. * @param {import('express').Response} res
  132. * @returns {Promise<void>}
  133. */
  134. async function paymentPage(req, res) {
  135. const user = SessionManager.getSessionUser(req.session)
  136. const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
  137. if (!plan) {
  138. return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
  139. }
  140. const hasSubscription =
  141. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  142. if (hasSubscription) {
  143. res.redirect('/user/subscription?hasSubscription=true')
  144. } else {
  145. // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
  146. // Recurly as well at this point (we don't do this most places for speed).
  147. const valid =
  148. await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
  149. user._id
  150. )
  151. if (!valid) {
  152. res.redirect('/user/subscription?hasSubscription=true')
  153. } else {
  154. let currency = null
  155. if (req.query.currency) {
  156. const queryCurrency = req.query.currency.toUpperCase()
  157. if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  158. currency = queryCurrency
  159. }
  160. }
  161. const { recommendedCurrency, countryCode } =
  162. await _getRecommendedCurrency(req, res)
  163. if (recommendedCurrency && currency == null) {
  164. currency = recommendedCurrency
  165. }
  166. // Block web sales to restricted countries
  167. if (['CU', 'IR', 'KP', 'RU', 'SY', 'VE'].includes(countryCode)) {
  168. return res.render('subscriptions/restricted-country', {
  169. title: 'restricted',
  170. })
  171. }
  172. res.render('subscriptions/new-react', {
  173. title: 'subscribe',
  174. currency,
  175. countryCode,
  176. plan,
  177. planCode: req.query.planCode,
  178. couponCode: req.query.cc,
  179. showCouponField: !!req.query.scf,
  180. itm_campaign: req.query.itm_campaign,
  181. itm_content: req.query.itm_content,
  182. itm_referrer: req.query.itm_referrer,
  183. })
  184. }
  185. }
  186. }
  187. function formatGroupPlansDataForDash() {
  188. return {
  189. plans: [...groupPlanModalOptions.plan_codes],
  190. sizes: [...groupPlanModalOptions.sizes],
  191. usages: [...groupPlanModalOptions.usages],
  192. priceByUsageTypeAndSize: JSON.parse(JSON.stringify(GroupPlansData)),
  193. }
  194. }
  195. /**
  196. * @param {import('express').Request} req
  197. * @param {import('express').Response} res
  198. * @returns {Promise<void>}
  199. */
  200. async function userSubscriptionPage(req, res) {
  201. const user = SessionManager.getSessionUser(req.session)
  202. const results =
  203. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  204. user
  205. )
  206. const {
  207. personalSubscription,
  208. memberGroupSubscriptions,
  209. managedGroupSubscriptions,
  210. currentInstitutionsWithLicence,
  211. managedInstitutions,
  212. managedPublishers,
  213. } = results
  214. const hasSubscription =
  215. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  216. const fromPlansPage = req.query.hasSubscription
  217. const plansData =
  218. SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
  219. personalSubscription?.plan
  220. )
  221. AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
  222. const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment(
  223. req,
  224. res,
  225. 'subscription-cancel-button'
  226. )
  227. const cancelButtonNewCopy = cancelButtonAssignment?.variant === 'new-copy'
  228. const groupPlansDataForDash = formatGroupPlansDataForDash()
  229. // display the Group Settings button only to admins of group subscriptions with the Managed Users feature available
  230. const groupSettingsEnabledFor = (managedGroupSubscriptions || [])
  231. .filter(
  232. subscription =>
  233. ManagedUsersManager.hasManagedUsersFeature(subscription) &&
  234. (subscription.admin_id._id || subscription.admin_id).toString() ===
  235. user._id.toString()
  236. )
  237. .map(subscription => subscription._id.toString())
  238. const data = {
  239. title: 'your_subscription',
  240. plans: plansData?.plans,
  241. planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
  242. user,
  243. hasSubscription,
  244. fromPlansPage,
  245. personalSubscription,
  246. memberGroupSubscriptions,
  247. managedGroupSubscriptions,
  248. managedInstitutions,
  249. managedPublishers,
  250. currentInstitutionsWithLicence,
  251. cancelButtonNewCopy,
  252. groupPlans: groupPlansDataForDash,
  253. groupSettingsEnabledFor,
  254. isManagedAccount: !!req.managedBy,
  255. userRestrictions: Array.from(req.userRestrictions || []),
  256. }
  257. res.render('subscriptions/dashboard-react', data)
  258. }
  259. async function interstitialPaymentPage(req, res) {
  260. const user = SessionManager.getSessionUser(req.session)
  261. const {
  262. recommendedCurrency,
  263. countryCode,
  264. geoPricingINRTestVariant,
  265. geoPricingLATAMTestVariant,
  266. } = await _getRecommendedCurrency(req, res)
  267. const hasSubscription =
  268. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  269. const showSkipLink = req.query?.skipLink === 'true'
  270. if (hasSubscription) {
  271. res.redirect('/user/subscription?hasSubscription=true')
  272. } else {
  273. let showInrGeoBanner, inrGeoBannerSplitTestName
  274. let inrGeoBannerVariant = 'default'
  275. if (countryCode === 'IN') {
  276. inrGeoBannerSplitTestName =
  277. geoPricingINRTestVariant === 'inr'
  278. ? 'geo-banners-inr-2'
  279. : 'geo-banners-inr-1'
  280. try {
  281. const geoBannerAssignment =
  282. await SplitTestHandler.promises.getAssignment(
  283. req,
  284. res,
  285. inrGeoBannerSplitTestName
  286. )
  287. inrGeoBannerVariant = geoBannerAssignment.variant
  288. if (inrGeoBannerVariant !== 'default') {
  289. showInrGeoBanner = true
  290. }
  291. } catch (error) {
  292. logger.error(
  293. { err: error },
  294. `Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
  295. )
  296. }
  297. }
  298. const paywallPlansPageViewSegmentation = {
  299. currency: recommendedCurrency,
  300. countryCode,
  301. 'geo-pricing-inr-group': geoPricingINRTestVariant,
  302. 'geo-pricing-inr-page': recommendedCurrency === 'INR' ? 'inr' : 'default',
  303. 'geo-pricing-latam-group': geoPricingLATAMTestVariant,
  304. 'geo-pricing-latam-page': ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(
  305. recommendedCurrency
  306. )
  307. ? 'latam'
  308. : 'default',
  309. }
  310. if (inrGeoBannerSplitTestName) {
  311. paywallPlansPageViewSegmentation[inrGeoBannerSplitTestName] =
  312. inrGeoBannerVariant
  313. }
  314. AnalyticsManager.recordEventForSession(
  315. req.session,
  316. 'paywall-plans-page-view',
  317. paywallPlansPageViewSegmentation
  318. )
  319. res.render('subscriptions/interstitial-payment', {
  320. title: 'subscribe',
  321. itm_content: req.query?.itm_content,
  322. itm_campaign: req.query?.itm_campaign,
  323. itm_referrer: req.query?.itm_referrer,
  324. recommendedCurrency,
  325. interstitialPaymentConfig,
  326. showSkipLink,
  327. showInrGeoBanner,
  328. })
  329. }
  330. }
  331. async function createSubscription(req, res) {
  332. const user = SessionManager.getSessionUser(req.session)
  333. const recurlyTokenIds = {
  334. billing: req.body.recurly_token_id,
  335. threeDSecureActionResult:
  336. req.body.recurly_three_d_secure_action_result_token_id,
  337. }
  338. const { subscriptionDetails } = req.body
  339. const hasSubscription =
  340. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  341. if (hasSubscription) {
  342. logger.warn({ userId: user._id }, 'user already has subscription')
  343. return res.sendStatus(409) // conflict
  344. }
  345. try {
  346. await SubscriptionHandler.promises.createSubscription(
  347. user,
  348. subscriptionDetails,
  349. recurlyTokenIds
  350. )
  351. res.sendStatus(201)
  352. } catch (err) {
  353. if (
  354. err instanceof SubscriptionErrors.RecurlyTransactionError ||
  355. err instanceof Errors.InvalidError
  356. ) {
  357. logger.error({ err }, 'recurly transaction error, potential 422')
  358. HttpErrorHandler.unprocessableEntity(
  359. req,
  360. res,
  361. err.message,
  362. OError.getFullInfo(err).public
  363. )
  364. } else {
  365. logger.warn(
  366. { err, userId: user._id },
  367. 'something went wrong creating subscription'
  368. )
  369. throw err
  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 currencyLookup = await GeoIpLookup.promises.getCurrencyCode(
  655. req.query?.ip || req.ip
  656. )
  657. const countryCode = currencyLookup.countryCode
  658. let assignmentINR, assignmentLATAM
  659. let recommendedCurrency = currencyLookup.currencyCode
  660. // for #12703
  661. try {
  662. // Split test is kept active, but all users geolocated in India can
  663. // now use the INR currency (See #13507)
  664. assignmentINR = await SplitTestHandler.promises.getAssignment(
  665. req,
  666. res,
  667. 'geo-pricing-inr'
  668. )
  669. } catch (error) {
  670. logger.error(
  671. { err: error },
  672. 'Failed to get assignment for geo-pricing-inr test'
  673. )
  674. }
  675. // for #13559
  676. try {
  677. assignmentLATAM = await SplitTestHandler.promises.getAssignment(
  678. req,
  679. res,
  680. 'geo-pricing-latam'
  681. )
  682. } catch (error) {
  683. logger.error(
  684. { err: error },
  685. 'Failed to get assignment for geo-pricing-latam test'
  686. )
  687. }
  688. if (
  689. ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
  690. assignmentLATAM?.variant !== 'latam'
  691. ) {
  692. recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
  693. }
  694. return {
  695. recommendedCurrency,
  696. countryCode,
  697. geoPricingINRTestVariant: assignmentINR?.variant,
  698. geoPricingLATAMTestVariant: assignmentLATAM?.variant,
  699. }
  700. }
  701. module.exports = {
  702. plansPage: expressify(plansPage),
  703. paymentPage: expressify(paymentPage),
  704. userSubscriptionPage: expressify(userSubscriptionPage),
  705. interstitialPaymentPage: expressify(interstitialPaymentPage),
  706. createSubscription: expressify(createSubscription),
  707. successfulSubscription: expressify(successfulSubscription),
  708. cancelSubscription,
  709. canceledSubscription,
  710. cancelV1Subscription,
  711. updateSubscription,
  712. cancelPendingSubscriptionChange,
  713. updateAccountEmailAddress,
  714. reactivateSubscription,
  715. recurlyCallback,
  716. renderUpgradeToAnnualPlanPage,
  717. processUpgradeToAnnualPlan,
  718. extendTrial: expressify(extendTrial),
  719. recurlyNotificationParser,
  720. refreshUserFeatures: expressify(refreshUserFeatures),
  721. redirectToHostedPage: expressify(redirectToHostedPage),
  722. }