SubscriptionGroupController.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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. HasNoAdditionalLicenseWhenManuallyCollectedError,
  22. } from './Errors.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. const groupAuditLog = {
  102. initiatorId: loggedInUserId,
  103. ipAddress: req.ip,
  104. }
  105. try {
  106. await SubscriptionGroupHandler.promises.removeUserFromGroup(
  107. subscriptionId,
  108. userToRemoveId,
  109. groupAuditLog
  110. )
  111. } catch (error) {
  112. logger.err(
  113. { err: error, userToRemoveId, subscriptionId },
  114. 'error removing self from group'
  115. )
  116. return res.sendStatus(500)
  117. }
  118. res.sendStatus(200)
  119. }
  120. /**
  121. * @param {import("express").Request} req
  122. * @param {import("express").Response} res
  123. * @returns {Promise<void>}
  124. */
  125. async function addSeatsToGroupSubscription(req, res) {
  126. try {
  127. const userId = SessionManager.getLoggedInUserId(req.session)
  128. const { subscription, paymentProviderSubscription, plan } =
  129. await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
  130. userId
  131. )
  132. await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
  133. await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges(
  134. paymentProviderSubscription
  135. )
  136. await SubscriptionGroupHandler.promises.ensureSubscriptionIsActive(
  137. subscription
  138. )
  139. await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice(
  140. subscription
  141. )
  142. const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } =
  143. await SplitTestHandler.promises.getAssignment(
  144. req,
  145. res,
  146. 'flexible-group-licensing-for-manually-billed-subscriptions'
  147. )
  148. if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') {
  149. await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
  150. paymentProviderSubscription,
  151. userId
  152. )
  153. await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
  154. paymentProviderSubscription
  155. )
  156. } else {
  157. await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
  158. paymentProviderSubscription
  159. )
  160. // Check if the user has missing billing details
  161. await Modules.promises.hooks.fire('getPaymentMethod', userId)
  162. }
  163. res.render('subscriptions/add-seats', {
  164. subscriptionId: subscription._id,
  165. groupName: subscription.teamName,
  166. totalLicenses: subscription.membersLimit,
  167. isProfessional: isProfessionalGroupPlan(subscription),
  168. isCollectionMethodManual:
  169. paymentProviderSubscription.isCollectionMethodManual,
  170. })
  171. } catch (error) {
  172. if (error instanceof MissingBillingInfoError) {
  173. return res.redirect(
  174. '/user/subscription/group/missing-billing-information'
  175. )
  176. }
  177. if (
  178. error instanceof ManuallyCollectedError ||
  179. error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
  180. ) {
  181. return res.redirect(
  182. '/user/subscription/group/manually-collected-subscription'
  183. )
  184. }
  185. if (
  186. error instanceof PendingChangeError ||
  187. error instanceof InactiveError ||
  188. error instanceof HasPastDueInvoiceError
  189. ) {
  190. return res.redirect('/user/subscription')
  191. }
  192. logger.err(
  193. { error },
  194. 'error while getting users group subscription details'
  195. )
  196. return res.redirect('/user/subscription')
  197. }
  198. }
  199. /**
  200. * @param {import("express").Request} req
  201. * @param {import("express").Response} res
  202. * @returns {Promise<void>}
  203. */
  204. async function previewAddSeatsSubscriptionChange(req, res) {
  205. try {
  206. const userId = SessionManager.getLoggedInUserId(req.session)
  207. const preview =
  208. await SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange(
  209. userId,
  210. req.body.adding
  211. )
  212. res.json(preview)
  213. } catch (error) {
  214. if (
  215. error instanceof MissingBillingInfoError ||
  216. error instanceof ManuallyCollectedError ||
  217. error instanceof PendingChangeError ||
  218. error instanceof InactiveError ||
  219. error instanceof HasPastDueInvoiceError ||
  220. error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
  221. ) {
  222. return res.status(422).end()
  223. }
  224. if (error instanceof SubtotalLimitExceededError) {
  225. return res.status(422).json({
  226. code: 'subtotal_limit_exceeded',
  227. adding: req.body.adding,
  228. })
  229. }
  230. logger.err(
  231. { error },
  232. 'error trying to preview "add seats" subscription change'
  233. )
  234. return res.status(500).end()
  235. }
  236. }
  237. /**
  238. * @param {import("express").Request} req
  239. * @param {import("express").Response} res
  240. * @returns {Promise<void>}
  241. */
  242. async function createAddSeatsSubscriptionChange(req, res) {
  243. try {
  244. const userId = SessionManager.getLoggedInUserId(req.session)
  245. const create =
  246. await SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange(
  247. userId,
  248. req.body.adding,
  249. req.body.poNumber
  250. )
  251. res.json(create)
  252. } catch (error) {
  253. if (
  254. error instanceof MissingBillingInfoError ||
  255. error instanceof ManuallyCollectedError ||
  256. error instanceof PendingChangeError ||
  257. error instanceof InactiveError ||
  258. error instanceof HasPastDueInvoiceError ||
  259. error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
  260. ) {
  261. return res.status(422).end()
  262. }
  263. if (error instanceof SubtotalLimitExceededError) {
  264. return res.status(422).json({
  265. code: 'subtotal_limit_exceeded',
  266. adding: req.body.adding,
  267. })
  268. }
  269. logger.err(
  270. { error },
  271. 'error trying to create "add seats" subscription change'
  272. )
  273. return res.status(500).end()
  274. }
  275. }
  276. async function submitForm(req, res) {
  277. const userId = SessionManager.getLoggedInUserId(req.session)
  278. const userEmail = await UserGetter.promises.getUserEmail(userId)
  279. const { adding, poNumber } = req.body
  280. const { paymentProviderSubscription } =
  281. await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
  282. userId
  283. )
  284. if (paymentProviderSubscription.isCollectionMethodManual) {
  285. await SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
  286. userId,
  287. paymentProviderSubscription,
  288. poNumber
  289. )
  290. }
  291. const messageLines = [`\n**Overleaf Sales Contact Form:**`]
  292. messageLines.push('**Subject:** Self-Serve Group User Increase Request')
  293. messageLines.push(`**Estimated Number of Users:** ${adding}`)
  294. if (poNumber) {
  295. messageLines.push(`**PO Number:** ${poNumber}`)
  296. }
  297. messageLines.push(
  298. `**Message:** This email has been generated on behalf of user with email **${userEmail}** ` +
  299. 'to request an increase in the total number of users for their subscription.'
  300. )
  301. const messageFormatted = messageLines.join('\n\n')
  302. const data = {
  303. email: userEmail,
  304. subject: 'Sales Contact Form',
  305. message: messageFormatted,
  306. inbox: 'sales',
  307. }
  308. await Modules.promises.hooks.fire('sendSupportRequest', data)
  309. res.sendStatus(204)
  310. }
  311. async function subscriptionUpgradePage(req, res) {
  312. try {
  313. const userId = SessionManager.getLoggedInUserId(req.session)
  314. const changePreview =
  315. await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId)
  316. const olSubscription = await Subscription.findOne({
  317. admin_id: userId,
  318. }).exec()
  319. res.render('subscriptions/upgrade-group-subscription-react', {
  320. changePreview,
  321. totalLicenses: olSubscription.membersLimit,
  322. groupName: olSubscription.teamName,
  323. })
  324. } catch (error) {
  325. if (error instanceof MissingBillingInfoError) {
  326. return res.redirect(
  327. '/user/subscription/group/missing-billing-information'
  328. )
  329. }
  330. if (error instanceof ManuallyCollectedError) {
  331. return res.redirect(
  332. '/user/subscription/group/manually-collected-subscription'
  333. )
  334. }
  335. if (error instanceof SubtotalLimitExceededError) {
  336. return res.redirect('/user/subscription/group/subtotal-limit-exceeded')
  337. }
  338. if (error instanceof PendingChangeError || error instanceof InactiveError) {
  339. return res.redirect('/user/subscription')
  340. }
  341. logger.err({ error }, 'error loading upgrade subscription page')
  342. return res.redirect('/user/subscription')
  343. }
  344. }
  345. async function upgradeSubscription(req, res) {
  346. try {
  347. const userId = SessionManager.getLoggedInUserId(req.session)
  348. await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
  349. return res.sendStatus(200)
  350. } catch (error) {
  351. logger.err({ error }, 'error trying to upgrade subscription')
  352. return res.sendStatus(500)
  353. }
  354. }
  355. async function missingBillingInformation(req, res) {
  356. try {
  357. const userId = SessionManager.getLoggedInUserId(req.session)
  358. const subscription =
  359. await SubscriptionLocator.promises.getUsersSubscription(userId)
  360. res.render('subscriptions/missing-billing-information', {
  361. groupName: subscription.teamName,
  362. })
  363. } catch (error) {
  364. logger.err(
  365. { error },
  366. 'error trying to render missing billing information page'
  367. )
  368. return res.render('/user/subscription')
  369. }
  370. }
  371. async function manuallyCollectedSubscription(req, res) {
  372. try {
  373. const userId = SessionManager.getLoggedInUserId(req.session)
  374. const subscription =
  375. await SubscriptionLocator.promises.getUsersSubscription(userId)
  376. await SplitTestHandler.promises.getAssignment(
  377. req,
  378. res,
  379. 'flexible-group-licensing-for-manually-billed-subscriptions'
  380. )
  381. res.render('subscriptions/manually-collected-subscription', {
  382. groupName: subscription.teamName,
  383. })
  384. } catch (error) {
  385. logger.err(
  386. { error },
  387. 'error trying to render manually collected subscription page'
  388. )
  389. return res.render('/user/subscription')
  390. }
  391. }
  392. async function subtotalLimitExceeded(req, res) {
  393. try {
  394. const userId = SessionManager.getLoggedInUserId(req.session)
  395. const subscription =
  396. await SubscriptionLocator.promises.getUsersSubscription(userId)
  397. res.render('subscriptions/subtotal-limit-exceeded', {
  398. groupName: subscription.teamName,
  399. })
  400. } catch (error) {
  401. logger.err({ error }, 'error trying to render subtotal limit exceeded page')
  402. return res.render('/user/subscription')
  403. }
  404. }
  405. export default {
  406. removeUserFromGroup: expressify(removeUserFromGroup),
  407. removeSelfFromGroup: expressify(removeSelfFromGroup),
  408. addSeatsToGroupSubscription: expressify(addSeatsToGroupSubscription),
  409. submitForm: expressify(submitForm),
  410. previewAddSeatsSubscriptionChange: expressify(
  411. previewAddSeatsSubscriptionChange
  412. ),
  413. createAddSeatsSubscriptionChange: expressify(
  414. createAddSeatsSubscriptionChange
  415. ),
  416. subscriptionUpgradePage: expressify(subscriptionUpgradePage),
  417. upgradeSubscription: expressify(upgradeSubscription),
  418. missingBillingInformation: expressify(missingBillingInformation),
  419. manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
  420. subtotalLimitExceeded: expressify(subtotalLimitExceeded),
  421. }