SubscriptionController.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  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 GroupPlansData = require('./GroupPlansData')
  13. const V1SubscriptionManager = require('./V1SubscriptionManager')
  14. const Errors = require('../Errors/Errors')
  15. const HttpErrorHandler = require('../Errors/HttpErrorHandler')
  16. const SubscriptionErrors = require('./Errors')
  17. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  18. const RecurlyEventHandler = require('./RecurlyEventHandler')
  19. const { expressify } = require('../../util/promises')
  20. const OError = require('@overleaf/o-error')
  21. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  22. const groupPlanModalOptions = Settings.groupPlanModalOptions
  23. const validGroupPlanModalOptions = {
  24. plan_code: groupPlanModalOptions.plan_codes.map(item => item.code),
  25. currency: groupPlanModalOptions.currencies.map(item => item.code),
  26. size: groupPlanModalOptions.sizes,
  27. usage: groupPlanModalOptions.usages.map(item => item.code),
  28. }
  29. async function plansPage(req, res) {
  30. const plans = SubscriptionViewModelBuilder.buildPlansList()
  31. const { currencyCode: recommendedCurrency } =
  32. await GeoIpLookup.promises.getCurrencyCode(
  33. (req.query ? req.query.ip : undefined) || req.ip
  34. )
  35. function getDefault(param, category, defaultValue) {
  36. const v = req.query && req.query[param]
  37. if (v && validGroupPlanModalOptions[category].includes(v)) {
  38. return v
  39. }
  40. return defaultValue
  41. }
  42. let defaultGroupPlanModalCurrency = 'USD'
  43. if (validGroupPlanModalOptions.currency.includes(recommendedCurrency)) {
  44. defaultGroupPlanModalCurrency = recommendedCurrency
  45. }
  46. const groupPlanModalDefaults = {
  47. plan_code: getDefault('plan', 'plan_code', 'collaborator'),
  48. size: getDefault('number', 'size', '10'),
  49. currency: getDefault('currency', 'currency', defaultGroupPlanModalCurrency),
  50. usage: getDefault('usage', 'usage', 'enterprise'),
  51. }
  52. AnalyticsManager.recordEventForSession(req.session, 'plans-page-view')
  53. const assignment = await SplitTestHandler.promises.getAssignment(
  54. req,
  55. 'plans-page-layout'
  56. )
  57. const newPlansPageVariant =
  58. assignment && assignment.variant === 'new-plans-page'
  59. const standardPlanNameAssignment =
  60. await SplitTestHandler.promises.getAssignment(req, 'standard-plan-name')
  61. const useNewPlanName =
  62. standardPlanNameAssignment &&
  63. standardPlanNameAssignment.variant === 'new-plan-name'
  64. res.render('subscriptions/plans-marketing', {
  65. title: 'plans_and_pricing',
  66. plans,
  67. itm_content: req.query && req.query.itm_content,
  68. recomendedCurrency: recommendedCurrency,
  69. recommendedCurrency,
  70. planFeatures,
  71. groupPlans: GroupPlansData,
  72. groupPlanModalOptions,
  73. groupPlanModalDefaults,
  74. newPlansPageVariant,
  75. useNewPlanName,
  76. })
  77. }
  78. // get to show the recurly.js page
  79. async function paymentPage(req, res) {
  80. const user = SessionManager.getSessionUser(req.session)
  81. const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
  82. if (!plan) {
  83. return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
  84. }
  85. const hasSubscription =
  86. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  87. if (hasSubscription) {
  88. res.redirect('/user/subscription?hasSubscription=true')
  89. } else {
  90. // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
  91. // Recurly as well at this point (we don't do this most places for speed).
  92. const valid =
  93. await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
  94. user._id
  95. )
  96. if (!valid) {
  97. res.redirect('/user/subscription?hasSubscription=true')
  98. } else {
  99. let currency = null
  100. if (req.query.currency) {
  101. const queryCurrency = req.query.currency.toUpperCase()
  102. if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
  103. currency = queryCurrency
  104. }
  105. }
  106. const { currencyCode: recommendedCurrency, countryCode } =
  107. await GeoIpLookup.promises.getCurrencyCode(
  108. (req.query ? req.query.ip : undefined) || req.ip
  109. )
  110. if (recommendedCurrency && currency == null) {
  111. currency = recommendedCurrency
  112. }
  113. const assignment = await SplitTestHandler.promises.getAssignment(
  114. req,
  115. 'payment-page'
  116. )
  117. const template =
  118. assignment && assignment.variant === 'updated-payment-page'
  119. ? 'subscriptions/new-updated'
  120. : 'subscriptions/new'
  121. res.render(template, {
  122. title: 'subscribe',
  123. currency,
  124. countryCode,
  125. plan,
  126. showStudentPlan: req.query.ssp === 'true',
  127. recurlyConfig: JSON.stringify({
  128. currency,
  129. subdomain: Settings.apis.recurly.subdomain,
  130. }),
  131. showCouponField: !!req.query.scf,
  132. showVatField: !!req.query.svf,
  133. })
  134. }
  135. }
  136. }
  137. async function userSubscriptionPage(req, res) {
  138. const user = SessionManager.getSessionUser(req.session)
  139. const results =
  140. await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
  141. user
  142. )
  143. const {
  144. personalSubscription,
  145. memberGroupSubscriptions,
  146. managedGroupSubscriptions,
  147. currentInstitutionsWithLicence,
  148. managedInstitutions,
  149. managedPublishers,
  150. v1SubscriptionStatus,
  151. } = results
  152. const hasSubscription =
  153. await LimitationsManager.promises.userHasV1OrV2Subscription(user)
  154. const fromPlansPage = req.query.hasSubscription
  155. const plans = SubscriptionViewModelBuilder.buildPlansList(
  156. personalSubscription ? personalSubscription.plan : undefined
  157. )
  158. AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
  159. const assignment = await SplitTestHandler.promises.getAssignment(
  160. req,
  161. 'subscription-cancel-button'
  162. )
  163. const cancelButtonNewCopy = assignment && assignment.variant === 'new-copy'
  164. const data = {
  165. title: 'your_subscription',
  166. plans,
  167. groupPlans: GroupPlansData,
  168. user,
  169. hasSubscription,
  170. fromPlansPage,
  171. personalSubscription,
  172. memberGroupSubscriptions,
  173. managedGroupSubscriptions,
  174. managedInstitutions,
  175. managedPublishers,
  176. v1SubscriptionStatus,
  177. currentInstitutionsWithLicence,
  178. groupPlanModalOptions,
  179. cancelButtonNewCopy,
  180. }
  181. res.render('subscriptions/dashboard', data)
  182. }
  183. function createSubscription(req, res, next) {
  184. const user = SessionManager.getSessionUser(req.session)
  185. const recurlyTokenIds = {
  186. billing: req.body.recurly_token_id,
  187. threeDSecureActionResult:
  188. req.body.recurly_three_d_secure_action_result_token_id,
  189. }
  190. const { subscriptionDetails } = req.body
  191. LimitationsManager.userHasV1OrV2Subscription(
  192. user,
  193. function (err, hasSubscription) {
  194. if (err) {
  195. return next(err)
  196. }
  197. if (hasSubscription) {
  198. logger.warn({ user_id: user._id }, 'user already has subscription')
  199. return res.sendStatus(409) // conflict
  200. }
  201. return SubscriptionHandler.createSubscription(
  202. user,
  203. subscriptionDetails,
  204. recurlyTokenIds,
  205. function (err) {
  206. if (!err) {
  207. return res.sendStatus(201)
  208. }
  209. if (
  210. err instanceof SubscriptionErrors.RecurlyTransactionError ||
  211. err instanceof Errors.InvalidError
  212. ) {
  213. logger.error({ err }, 'recurly transaction error, potential 422')
  214. HttpErrorHandler.unprocessableEntity(
  215. req,
  216. res,
  217. err.message,
  218. OError.getFullInfo(err).public
  219. )
  220. } else {
  221. logger.warn(
  222. { err, user_id: user._id },
  223. 'something went wrong creating subscription'
  224. )
  225. next(err)
  226. }
  227. }
  228. )
  229. }
  230. )
  231. }
  232. function successfulSubscription(req, res, next) {
  233. const user = SessionManager.getSessionUser(req.session)
  234. return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel(
  235. user,
  236. function (error, { personalSubscription } = {}) {
  237. if (error) {
  238. return next(error)
  239. }
  240. if (personalSubscription == null) {
  241. res.redirect('/user/subscription/plans')
  242. } else {
  243. res.render('subscriptions/successful_subscription', {
  244. title: 'thank_you',
  245. personalSubscription,
  246. })
  247. }
  248. }
  249. )
  250. }
  251. function cancelSubscription(req, res, next) {
  252. const user = SessionManager.getSessionUser(req.session)
  253. logger.log({ user_id: user._id }, 'canceling subscription')
  254. SubscriptionHandler.cancelSubscription(user, function (err) {
  255. if (err) {
  256. OError.tag(err, 'something went wrong canceling subscription', {
  257. user_id: user._id,
  258. })
  259. return next(err)
  260. }
  261. // Note: this redirect isn't used in the main flow as the redirection is
  262. // handled by Angular
  263. res.redirect('/user/subscription/canceled')
  264. })
  265. }
  266. function canceledSubscription(req, res, next) {
  267. return res.render('subscriptions/canceled_subscription', {
  268. title: 'subscription_canceled',
  269. })
  270. }
  271. function cancelV1Subscription(req, res, next) {
  272. const userId = SessionManager.getLoggedInUserId(req.session)
  273. logger.log({ userId }, 'canceling v1 subscription')
  274. V1SubscriptionManager.cancelV1Subscription(userId, function (err) {
  275. if (err) {
  276. OError.tag(err, 'something went wrong canceling v1 subscription', {
  277. userId,
  278. })
  279. return next(err)
  280. }
  281. res.redirect('/user/subscription')
  282. })
  283. }
  284. function updateSubscription(req, res, next) {
  285. const origin = req && req.query ? req.query.origin : null
  286. const user = SessionManager.getSessionUser(req.session)
  287. const planCode = req.body.plan_code
  288. if (planCode == null) {
  289. const err = new Error('plan_code is not defined')
  290. logger.warn(
  291. { user_id: user._id, err, planCode, origin, body: req.body },
  292. '[Subscription] error in updateSubscription form'
  293. )
  294. return next(err)
  295. }
  296. logger.log({ planCode, user_id: user._id }, 'updating subscription')
  297. SubscriptionHandler.updateSubscription(user, planCode, null, function (err) {
  298. if (err) {
  299. OError.tag(err, 'something went wrong updating subscription', {
  300. user_id: user._id,
  301. })
  302. return next(err)
  303. }
  304. res.redirect('/user/subscription')
  305. })
  306. }
  307. function cancelPendingSubscriptionChange(req, res, next) {
  308. const user = SessionManager.getSessionUser(req.session)
  309. logger.log({ user_id: user._id }, 'canceling pending subscription change')
  310. SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) {
  311. if (err) {
  312. OError.tag(
  313. err,
  314. 'something went wrong canceling pending subscription change',
  315. {
  316. user_id: user._id,
  317. }
  318. )
  319. return next(err)
  320. }
  321. res.redirect('/user/subscription')
  322. })
  323. }
  324. function updateAccountEmailAddress(req, res, next) {
  325. const user = SessionManager.getSessionUser(req.session)
  326. RecurlyWrapper.updateAccountEmailAddress(
  327. user._id,
  328. user.email,
  329. function (error) {
  330. if (error) {
  331. return next(error)
  332. }
  333. res.sendStatus(200)
  334. }
  335. )
  336. }
  337. function reactivateSubscription(req, res, next) {
  338. const user = SessionManager.getSessionUser(req.session)
  339. logger.log({ user_id: user._id }, 'reactivating subscription')
  340. SubscriptionHandler.reactivateSubscription(user, function (err) {
  341. if (err) {
  342. OError.tag(err, 'something went wrong reactivating subscription', {
  343. user_id: user._id,
  344. })
  345. return next(err)
  346. }
  347. res.redirect('/user/subscription')
  348. })
  349. }
  350. function recurlyCallback(req, res, next) {
  351. logger.log({ data: req.body }, 'received recurly callback')
  352. const event = Object.keys(req.body)[0]
  353. const eventData = req.body[event]
  354. RecurlyEventHandler.sendRecurlyAnalyticsEvent(event, eventData).catch(error =>
  355. logger.error(
  356. { err: error },
  357. 'Failed to process analytics event on Recurly webhook'
  358. )
  359. )
  360. if (
  361. [
  362. 'new_subscription_notification',
  363. 'updated_subscription_notification',
  364. 'expired_subscription_notification',
  365. ].includes(event)
  366. ) {
  367. const recurlySubscription = eventData.subscription
  368. SubscriptionHandler.syncSubscription(
  369. recurlySubscription,
  370. { ip: req.ip },
  371. function (err) {
  372. if (err) {
  373. return next(err)
  374. }
  375. res.sendStatus(200)
  376. }
  377. )
  378. } else if (event === 'billing_info_updated_notification') {
  379. const recurlyAccountCode = eventData.account.account_code
  380. SubscriptionHandler.attemptPaypalInvoiceCollection(
  381. recurlyAccountCode,
  382. function (err) {
  383. if (err) {
  384. return next(err)
  385. }
  386. res.sendStatus(200)
  387. }
  388. )
  389. } else {
  390. res.sendStatus(200)
  391. }
  392. }
  393. function renderUpgradeToAnnualPlanPage(req, res, next) {
  394. const user = SessionManager.getSessionUser(req.session)
  395. LimitationsManager.userHasV2Subscription(
  396. user,
  397. function (err, hasSubscription, subscription) {
  398. let planName
  399. if (err) {
  400. return next(err)
  401. }
  402. const planCode = subscription
  403. ? subscription.planCode.toLowerCase()
  404. : undefined
  405. if ((planCode ? planCode.indexOf('annual') : undefined) !== -1) {
  406. planName = 'annual'
  407. } else if ((planCode ? planCode.indexOf('student') : undefined) !== -1) {
  408. planName = 'student'
  409. } else if (
  410. (planCode ? planCode.indexOf('collaborator') : undefined) !== -1
  411. ) {
  412. planName = 'collaborator'
  413. }
  414. if (hasSubscription) {
  415. res.render('subscriptions/upgradeToAnnual', {
  416. title: 'Upgrade to annual',
  417. planName,
  418. })
  419. } else {
  420. res.redirect('/user/subscription/plans')
  421. }
  422. }
  423. )
  424. }
  425. function processUpgradeToAnnualPlan(req, res, next) {
  426. const user = SessionManager.getSessionUser(req.session)
  427. const { planName } = req.body
  428. const couponCode = Settings.coupon_codes.upgradeToAnnualPromo[planName]
  429. const annualPlanName = `${planName}-annual`
  430. logger.log(
  431. { user_id: user._id, planName: annualPlanName },
  432. 'user is upgrading to annual billing with discount'
  433. )
  434. return SubscriptionHandler.updateSubscription(
  435. user,
  436. annualPlanName,
  437. couponCode,
  438. function (err) {
  439. if (err) {
  440. OError.tag(err, 'error updating subscription', {
  441. user_id: user._id,
  442. })
  443. return next(err)
  444. }
  445. res.sendStatus(200)
  446. }
  447. )
  448. }
  449. async function extendTrial(req, res) {
  450. const user = SessionManager.getSessionUser(req.session)
  451. const { subscription } =
  452. await LimitationsManager.promises.userHasV2Subscription(user)
  453. try {
  454. await SubscriptionHandler.promises.extendTrial(subscription, 14)
  455. AnalyticsManager.recordEventForSession(
  456. req.session,
  457. 'subscription-trial-extended'
  458. )
  459. } catch (error) {
  460. return res.sendStatus(500)
  461. }
  462. res.sendStatus(200)
  463. }
  464. function recurlyNotificationParser(req, res, next) {
  465. let xml = ''
  466. req.on('data', chunk => (xml += chunk))
  467. req.on('end', () =>
  468. RecurlyWrapper._parseXml(xml, function (error, body) {
  469. if (error) {
  470. return next(error)
  471. }
  472. req.body = body
  473. next()
  474. })
  475. )
  476. }
  477. async function refreshUserFeatures(req, res) {
  478. const { user_id: userId } = req.params
  479. await FeaturesUpdater.promises.refreshFeatures(userId, 'acceptance-test')
  480. res.sendStatus(200)
  481. }
  482. async function redirectToHostedPage(req, res) {
  483. const userId = SessionManager.getLoggedInUserId(req.session)
  484. const { pageType } = req.params
  485. const url =
  486. await SubscriptionViewModelBuilder.promises.getRedirectToHostedPage(
  487. userId,
  488. pageType
  489. )
  490. logger.warn({ userId, pageType }, 'redirecting to recurly hosted page')
  491. res.redirect(url)
  492. }
  493. module.exports = {
  494. plansPage: expressify(plansPage),
  495. paymentPage: expressify(paymentPage),
  496. userSubscriptionPage: expressify(userSubscriptionPage),
  497. createSubscription,
  498. successfulSubscription,
  499. cancelSubscription,
  500. canceledSubscription,
  501. cancelV1Subscription,
  502. updateSubscription,
  503. cancelPendingSubscriptionChange,
  504. updateAccountEmailAddress,
  505. reactivateSubscription,
  506. recurlyCallback,
  507. renderUpgradeToAnnualPlanPage,
  508. processUpgradeToAnnualPlan,
  509. extendTrial: expressify(extendTrial),
  510. recurlyNotificationParser,
  511. refreshUserFeatures: expressify(refreshUserFeatures),
  512. redirectToHostedPage: expressify(redirectToHostedPage),
  513. }