SubscriptionGroupController.mjs 14 KB

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