SubscriptionController.js 24 KB

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