SubscriptionController.js 14 KB

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