SubscriptionController.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  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. // Block web sales to restricted countries
  201. if (['CU', 'IR', 'KP', 'RU', 'SY', 'VE'].includes(countryCode)) {
  202. return res.render('subscriptions/restricted-country', {
  203. title: 'restricted',
  204. })
  205. }
  206. res.render('subscriptions/new-react', {
  207. title: 'subscribe',
  208. currency,
  209. countryCode,
  210. plan,
  211. planCode: req.query.planCode,
  212. couponCode: req.query.cc,
  213. showCouponField: !!req.query.scf,
  214. itm_campaign: req.query.itm_campaign,
  215. itm_content: req.query.itm_content,
  216. itm_referrer: req.query.itm_referrer,
  217. })
  218. }
  219. }
  220. }
  221. function formatGroupPlansDataForDash() {
  222. return {
  223. plans: [...groupPlanModalOptions.plan_codes],
  224. sizes: [...groupPlanModalOptions.sizes],
  225. usages: [...groupPlanModalOptions.usages],
  226. priceByUsageTypeAndSize: JSON.parse(JSON.stringify(GroupPlansData)),
  227. }
  228. }
  229. /**
  230. * @param {import("express").Request} req
  231. * @param {import("express").Response} res
  232. * @returns {Promise<void>}
  233. */
  234. async function userSubscriptionPage(req, res) {
  235. const user = SessionManager.getSessionUser(req.session)
  236. const results =
  237. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  238. user
  239. )
  240. const {
  241. personalSubscription,
  242. memberGroupSubscriptions,
  243. managedGroupSubscriptions,
  244. currentInstitutionsWithLicence,
  245. managedInstitutions,
  246. managedPublishers,
  247. } = results
  248. const hasSubscription =
  249. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  250. const fromPlansPage = req.query.hasSubscription
  251. const plansData =
  252. SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
  253. personalSubscription?.plan
  254. )
  255. AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
  256. const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment(
  257. req,
  258. res,
  259. 'subscription-cancel-button'
  260. )
  261. const cancelButtonNewCopy = cancelButtonAssignment?.variant === 'new-copy'
  262. const groupPlansDataForDash = formatGroupPlansDataForDash()
  263. const groupSettingsEnabledFor = (managedGroupSubscriptions || [])
  264. .map(sub => sub._id.toString())
  265. .filter(id => ManagedUsersManager.hasManagedUsersFeature(id))
  266. const data = {
  267. title: 'your_subscription',
  268. plans: plansData?.plans,
  269. planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
  270. user,
  271. hasSubscription,
  272. fromPlansPage,
  273. personalSubscription,
  274. memberGroupSubscriptions,
  275. managedGroupSubscriptions,
  276. managedInstitutions,
  277. managedPublishers,
  278. currentInstitutionsWithLicence,
  279. cancelButtonNewCopy,
  280. groupPlans: groupPlansDataForDash,
  281. groupSettingsEnabledFor,
  282. isManagedAccount: !!req.managedBy,
  283. userRestrictions: Array.from(req.userRestrictions || []),
  284. }
  285. res.render('subscriptions/dashboard-react', data)
  286. }
  287. async function interstitialPaymentPage(req, res) {
  288. const user = SessionManager.getSessionUser(req.session)
  289. const {
  290. recommendedCurrency,
  291. countryCode,
  292. geoPricingINRTestVariant,
  293. geoPricingLATAMTestVariant,
  294. } = await _getRecommendedCurrency(req, res)
  295. const hasSubscription =
  296. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  297. const showSkipLink = req.query?.skipLink === 'true'
  298. let removePersonalPlanAssingment = { variant: 'default' }
  299. try {
  300. removePersonalPlanAssingment =
  301. await SplitTestHandler.promises.getAssignment(
  302. req,
  303. res,
  304. 'remove-personal-plan'
  305. )
  306. } catch (error) {
  307. logger.error(
  308. { err: error },
  309. 'Failed to get assignment for remove-personal-plan test'
  310. )
  311. }
  312. const { interstitialPaymentConfig, directory } = getPlansSplitOptions(
  313. removePersonalPlanAssingment
  314. )
  315. if (hasSubscription) {
  316. res.redirect('/user/subscription?hasSubscription=true')
  317. } else {
  318. let showInrGeoBanner, inrGeoBannerSplitTestName
  319. let inrGeoBannerVariant = 'default'
  320. if (countryCode === 'IN') {
  321. inrGeoBannerSplitTestName =
  322. geoPricingINRTestVariant === 'inr'
  323. ? 'geo-banners-inr-2'
  324. : 'geo-banners-inr-1'
  325. try {
  326. const geoBannerAssignment =
  327. await SplitTestHandler.promises.getAssignment(
  328. req,
  329. res,
  330. inrGeoBannerSplitTestName
  331. )
  332. inrGeoBannerVariant = geoBannerAssignment.variant
  333. if (inrGeoBannerVariant !== 'default') {
  334. showInrGeoBanner = true
  335. }
  336. } catch (error) {
  337. logger.error(
  338. { err: error },
  339. `Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
  340. )
  341. }
  342. }
  343. const paywallPlansPageViewSegmentation = {
  344. currency: recommendedCurrency,
  345. countryCode,
  346. 'geo-pricing-inr-group': geoPricingINRTestVariant,
  347. 'geo-pricing-inr-page': recommendedCurrency === 'INR' ? 'inr' : 'default',
  348. 'geo-pricing-latam-group': geoPricingLATAMTestVariant,
  349. 'geo-pricing-latam-page': ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(
  350. recommendedCurrency
  351. )
  352. ? 'latam'
  353. : 'default',
  354. 'remove-personal-plan-page': removePersonalPlanAssingment?.variant,
  355. }
  356. if (inrGeoBannerSplitTestName) {
  357. paywallPlansPageViewSegmentation[inrGeoBannerSplitTestName] =
  358. inrGeoBannerVariant
  359. }
  360. AnalyticsManager.recordEventForSession(
  361. req.session,
  362. 'paywall-plans-page-view',
  363. paywallPlansPageViewSegmentation
  364. )
  365. res.render(
  366. `subscriptions/plans-marketing/${directory}/interstitial-payment`,
  367. {
  368. title: 'subscribe',
  369. itm_content: req.query?.itm_content,
  370. itm_campaign: req.query?.itm_campaign,
  371. itm_referrer: req.query?.itm_referrer,
  372. recommendedCurrency,
  373. interstitialPaymentConfig,
  374. showSkipLink,
  375. showInrGeoBanner,
  376. }
  377. )
  378. }
  379. }
  380. async function createSubscription(req, res) {
  381. const user = SessionManager.getSessionUser(req.session)
  382. const recurlyTokenIds = {
  383. billing: req.body.recurly_token_id,
  384. threeDSecureActionResult:
  385. req.body.recurly_three_d_secure_action_result_token_id,
  386. }
  387. const { subscriptionDetails } = req.body
  388. const hasSubscription =
  389. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  390. if (hasSubscription) {
  391. logger.warn({ userId: user._id }, 'user already has subscription')
  392. return res.sendStatus(409) // conflict
  393. }
  394. try {
  395. await SubscriptionHandler.promises.createSubscription(
  396. user,
  397. subscriptionDetails,
  398. recurlyTokenIds
  399. )
  400. res.sendStatus(201)
  401. } catch (err) {
  402. if (
  403. err instanceof SubscriptionErrors.RecurlyTransactionError ||
  404. err instanceof Errors.InvalidError
  405. ) {
  406. logger.error({ err }, 'recurly transaction error, potential 422')
  407. HttpErrorHandler.unprocessableEntity(
  408. req,
  409. res,
  410. err.message,
  411. OError.getFullInfo(err).public
  412. )
  413. } else {
  414. logger.warn(
  415. { err, userId: user._id },
  416. 'something went wrong creating subscription'
  417. )
  418. throw err
  419. }
  420. }
  421. }
  422. /**
  423. * @param {import("express").Request} req
  424. * @param {import("express").Response} res
  425. * @returns {Promise<void>}
  426. */
  427. async function successfulSubscription(req, res) {
  428. const user = SessionManager.getSessionUser(req.session)
  429. const { personalSubscription } =
  430. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  431. user
  432. )
  433. const postCheckoutRedirect = req.session?.postCheckoutRedirect
  434. if (!personalSubscription) {
  435. res.redirect('/user/subscription/plans')
  436. } else {
  437. res.render('subscriptions/successful-subscription-react', {
  438. title: 'thank_you',
  439. personalSubscription,
  440. postCheckoutRedirect,
  441. })
  442. }
  443. }
  444. function cancelSubscription(req, res, next) {
  445. const user = SessionManager.getSessionUser(req.session)
  446. logger.debug({ userId: user._id }, 'canceling subscription')
  447. SubscriptionHandler.cancelSubscription(user, function (err) {
  448. if (err) {
  449. OError.tag(err, 'something went wrong canceling subscription', {
  450. user_id: user._id,
  451. })
  452. return next(err)
  453. }
  454. // Note: this redirect isn't used in the main flow as the redirection is
  455. // handled by Angular
  456. res.redirect('/user/subscription/canceled')
  457. })
  458. }
  459. /**
  460. * @param {import("express").Request} req
  461. * @param {import("express").Response} res
  462. * @param {import("express").NextFunction} next
  463. * @returns {Promise<void>}
  464. */
  465. function canceledSubscription(req, res, next) {
  466. return res.render('subscriptions/canceled-subscription-react', {
  467. title: 'subscription_canceled',
  468. })
  469. }
  470. function cancelV1Subscription(req, res, next) {
  471. const userId = SessionManager.getLoggedInUserId(req.session)
  472. logger.debug({ userId }, 'canceling v1 subscription')
  473. V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
  474. if (err) {
  475. OError.tag(err, 'something went wrong canceling v1 subscription', {
  476. userId,
  477. })
  478. return next(err)
  479. }
  480. res.redirect('/user/subscription')
  481. })
  482. }
  483. function updateSubscription(req, res, next) {
  484. const origin = req && req.query ? req.query.origin : null
  485. const user = SessionManager.getSessionUser(req.session)
  486. const planCode = req.body.plan_code
  487. if (planCode == null) {
  488. const err = new Error('plan_code is not defined')
  489. logger.warn(
  490. { userId: user._id, err, planCode, origin, body: req.body },
  491. '[Subscription] error in updateSubscription form'
  492. )
  493. return next(err)
  494. }
  495. logger.debug({ planCode, userId: user._id }, 'updating subscription')
  496. SubscriptionHandler.updateSubscription(user, planCode, null, function (err) {
  497. if (err) {
  498. OError.tag(err, 'something went wrong updating subscription', {
  499. user_id: user._id,
  500. })
  501. return next(err)
  502. }
  503. res.redirect('/user/subscription')
  504. })
  505. }
  506. function cancelPendingSubscriptionChange(req, res, next) {
  507. const user = SessionManager.getSessionUser(req.session)
  508. logger.debug({ userId: user._id }, 'canceling pending subscription change')
  509. SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
  510. if (err) {
  511. OError.tag(
  512. err,
  513. 'something went wrong canceling pending subscription change',
  514. {
  515. user_id: user._id,
  516. }
  517. )
  518. return next(err)
  519. }
  520. res.redirect('/user/subscription')
  521. })
  522. }
  523. function updateAccountEmailAddress(req, res, next) {
  524. const user = SessionManager.getSessionUser(req.session)
  525. RecurlyWrapper.updateAccountEmailAddress(
  526. user._id,
  527. user.email,
  528. function (error) {
  529. if (error) {
  530. return next(error)
  531. }
  532. res.sendStatus(200)
  533. }
  534. )
  535. }
  536. function reactivateSubscription(req, res, next) {
  537. const user = SessionManager.getSessionUser(req.session)
  538. logger.debug({ userId: user._id }, 'reactivating subscription')
  539. try {
  540. if (req.isManagedGroupAdmin) {
  541. // allow admins to reactivate subscriptions
  542. } else {
  543. // otherwise require the user to have the reactivate-subscription permission
  544. req.assertPermission('reactivate-subscription')
  545. }
  546. } catch (error) {
  547. return next(error)
  548. }
  549. SubscriptionHandler.reactivateSubscription(user, function (err) {
  550. if (err) {
  551. OError.tag(err, 'something went wrong reactivating subscription', {
  552. user_id: user._id,
  553. })
  554. return next(err)
  555. }
  556. res.redirect('/user/subscription')
  557. })
  558. }
  559. function recurlyCallback(req, res, next) {
  560. logger.debug({ data: req.body }, 'received recurly callback')
  561. const event = Object.keys(req.body)[0]
  562. const eventData = req.body[event]
  563. RecurlyEventHandler.sendRecurlyAnalyticsEvent(event, eventData).catch(error =>
  564. logger.error(
  565. { err: error },
  566. 'Failed to process analytics event on Recurly webhook'
  567. )
  568. )
  569. if (
  570. [
  571. 'new_subscription_notification',
  572. 'updated_subscription_notification',
  573. 'expired_subscription_notification',
  574. ].includes(event)
  575. ) {
  576. const recurlySubscription = eventData.subscription
  577. SubscriptionHandler.syncSubscription(
  578. recurlySubscription,
  579. { ip: req.ip },
  580. function (err) {
  581. if (err) {
  582. return next(err)
  583. }
  584. res.sendStatus(200)
  585. }
  586. )
  587. } else if (event === 'billing_info_updated_notification') {
  588. const recurlyAccountCode = eventData.account.account_code
  589. SubscriptionHandler.attemptPaypalInvoiceCollection(
  590. recurlyAccountCode,
  591. function (err) {
  592. if (err) {
  593. return next(err)
  594. }
  595. res.sendStatus(200)
  596. }
  597. )
  598. } else {
  599. res.sendStatus(200)
  600. }
  601. }
  602. function renderUpgradeToAnnualPlanPage(req, res, next) {
  603. const user = SessionManager.getSessionUser(req.session)
  604. LimitationsManager.userHasV2Subscription(
  605. user,
  606. function (err, hasSubscription, subscription) {
  607. let planName
  608. if (err) {
  609. return next(err)
  610. }
  611. const planCode = subscription
  612. ? subscription.planCode.toLowerCase()
  613. : undefined
  614. if ((planCode ? planCode.indexOf('annual') : undefined) !== -1) {
  615. planName = 'annual'
  616. } else if ((planCode ? planCode.indexOf('student') : undefined) !== -1) {
  617. planName = 'student'
  618. } else if (
  619. (planCode ? planCode.indexOf('collaborator') : undefined) !== -1
  620. ) {
  621. planName = 'collaborator'
  622. }
  623. if (hasSubscription) {
  624. res.render('subscriptions/upgradeToAnnual', {
  625. title: 'Upgrade to annual',
  626. planName,
  627. })
  628. } else {
  629. res.redirect('/user/subscription/plans')
  630. }
  631. }
  632. )
  633. }
  634. function processUpgradeToAnnualPlan(req, res, next) {
  635. const user = SessionManager.getSessionUser(req.session)
  636. const { planName } = req.body
  637. const couponCode = Settings.coupon_codes.upgradeToAnnualPromo[planName]
  638. const annualPlanName = `${planName}-annual`
  639. logger.debug(
  640. { userId: user._id, planName: annualPlanName },
  641. 'user is upgrading to annual billing with discount'
  642. )
  643. return SubscriptionHandler.updateSubscription(
  644. user,
  645. annualPlanName,
  646. couponCode,
  647. function (err) {
  648. if (err) {
  649. OError.tag(err, 'error updating subscription', {
  650. user_id: user._id,
  651. })
  652. return next(err)
  653. }
  654. res.sendStatus(200)
  655. }
  656. )
  657. }
  658. async function extendTrial(req, res) {
  659. const user = SessionManager.getSessionUser(req.session)
  660. const { subscription } =
  661. await LimitationsManager.promises.userHasV2Subscription(user)
  662. try {
  663. await SubscriptionHandler.promises.extendTrial(subscription, 14)
  664. AnalyticsManager.recordEventForSession(
  665. req.session,
  666. 'subscription-trial-extended'
  667. )
  668. } catch (error) {
  669. return res.sendStatus(500)
  670. }
  671. res.sendStatus(200)
  672. }
  673. function recurlyNotificationParser(req, res, next) {
  674. let xml = ''
  675. req.on('data', chunk => (xml += chunk))
  676. req.on('end', () =>
  677. RecurlyWrapper._parseXml(xml, function (error, body) {
  678. if (error) {
  679. return next(error)
  680. }
  681. req.body = body
  682. next()
  683. })
  684. )
  685. }
  686. async function refreshUserFeatures(req, res) {
  687. const { user_id: userId } = req.params
  688. await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test')
  689. res.sendStatus(200)
  690. }
  691. async function redirectToHostedPage(req, res) {
  692. const userId = SessionManager.getLoggedInUserId(req.session)
  693. const { pageType } = req.params
  694. const url =
  695. await SubscriptionViewModelBuilder.promises.getRedirectToHostedPage(
  696. userId,
  697. pageType
  698. )
  699. logger.warn({ userId, pageType }, 'redirecting to recurly hosted page')
  700. res.redirect(url)
  701. }
  702. async function _getRecommendedCurrency(req, res) {
  703. const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(
  704. req.query?.ip || req.ip
  705. )
  706. const countryCode = currencyLookup.countryCode
  707. let assignmentINR, assignmentLATAM
  708. let recommendedCurrency = currencyLookup.currencyCode
  709. // for #12703
  710. try {
  711. // Split test is kept active, but all users geolocated in India can
  712. // now use the INR currency (See #13507)
  713. assignmentINR = await SplitTestHandler.promises.getAssignment(
  714. req,
  715. res,
  716. 'geo-pricing-inr'
  717. )
  718. } catch (error) {
  719. logger.error(
  720. { err: error },
  721. 'Failed to get assignment for geo-pricing-inr test'
  722. )
  723. }
  724. // for #13559
  725. try {
  726. assignmentLATAM = await SplitTestHandler.promises.getAssignment(
  727. req,
  728. res,
  729. 'geo-pricing-latam'
  730. )
  731. } catch (error) {
  732. logger.error(
  733. { err: error },
  734. 'Failed to get assignment for geo-pricing-latam test'
  735. )
  736. }
  737. if (
  738. ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
  739. assignmentLATAM?.variant !== 'latam'
  740. ) {
  741. recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
  742. }
  743. return {
  744. recommendedCurrency,
  745. countryCode,
  746. geoPricingINRTestVariant: assignmentINR?.variant,
  747. geoPricingLATAMTestVariant: assignmentLATAM?.variant,
  748. }
  749. }
  750. module.exports = {
  751. plansPage: expressify(plansPage),
  752. paymentPage: expressify(paymentPage),
  753. userSubscriptionPage: expressify(userSubscriptionPage),
  754. interstitialPaymentPage: expressify(interstitialPaymentPage),
  755. createSubscription: expressify(createSubscription),
  756. successfulSubscription: expressify(successfulSubscription),
  757. cancelSubscription,
  758. canceledSubscription,
  759. cancelV1Subscription,
  760. updateSubscription,
  761. cancelPendingSubscriptionChange,
  762. updateAccountEmailAddress,
  763. reactivateSubscription,
  764. recurlyCallback,
  765. renderUpgradeToAnnualPlanPage,
  766. processUpgradeToAnnualPlan,
  767. extendTrial: expressify(extendTrial),
  768. recurlyNotificationParser,
  769. refreshUserFeatures: expressify(refreshUserFeatures),
  770. redirectToHostedPage: expressify(redirectToHostedPage),
  771. }