SubscriptionGroupController.mjs 12 KB

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