SubscriptionGroupController.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. // ts-check
  2. import SubscriptionGroupHandler from './SubscriptionGroupHandler.js'
  3. import OError from '@overleaf/o-error'
  4. import logger from '@overleaf/logger'
  5. import SubscriptionLocator from './SubscriptionLocator.js'
  6. import SessionManager from '../Authentication/SessionManager.js'
  7. import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
  8. import { expressify } from '@overleaf/promise-utils'
  9. import Modules from '../../infrastructure/Modules.js'
  10. import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
  11. import UserGetter from '../User/UserGetter.js'
  12. import { Subscription } from '../../models/Subscription.js'
  13. import { isProfessionalGroupPlan } from './PlansHelper.mjs'
  14. import {
  15. MissingBillingInfoError,
  16. ManuallyCollectedError,
  17. PendingChangeError,
  18. InactiveError,
  19. SubtotalLimitExceededError,
  20. HasPastDueInvoiceError,
  21. } from './Errors.js'
  22. import RecurlyClient from './RecurlyClient.js'
  23. /**
  24. * @import { Subscription } from "../../../../types/subscription/dashboard/subscription.js"
  25. */
  26. /**
  27. * @param {import("express").Request} req
  28. * @param {import("express").Response} res
  29. * @returns {Promise<void>}
  30. */
  31. async function removeUserFromGroup(req, res) {
  32. const subscription = req.entity
  33. const userToRemoveId = req.params.user_id
  34. const loggedInUserId = SessionManager.getLoggedInUserId(req.session)
  35. const subscriptionId = subscription._id
  36. logger.debug(
  37. { subscriptionId, userToRemoveId },
  38. 'removing user from group subscription'
  39. )
  40. await _removeUserFromGroup(req, res, {
  41. userToRemoveId,
  42. loggedInUserId,
  43. subscription,
  44. })
  45. }
  46. /**
  47. * @param {import("express").Request} req
  48. * @param {import("express").Response} res
  49. * @returns {Promise<void>}
  50. */
  51. async function removeSelfFromGroup(req, res) {
  52. const userToRemoveId = SessionManager.getLoggedInUserId(req.session)
  53. const subscription = await SubscriptionLocator.promises.getSubscription(
  54. req.query.subscriptionId
  55. )
  56. await _removeUserFromGroup(req, res, {
  57. userToRemoveId,
  58. loggedInUserId: userToRemoveId,
  59. subscription,
  60. })
  61. }
  62. /**
  63. * @param {import("express").Request} req
  64. * @param {import("express").Response} res
  65. * @param {string} userToRemoveId
  66. * @param {string} loggedInUserId
  67. * @param {Subscription} subscription
  68. * @returns {Promise<void>}
  69. * @private
  70. */
  71. async function _removeUserFromGroup(
  72. req,
  73. res,
  74. { userToRemoveId, loggedInUserId, subscription }
  75. ) {
  76. const subscriptionId = subscription._id
  77. const groupSSOActive = (
  78. await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription)
  79. )?.[0]
  80. if (groupSSOActive) {
  81. await Modules.promises.hooks.fire(
  82. 'unlinkUserFromGroupSSO',
  83. userToRemoveId,
  84. subscriptionId
  85. )
  86. }
  87. try {
  88. await UserAuditLogHandler.promises.addEntry(
  89. userToRemoveId,
  90. 'remove-from-group-subscription',
  91. loggedInUserId,
  92. req.ip,
  93. { subscriptionId }
  94. )
  95. } catch (auditLogError) {
  96. throw OError.tag(auditLogError, 'error adding audit log entry', {
  97. userToRemoveId,
  98. subscriptionId,
  99. })
  100. }
  101. try {
  102. await SubscriptionGroupHandler.promises.removeUserFromGroup(
  103. subscriptionId,
  104. userToRemoveId
  105. )
  106. } catch (error) {
  107. logger.err(
  108. { err: error, userToRemoveId, subscriptionId },
  109. 'error removing self from group'
  110. )
  111. return res.sendStatus(500)
  112. }
  113. res.sendStatus(200)
  114. }
  115. /**
  116. * @param {import("express").Request} req
  117. * @param {import("express").Response} res
  118. * @returns {Promise<void>}
  119. */
  120. async function addSeatsToGroupSubscription(req, res) {
  121. try {
  122. const userId = SessionManager.getLoggedInUserId(req.session)
  123. const { subscription, recurlySubscription, plan } =
  124. await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
  125. userId
  126. )
  127. await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
  128. await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges(
  129. recurlySubscription
  130. )
  131. await SubscriptionGroupHandler.promises.ensureSubscriptionIsActive(
  132. subscription
  133. )
  134. await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice(
  135. subscription
  136. )
  137. const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } =
  138. await SplitTestHandler.promises.getAssignment(
  139. req,
  140. res,
  141. 'flexible-group-licensing-for-manually-billed-subscriptions'
  142. )
  143. if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') {
  144. await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
  145. recurlySubscription,
  146. userId
  147. )
  148. } else {
  149. await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
  150. recurlySubscription
  151. )
  152. // Check if the user has missing billing details
  153. await RecurlyClient.promises.getPaymentMethod(userId)
  154. }
  155. res.render('subscriptions/add-seats', {
  156. subscriptionId: subscription._id,
  157. groupName: subscription.teamName,
  158. totalLicenses: subscription.membersLimit,
  159. isProfessional: isProfessionalGroupPlan(subscription),
  160. isCollectionMethodManual: recurlySubscription.isCollectionMethodManual,
  161. })
  162. } catch (error) {
  163. if (error instanceof MissingBillingInfoError) {
  164. return res.redirect(
  165. '/user/subscription/group/missing-billing-information'
  166. )
  167. }
  168. if (error instanceof ManuallyCollectedError) {
  169. return res.redirect(
  170. '/user/subscription/group/manually-collected-subscription'
  171. )
  172. }
  173. if (
  174. error instanceof PendingChangeError ||
  175. error instanceof InactiveError ||
  176. error instanceof HasPastDueInvoiceError
  177. ) {
  178. return res.redirect('/user/subscription')
  179. }
  180. logger.err(
  181. { error },
  182. 'error while getting users group subscription details'
  183. )
  184. return res.redirect('/user/subscription')
  185. }
  186. }
  187. /**
  188. * @param {import("express").Request} req
  189. * @param {import("express").Response} res
  190. * @returns {Promise<void>}
  191. */
  192. async function previewAddSeatsSubscriptionChange(req, res) {
  193. try {
  194. const userId = SessionManager.getLoggedInUserId(req.session)
  195. const preview =
  196. await SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange(
  197. userId,
  198. req.body.adding
  199. )
  200. res.json(preview)
  201. } catch (error) {
  202. if (
  203. error instanceof MissingBillingInfoError ||
  204. error instanceof ManuallyCollectedError ||
  205. error instanceof PendingChangeError ||
  206. error instanceof InactiveError ||
  207. error instanceof HasPastDueInvoiceError
  208. ) {
  209. return res.status(422).end()
  210. }
  211. if (error instanceof SubtotalLimitExceededError) {
  212. return res.status(422).json({
  213. code: 'subtotal_limit_exceeded',
  214. adding: req.body.adding,
  215. })
  216. }
  217. logger.err(
  218. { error },
  219. 'error trying to preview "add seats" subscription change'
  220. )
  221. return res.status(500).end()
  222. }
  223. }
  224. /**
  225. * @param {import("express").Request} req
  226. * @param {import("express").Response} res
  227. * @returns {Promise<void>}
  228. */
  229. async function createAddSeatsSubscriptionChange(req, res) {
  230. try {
  231. const userId = SessionManager.getLoggedInUserId(req.session)
  232. const create =
  233. await SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange(
  234. userId,
  235. req.body.adding,
  236. req.body.poNumber
  237. )
  238. res.json(create)
  239. } catch (error) {
  240. if (
  241. error instanceof MissingBillingInfoError ||
  242. error instanceof ManuallyCollectedError ||
  243. error instanceof PendingChangeError ||
  244. error instanceof InactiveError ||
  245. error instanceof HasPastDueInvoiceError
  246. ) {
  247. return res.status(422).end()
  248. }
  249. if (error instanceof SubtotalLimitExceededError) {
  250. return res.status(422).json({
  251. code: 'subtotal_limit_exceeded',
  252. adding: req.body.adding,
  253. })
  254. }
  255. logger.err(
  256. { error },
  257. 'error trying to create "add seats" subscription change'
  258. )
  259. return res.status(500).end()
  260. }
  261. }
  262. async function submitForm(req, res) {
  263. const userId = SessionManager.getLoggedInUserId(req.session)
  264. const userEmail = await UserGetter.promises.getUserEmail(userId)
  265. const { adding, poNumber } = req.body
  266. const { recurlySubscription } =
  267. await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
  268. userId
  269. )
  270. if (recurlySubscription.isCollectionMethodManual) {
  271. await SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
  272. userId,
  273. recurlySubscription,
  274. poNumber
  275. )
  276. }
  277. const messageLines = [`\n**Overleaf Sales Contact Form:**`]
  278. messageLines.push('**Subject:** Self-Serve Group User Increase Request')
  279. messageLines.push(`**Estimated Number of Users:** ${adding}`)
  280. if (poNumber) {
  281. messageLines.push(`**PO Number:** ${poNumber}`)
  282. }
  283. messageLines.push(
  284. `**Message:** This email has been generated on behalf of user with email **${userEmail}** ` +
  285. 'to request an increase in the total number of users for their subscription.'
  286. )
  287. const messageFormatted = messageLines.join('\n\n')
  288. const data = {
  289. email: userEmail,
  290. subject: 'Sales Contact Form',
  291. message: messageFormatted,
  292. inbox: 'sales',
  293. }
  294. await Modules.promises.hooks.fire('sendSupportRequest', data)
  295. res.sendStatus(204)
  296. }
  297. async function subscriptionUpgradePage(req, res) {
  298. try {
  299. const userId = SessionManager.getLoggedInUserId(req.session)
  300. const changePreview =
  301. await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId)
  302. const olSubscription = await Subscription.findOne({
  303. admin_id: userId,
  304. }).exec()
  305. res.render('subscriptions/upgrade-group-subscription-react', {
  306. changePreview,
  307. totalLicenses: olSubscription.membersLimit,
  308. groupName: olSubscription.teamName,
  309. })
  310. } catch (error) {
  311. if (error instanceof MissingBillingInfoError) {
  312. return res.redirect(
  313. '/user/subscription/group/missing-billing-information'
  314. )
  315. }
  316. if (error instanceof ManuallyCollectedError) {
  317. return res.redirect(
  318. '/user/subscription/group/manually-collected-subscription'
  319. )
  320. }
  321. if (error instanceof SubtotalLimitExceededError) {
  322. return res.redirect('/user/subscription/group/subtotal-limit-exceeded')
  323. }
  324. if (error instanceof PendingChangeError || error instanceof InactiveError) {
  325. return res.redirect('/user/subscription')
  326. }
  327. logger.err({ error }, 'error loading upgrade subscription page')
  328. return res.redirect('/user/subscription')
  329. }
  330. }
  331. async function upgradeSubscription(req, res) {
  332. try {
  333. const userId = SessionManager.getLoggedInUserId(req.session)
  334. await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
  335. return res.sendStatus(200)
  336. } catch (error) {
  337. logger.err({ error }, 'error trying to upgrade subscription')
  338. return res.sendStatus(500)
  339. }
  340. }
  341. async function missingBillingInformation(req, res) {
  342. try {
  343. const userId = SessionManager.getLoggedInUserId(req.session)
  344. const subscription =
  345. await SubscriptionLocator.promises.getUsersSubscription(userId)
  346. res.render('subscriptions/missing-billing-information', {
  347. groupName: subscription.teamName,
  348. })
  349. } catch (error) {
  350. logger.err(
  351. { error },
  352. 'error trying to render missing billing information page'
  353. )
  354. return res.render('/user/subscription')
  355. }
  356. }
  357. async function manuallyCollectedSubscription(req, res) {
  358. try {
  359. const userId = SessionManager.getLoggedInUserId(req.session)
  360. const subscription =
  361. await SubscriptionLocator.promises.getUsersSubscription(userId)
  362. await SplitTestHandler.promises.getAssignment(
  363. req,
  364. res,
  365. 'flexible-group-licensing-for-manually-billed-subscriptions'
  366. )
  367. res.render('subscriptions/manually-collected-subscription', {
  368. groupName: subscription.teamName,
  369. })
  370. } catch (error) {
  371. logger.err(
  372. { error },
  373. 'error trying to render manually collected subscription page'
  374. )
  375. return res.render('/user/subscription')
  376. }
  377. }
  378. async function subtotalLimitExceeded(req, res) {
  379. try {
  380. const userId = SessionManager.getLoggedInUserId(req.session)
  381. const subscription =
  382. await SubscriptionLocator.promises.getUsersSubscription(userId)
  383. res.render('subscriptions/subtotal-limit-exceeded', {
  384. groupName: subscription.teamName,
  385. })
  386. } catch (error) {
  387. logger.err({ error }, 'error trying to render subtotal limit exceeded page')
  388. return res.render('/user/subscription')
  389. }
  390. }
  391. export default {
  392. removeUserFromGroup: expressify(removeUserFromGroup),
  393. removeSelfFromGroup: expressify(removeSelfFromGroup),
  394. addSeatsToGroupSubscription: expressify(addSeatsToGroupSubscription),
  395. submitForm: expressify(submitForm),
  396. previewAddSeatsSubscriptionChange: expressify(
  397. previewAddSeatsSubscriptionChange
  398. ),
  399. createAddSeatsSubscriptionChange: expressify(
  400. createAddSeatsSubscriptionChange
  401. ),
  402. subscriptionUpgradePage: expressify(subscriptionUpgradePage),
  403. upgradeSubscription: expressify(upgradeSubscription),
  404. missingBillingInformation: expressify(missingBillingInformation),
  405. manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
  406. subtotalLimitExceeded: expressify(subtotalLimitExceeded),
  407. }