| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- // ts-check
- import SubscriptionGroupHandler from './SubscriptionGroupHandler.js'
- import OError from '@overleaf/o-error'
- import logger from '@overleaf/logger'
- import SubscriptionLocator from './SubscriptionLocator.js'
- import SessionManager from '../Authentication/SessionManager.js'
- import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
- import { expressify } from '@overleaf/promise-utils'
- import Modules from '../../infrastructure/Modules.js'
- import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
- import UserGetter from '../User/UserGetter.js'
- import { Subscription } from '../../models/Subscription.js'
- import { isProfessionalGroupPlan } from './PlansHelper.mjs'
- import {
- MissingBillingInfoError,
- ManuallyCollectedError,
- PendingChangeError,
- InactiveError,
- SubtotalLimitExceededError,
- HasPastDueInvoiceError,
- HasNoAdditionalLicenseWhenManuallyCollectedError,
- } from './Errors.js'
- /**
- * @import { Subscription } from "../../../../types/subscription/dashboard/subscription.js"
- */
- /**
- * @param {import("express").Request} req
- * @param {import("express").Response} res
- * @returns {Promise<void>}
- */
- async function removeUserFromGroup(req, res) {
- const subscription = req.entity
- const userToRemoveId = req.params.user_id
- const loggedInUserId = SessionManager.getLoggedInUserId(req.session)
- const subscriptionId = subscription._id
- logger.debug(
- { subscriptionId, userToRemoveId },
- 'removing user from group subscription'
- )
- await _removeUserFromGroup(req, res, {
- userToRemoveId,
- loggedInUserId,
- subscription,
- })
- }
- /**
- * @param {import("express").Request} req
- * @param {import("express").Response} res
- * @returns {Promise<void>}
- */
- async function removeSelfFromGroup(req, res) {
- const userToRemoveId = SessionManager.getLoggedInUserId(req.session)
- const subscription = await SubscriptionLocator.promises.getSubscription(
- req.query.subscriptionId
- )
- await _removeUserFromGroup(req, res, {
- userToRemoveId,
- loggedInUserId: userToRemoveId,
- subscription,
- })
- }
- /**
- * @param {import("express").Request} req
- * @param {import("express").Response} res
- * @param {string} userToRemoveId
- * @param {string} loggedInUserId
- * @param {Subscription} subscription
- * @returns {Promise<void>}
- * @private
- */
- async function _removeUserFromGroup(
- req,
- res,
- { userToRemoveId, loggedInUserId, subscription }
- ) {
- const subscriptionId = subscription._id
- const groupSSOActive = (
- await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription)
- )?.[0]
- if (groupSSOActive) {
- await Modules.promises.hooks.fire(
- 'unlinkUserFromGroupSSO',
- userToRemoveId,
- subscriptionId
- )
- }
- try {
- await UserAuditLogHandler.promises.addEntry(
- userToRemoveId,
- 'remove-from-group-subscription',
- loggedInUserId,
- req.ip,
- { subscriptionId }
- )
- } catch (auditLogError) {
- throw OError.tag(auditLogError, 'error adding audit log entry', {
- userToRemoveId,
- subscriptionId,
- })
- }
- const groupAuditLog = {
- initiatorId: loggedInUserId,
- ipAddress: req.ip,
- }
- try {
- await SubscriptionGroupHandler.promises.removeUserFromGroup(
- subscriptionId,
- userToRemoveId,
- groupAuditLog
- )
- } catch (error) {
- logger.err(
- { err: error, userToRemoveId, subscriptionId },
- 'error removing self from group'
- )
- return res.sendStatus(500)
- }
- res.sendStatus(200)
- }
- /**
- * @param {import("express").Request} req
- * @param {import("express").Response} res
- * @returns {Promise<void>}
- */
- async function addSeatsToGroupSubscription(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const { subscription, paymentProviderSubscription, plan } =
- await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
- userId
- )
- await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
- await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges(
- paymentProviderSubscription
- )
- await SubscriptionGroupHandler.promises.ensureSubscriptionIsActive(
- subscription
- )
- await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice(
- subscription
- )
- const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } =
- await SplitTestHandler.promises.getAssignment(
- req,
- res,
- 'flexible-group-licensing-for-manually-billed-subscriptions'
- )
- if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') {
- await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
- paymentProviderSubscription,
- userId
- )
- await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
- paymentProviderSubscription
- )
- } else {
- await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
- paymentProviderSubscription
- )
- // Check if the user has missing billing details
- await Modules.promises.hooks.fire('getPaymentMethod', userId)
- }
- res.render('subscriptions/add-seats', {
- subscriptionId: subscription._id,
- groupName: subscription.teamName,
- totalLicenses: subscription.membersLimit,
- isProfessional: isProfessionalGroupPlan(subscription),
- isCollectionMethodManual:
- paymentProviderSubscription.isCollectionMethodManual,
- })
- } catch (error) {
- if (error instanceof MissingBillingInfoError) {
- return res.redirect(
- '/user/subscription/group/missing-billing-information'
- )
- }
- if (
- error instanceof ManuallyCollectedError ||
- error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
- ) {
- return res.redirect(
- '/user/subscription/group/manually-collected-subscription'
- )
- }
- if (
- error instanceof PendingChangeError ||
- error instanceof InactiveError ||
- error instanceof HasPastDueInvoiceError
- ) {
- return res.redirect('/user/subscription')
- }
- logger.err(
- { error },
- 'error while getting users group subscription details'
- )
- return res.redirect('/user/subscription')
- }
- }
- /**
- * @param {import("express").Request} req
- * @param {import("express").Response} res
- * @returns {Promise<void>}
- */
- async function previewAddSeatsSubscriptionChange(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const preview =
- await SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange(
- userId,
- req.body.adding
- )
- res.json(preview)
- } catch (error) {
- if (
- error instanceof MissingBillingInfoError ||
- error instanceof ManuallyCollectedError ||
- error instanceof PendingChangeError ||
- error instanceof InactiveError ||
- error instanceof HasPastDueInvoiceError ||
- error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
- ) {
- return res.status(422).end()
- }
- if (error instanceof SubtotalLimitExceededError) {
- return res.status(422).json({
- code: 'subtotal_limit_exceeded',
- adding: req.body.adding,
- })
- }
- logger.err(
- { error },
- 'error trying to preview "add seats" subscription change'
- )
- return res.status(500).end()
- }
- }
- /**
- * @param {import("express").Request} req
- * @param {import("express").Response} res
- * @returns {Promise<void>}
- */
- async function createAddSeatsSubscriptionChange(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const create =
- await SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange(
- userId,
- req.body.adding,
- req.body.poNumber
- )
- res.json(create)
- } catch (error) {
- if (
- error instanceof MissingBillingInfoError ||
- error instanceof ManuallyCollectedError ||
- error instanceof PendingChangeError ||
- error instanceof InactiveError ||
- error instanceof HasPastDueInvoiceError ||
- error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
- ) {
- return res.status(422).end()
- }
- if (error instanceof SubtotalLimitExceededError) {
- return res.status(422).json({
- code: 'subtotal_limit_exceeded',
- adding: req.body.adding,
- })
- }
- logger.err(
- { error },
- 'error trying to create "add seats" subscription change'
- )
- return res.status(500).end()
- }
- }
- async function submitForm(req, res) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const userEmail = await UserGetter.promises.getUserEmail(userId)
- const { adding, poNumber } = req.body
- const { paymentProviderSubscription } =
- await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
- userId
- )
- if (paymentProviderSubscription.isCollectionMethodManual) {
- await SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms(
- userId,
- paymentProviderSubscription,
- poNumber
- )
- }
- const messageLines = [`\n**Overleaf Sales Contact Form:**`]
- messageLines.push('**Subject:** Self-Serve Group User Increase Request')
- messageLines.push(`**Estimated Number of Users:** ${adding}`)
- if (poNumber) {
- messageLines.push(`**PO Number:** ${poNumber}`)
- }
- messageLines.push(
- `**Message:** This email has been generated on behalf of user with email **${userEmail}** ` +
- 'to request an increase in the total number of users for their subscription.'
- )
- const messageFormatted = messageLines.join('\n\n')
- const data = {
- email: userEmail,
- subject: 'Sales Contact Form',
- message: messageFormatted,
- inbox: 'sales',
- }
- await Modules.promises.hooks.fire('sendSupportRequest', data)
- res.sendStatus(204)
- }
- async function subscriptionUpgradePage(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const changePreview =
- await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId)
- const olSubscription = await Subscription.findOne({
- admin_id: userId,
- }).exec()
- res.render('subscriptions/upgrade-group-subscription-react', {
- changePreview,
- totalLicenses: olSubscription.membersLimit,
- groupName: olSubscription.teamName,
- })
- } catch (error) {
- if (error instanceof MissingBillingInfoError) {
- return res.redirect(
- '/user/subscription/group/missing-billing-information'
- )
- }
- if (error instanceof ManuallyCollectedError) {
- return res.redirect(
- '/user/subscription/group/manually-collected-subscription'
- )
- }
- if (error instanceof SubtotalLimitExceededError) {
- return res.redirect('/user/subscription/group/subtotal-limit-exceeded')
- }
- if (error instanceof PendingChangeError || error instanceof InactiveError) {
- return res.redirect('/user/subscription')
- }
- logger.err({ error }, 'error loading upgrade subscription page')
- return res.redirect('/user/subscription')
- }
- }
- async function upgradeSubscription(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
- return res.sendStatus(200)
- } catch (error) {
- logger.err({ error }, 'error trying to upgrade subscription')
- return res.sendStatus(500)
- }
- }
- async function missingBillingInformation(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const subscription =
- await SubscriptionLocator.promises.getUsersSubscription(userId)
- res.render('subscriptions/missing-billing-information', {
- groupName: subscription.teamName,
- })
- } catch (error) {
- logger.err(
- { error },
- 'error trying to render missing billing information page'
- )
- return res.render('/user/subscription')
- }
- }
- async function manuallyCollectedSubscription(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const subscription =
- await SubscriptionLocator.promises.getUsersSubscription(userId)
- await SplitTestHandler.promises.getAssignment(
- req,
- res,
- 'flexible-group-licensing-for-manually-billed-subscriptions'
- )
- res.render('subscriptions/manually-collected-subscription', {
- groupName: subscription.teamName,
- })
- } catch (error) {
- logger.err(
- { error },
- 'error trying to render manually collected subscription page'
- )
- return res.render('/user/subscription')
- }
- }
- async function subtotalLimitExceeded(req, res) {
- try {
- const userId = SessionManager.getLoggedInUserId(req.session)
- const subscription =
- await SubscriptionLocator.promises.getUsersSubscription(userId)
- res.render('subscriptions/subtotal-limit-exceeded', {
- groupName: subscription.teamName,
- })
- } catch (error) {
- logger.err({ error }, 'error trying to render subtotal limit exceeded page')
- return res.render('/user/subscription')
- }
- }
- export default {
- removeUserFromGroup: expressify(removeUserFromGroup),
- removeSelfFromGroup: expressify(removeSelfFromGroup),
- addSeatsToGroupSubscription: expressify(addSeatsToGroupSubscription),
- submitForm: expressify(submitForm),
- previewAddSeatsSubscriptionChange: expressify(
- previewAddSeatsSubscriptionChange
- ),
- createAddSeatsSubscriptionChange: expressify(
- createAddSeatsSubscriptionChange
- ),
- subscriptionUpgradePage: expressify(subscriptionUpgradePage),
- upgradeSubscription: expressify(upgradeSubscription),
- missingBillingInformation: expressify(missingBillingInformation),
- manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
- subtotalLimitExceeded: expressify(subtotalLimitExceeded),
- }
|