// 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, } from './Errors.js' import RecurlyClient from './RecurlyClient.js' /** * @import { Subscription } from "../../../../types/subscription/dashboard/subscription.js" */ /** * @param {import("express").Request} req * @param {import("express").Response} res * @returns {Promise} */ 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} */ 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} * @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, }) } try { await SubscriptionGroupHandler.promises.removeUserFromGroup( subscriptionId, userToRemoveId ) } 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} */ async function addSeatsToGroupSubscription(req, res) { try { const userId = SessionManager.getLoggedInUserId(req.session) const { subscription, recurlySubscription, plan } = await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails( userId ) await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan) await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges( recurlySubscription ) await SubscriptionGroupHandler.promises.ensureSubscriptionIsActive( subscription ) const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } = await SplitTestHandler.promises.getAssignment( req, res, 'flexible-group-licensing-for-manually-billed-subscriptions' ) if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') { await SubscriptionGroupHandler.promises.checkBillingInfoExistence( recurlySubscription, userId ) } else { await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual( recurlySubscription ) // Check if the user has missing billing details await RecurlyClient.promises.getPaymentMethod(userId) } res.render('subscriptions/add-seats', { subscriptionId: subscription._id, groupName: subscription.teamName, totalLicenses: subscription.membersLimit, isProfessional: isProfessionalGroupPlan(subscription), isCollectionMethodManual: recurlySubscription.isCollectionMethodManual, }) } 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 PendingChangeError || error instanceof InactiveError) { 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} */ 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 ) { 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} */ 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 ) { 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 { recurlySubscription } = await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails( userId ) if (recurlySubscription.isCollectionMethodManual) { await SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms( userId, recurlySubscription, 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) 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), }