SubscriptionController.js 25 KB

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