SubscriptionGroupController.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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, parseReq } from '../../infrastructure/Validation.mjs'
  12. import { isProfessionalGroupPlan } from './PlansHelper.mjs'
  13. import {
  14. MissingBillingInfoError,
  15. ManuallyCollectedError,
  16. InactiveError,
  17. SubtotalLimitExceededError,
  18. HasPastDueInvoiceError,
  19. HasNoAdditionalLicenseWhenManuallyCollectedError,
  20. PaymentActionRequiredError,
  21. MultiplePendingChangesError,
  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.ensureSubscriptionIsActive(
  136. subscription
  137. )
  138. await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice(
  139. subscription
  140. )
  141. await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
  142. paymentProviderSubscription,
  143. userId
  144. )
  145. await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
  146. paymentProviderSubscription
  147. )
  148. res.render('subscriptions/add-seats', {
  149. subscriptionId: subscription._id,
  150. groupName: subscription.teamName,
  151. totalLicenses: subscription.membersLimit,
  152. isProfessional: isProfessionalGroupPlan(subscription),
  153. isCollectionMethodManual:
  154. paymentProviderSubscription.isCollectionMethodManual,
  155. redirectedPaymentErrorCode: req.query.errorCode,
  156. })
  157. } catch (error) {
  158. if (error instanceof MissingBillingInfoError) {
  159. return res.redirect(
  160. '/user/subscription/group/missing-billing-information'
  161. )
  162. }
  163. if (error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError) {
  164. return res.redirect(
  165. '/user/subscription/group/manually-collected-subscription?error_type=no-additional-license'
  166. )
  167. }
  168. if (
  169. error instanceof InactiveError ||
  170. error instanceof HasPastDueInvoiceError
  171. ) {
  172. return res.redirect('/user/subscription')
  173. }
  174. logger.err(
  175. { error },
  176. 'error while getting users group subscription details'
  177. )
  178. return res.redirect('/user/subscription')
  179. }
  180. }
  181. const previewAddSeatsSubscriptionChangeSchema = z.object({
  182. body: z.object({
  183. adding: z.number().int().min(1).max(MAX_NUMBER_OF_USERS),
  184. poNumber: z.string().max(MAX_NUMBER_OF_PO_NUMBER_CHARACTERS).optional(),
  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. const { body } = parseReq(req, previewAddSeatsSubscriptionChangeSchema)
  194. try {
  195. const userId = SessionManager.getLoggedInUserId(req.session)
  196. const preview =
  197. await SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange(
  198. userId,
  199. body.adding
  200. )
  201. res.json(preview)
  202. } catch (error) {
  203. if (
  204. error instanceof MissingBillingInfoError ||
  205. error instanceof InactiveError ||
  206. error instanceof HasPastDueInvoiceError ||
  207. error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
  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: 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 MultiplePendingChangesError ||
  243. error instanceof InactiveError ||
  244. error instanceof HasPastDueInvoiceError ||
  245. error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
  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. if (error instanceof PaymentActionRequiredError) {
  256. return res.status(402).json({
  257. message: 'Payment action required',
  258. clientSecret: error.info.clientSecret,
  259. publicKey: error.info.publicKey,
  260. })
  261. }
  262. logger.err(
  263. { error },
  264. 'error trying to create "add seats" subscription change'
  265. )
  266. return res.status(500).end()
  267. }
  268. }
  269. const submitFormSchema = z.object({
  270. body: z.object({
  271. adding: z.coerce.number().int().min(MAX_NUMBER_OF_USERS),
  272. poNumber: z.string().optional(),
  273. }),
  274. })
  275. async function submitForm(req, res) {
  276. const { body } = parseReq(req, submitFormSchema)
  277. const { adding, poNumber } = body
  278. const userId = SessionManager.getLoggedInUserId(req.session)
  279. const userEmail = await UserGetter.promises.getUserEmail(userId)
  280. const { paymentProviderSubscription } =
  281. await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
  282. userId
  283. )
  284. if (paymentProviderSubscription.isCollectionMethodManual) {
  285. await SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
  286. paymentProviderSubscription,
  287. poNumber
  288. )
  289. }
  290. const messageLines = [`\n**Overleaf Sales Contact Form:**`]
  291. messageLines.push('**Subject:** Self-Serve Group User Increase Request')
  292. messageLines.push(`**Estimated Number of Users:** ${adding}`)
  293. if (poNumber) {
  294. messageLines.push(`**PO Number:** ${poNumber}`)
  295. }
  296. messageLines.push(
  297. `**Message:** This email has been generated on behalf of user with email **${userEmail}** ` +
  298. 'to request an increase in the total number of users for their subscription.'
  299. )
  300. const messageFormatted = messageLines.join('\n\n')
  301. const data = {
  302. email: userEmail,
  303. subject: 'Sales Contact Form',
  304. message: messageFormatted,
  305. inbox: 'sales',
  306. }
  307. await Modules.promises.hooks.fire('sendSupportRequest', data)
  308. res.sendStatus(204)
  309. }
  310. async function subscriptionUpgradePage(req, res) {
  311. try {
  312. const userId = SessionManager.getLoggedInUserId(req.session)
  313. const changePreview =
  314. await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId)
  315. const olSubscription = await Subscription.findOne({
  316. admin_id: userId,
  317. }).exec()
  318. res.render('subscriptions/upgrade-group-subscription-react', {
  319. changePreview,
  320. totalLicenses: olSubscription.membersLimit,
  321. groupName: olSubscription.teamName,
  322. redirectedPaymentErrorCode: req.query.errorCode,
  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?error_type=plan-upgrade'
  333. )
  334. }
  335. if (error instanceof SubtotalLimitExceededError) {
  336. return res.redirect('/user/subscription/group/subtotal-limit-exceeded')
  337. }
  338. if (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. if (error instanceof PaymentActionRequiredError) {
  352. return res.status(402).json({
  353. message: 'Payment action required',
  354. clientSecret: error.info.clientSecret,
  355. publicKey: error.info.publicKey,
  356. })
  357. }
  358. if (error instanceof MultiplePendingChangesError) {
  359. return res.status(422).json({
  360. code: 'multiple_pending_changes',
  361. message:
  362. 'Cannot upgrade subscription while there are multiple pending subscription changes. Please contact support.',
  363. })
  364. }
  365. logger.err({ error }, 'error trying to upgrade subscription')
  366. return res.sendStatus(500)
  367. }
  368. }
  369. async function missingBillingInformation(req, res) {
  370. try {
  371. const userId = SessionManager.getLoggedInUserId(req.session)
  372. const subscription =
  373. await SubscriptionLocator.promises.getUsersSubscription(userId)
  374. res.render('subscriptions/missing-billing-information', {
  375. groupName: subscription.teamName,
  376. })
  377. } catch (error) {
  378. logger.err(
  379. { error },
  380. 'error trying to render missing billing information page'
  381. )
  382. return res.redirect('/user/subscription')
  383. }
  384. }
  385. async function manuallyCollectedSubscription(req, res) {
  386. try {
  387. const userId = SessionManager.getLoggedInUserId(req.session)
  388. const subscription =
  389. await SubscriptionLocator.promises.getUsersSubscription(userId)
  390. res.render('subscriptions/manually-collected-subscription', {
  391. groupName: subscription.teamName,
  392. errorType: req.query.error_type,
  393. })
  394. } catch (error) {
  395. logger.err(
  396. { error },
  397. 'error trying to render manually collected subscription page'
  398. )
  399. return res.redirect('/user/subscription')
  400. }
  401. }
  402. async function subtotalLimitExceeded(req, res) {
  403. try {
  404. const userId = SessionManager.getLoggedInUserId(req.session)
  405. const subscription =
  406. await SubscriptionLocator.promises.getUsersSubscription(userId)
  407. res.render('subscriptions/subtotal-limit-exceeded', {
  408. groupName: subscription.teamName,
  409. })
  410. } catch (error) {
  411. logger.err({ error }, 'error trying to render subtotal limit exceeded page')
  412. return res.redirect('/user/subscription')
  413. }
  414. }
  415. async function getGroupPlanPerUserPrices(req, res) {
  416. try {
  417. const userId = SessionManager.getLoggedInUserId(req.session)
  418. const prices = await Modules.promises.hooks.fire(
  419. 'getGroupPlanPerUserPrices',
  420. userId,
  421. req.query.currency
  422. )
  423. return res.json(prices[0])
  424. } catch (error) {
  425. logger.err({ error }, 'error trying to get websale group product prices')
  426. return res.sendStatus(500)
  427. }
  428. }
  429. export default {
  430. removeUserFromGroup: expressify(removeUserFromGroup),
  431. removeSelfFromGroup: expressify(removeSelfFromGroup),
  432. addSeatsToGroupSubscription: expressify(addSeatsToGroupSubscription),
  433. submitForm: expressify(submitForm),
  434. previewAddSeatsSubscriptionChange: expressify(
  435. previewAddSeatsSubscriptionChange
  436. ),
  437. createAddSeatsSubscriptionChange: expressify(
  438. createAddSeatsSubscriptionChange
  439. ),
  440. subscriptionUpgradePage: expressify(subscriptionUpgradePage),
  441. upgradeSubscription: expressify(upgradeSubscription),
  442. missingBillingInformation: expressify(missingBillingInformation),
  443. manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
  444. subtotalLimitExceeded: expressify(subtotalLimitExceeded),
  445. getGroupPlanPerUserPrices: expressify(getGroupPlanPerUserPrices),
  446. }