Files
overleaf-cep/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js
Liangjun Song 14e853a057 Merge pull request #22816 from overleaf/enable-group-plan-upgrade-for-legacy-plans
Enable group plan upgrade for legacy plans

GitOrigin-RevId: 9dde0371eeb791a6331ab50733fd457e28837ba9
2025-01-29 09:05:13 +00:00

236 lines
7.3 KiB
JavaScript

const { callbackify } = require('util')
const SubscriptionUpdater = require('./SubscriptionUpdater')
const SubscriptionLocator = require('./SubscriptionLocator')
const SubscriptionController = require('./SubscriptionController')
const { Subscription } = require('../../models/Subscription')
const SessionManager = require('../Authentication/SessionManager')
const RecurlyClient = require('./RecurlyClient')
const PlansLocator = require('./PlansLocator')
const SubscriptionHandler = require('./SubscriptionHandler')
async function removeUserFromGroup(subscriptionId, userIdToRemove) {
await SubscriptionUpdater.promises.removeUserFromGroup(
subscriptionId,
userIdToRemove
)
}
async function replaceUserReferencesInGroups(oldId, newId) {
await Subscription.updateOne({ admin_id: oldId }, { admin_id: newId }).exec()
await _replaceInArray(Subscription, 'manager_ids', oldId, newId)
await _replaceInArray(Subscription, 'member_ids', oldId, newId)
}
async function isUserPartOfGroup(userId, subscriptionId) {
const subscription =
await SubscriptionLocator.promises.getSubscriptionByMemberIdAndId(
userId,
subscriptionId
)
return !!subscription
}
async function getTotalConfirmedUsersInGroup(subscriptionId) {
const subscription =
await SubscriptionLocator.promises.getSubscription(subscriptionId)
return subscription?.member_ids?.length
}
async function _replaceInArray(model, property, oldValue, newValue) {
// Mongo won't let us pull and addToSet in the same query, so do it in
// two. Note we need to add first, since the query is based on the old user.
const query = {}
query[property] = oldValue
const setNewValue = {}
setNewValue[property] = newValue
const setOldValue = {}
setOldValue[property] = oldValue
await model.updateMany(query, { $addToSet: setNewValue })
await model.updateMany(query, { $pull: setOldValue })
}
async function ensureFlexibleLicensingEnabled(plan) {
if (!plan?.canUseFlexibleLicensing) {
throw new Error('The group plan does not support flexible licencing')
}
}
async function getUsersGroupSubscriptionDetails(req) {
const userId = SessionManager.getLoggedInUserId(req.session)
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
if (!subscription.groupPlan) {
throw new Error('User subscription is not a group plan')
}
const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
const recurlySubscription = await RecurlyClient.promises.getSubscription(
subscription.recurlySubscription_id
)
return {
subscription,
recurlySubscription,
plan,
}
}
async function _addSeatsSubscriptionChange(req) {
const adding = req.body.adding
const { recurlySubscription, plan } =
await getUsersGroupSubscriptionDetails(req)
await ensureFlexibleLicensingEnabled(plan)
const userId = SessionManager.getLoggedInUserId(req.session)
const currentAddonQuantity =
recurlySubscription.addOns.find(
addOn => addOn.code === plan.membersLimitAddOn
)?.quantity ?? 0
// Keeps only the new total quantity of addon
const nextAddonQuantity = currentAddonQuantity + adding
const changeRequest = recurlySubscription.getRequestForAddOnUpdate(
plan.membersLimitAddOn,
nextAddonQuantity
)
return {
changeRequest,
userId,
currentAddonQuantity,
recurlySubscription,
plan,
}
}
async function previewAddSeatsSubscriptionChange(req) {
const { changeRequest, userId, currentAddonQuantity, plan } =
await _addSeatsSubscriptionChange(req)
const paymentMethod = await RecurlyClient.promises.getPaymentMethod(userId)
const subscriptionChange =
await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
const subscriptionChangePreview =
await SubscriptionController.makeChangePreview(
{
type: 'add-on-update',
addOn: {
code: plan.membersLimitAddOn,
quantity: subscriptionChange.nextAddOns.find(
addon => addon.code === plan.membersLimitAddOn
).quantity,
prevQuantity: currentAddonQuantity,
},
},
subscriptionChange,
paymentMethod
)
return subscriptionChangePreview
}
async function createAddSeatsSubscriptionChange(req) {
const { changeRequest, userId, recurlySubscription } =
await _addSeatsSubscriptionChange(req)
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
await SubscriptionHandler.promises.syncSubscription(
{ uuid: recurlySubscription.id },
userId
)
return { adding: req.body.adding }
}
async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
if (
subscription.planCode.includes('professional') ||
!subscription.groupPlan
) {
throw new Error('Not eligible for group plan upgrade')
}
return subscription.planCode.includes('educational')
? 'group_professional_educational'
: 'group_professional'
}
async function _getGroupPlanUpgradeChangeRequest(ownerId) {
const olSubscription =
await SubscriptionLocator.promises.getUsersSubscription(ownerId)
const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
const recurlySubscription = await RecurlyClient.promises.getSubscription(
olSubscription.recurlySubscription_id
)
return recurlySubscription.getRequestForGroupPlanUpgrade(
newPlanCode,
olSubscription.membersLimit
)
}
function _getPlanNameForDisplay(subscription) {
if (/^group_collaborator_\d+_enterprise$/.test(subscription.planCode)) {
return 'Overleaf Standard Group'
}
if (/^group_collaborator_\d+_educational$/.test(subscription.planCode)) {
return 'Overleaf Standard Group Educational'
}
return subscription.planName
}
async function getGroupPlanUpgradePreview(ownerId) {
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
const subscriptionChange =
await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId)
return SubscriptionController.makeChangePreview(
{
type: 'group-plan-upgrade',
prevPlan: {
name: _getPlanNameForDisplay(subscriptionChange.subscription),
},
},
subscriptionChange,
paymentMethod
)
}
async function upgradeGroupPlan(ownerId) {
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
await SubscriptionHandler.promises.syncSubscription(
{ uuid: changeRequest.subscription.id },
ownerId
)
}
module.exports = {
removeUserFromGroup: callbackify(removeUserFromGroup),
replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
isUserPartOfGroup: callbackify(isUserPartOfGroup),
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
upgradeGroupPlan: callbackify(upgradeGroupPlan),
promises: {
removeUserFromGroup,
replaceUserReferencesInGroups,
ensureFlexibleLicensingEnabled,
getTotalConfirmedUsersInGroup,
isUserPartOfGroup,
getUsersGroupSubscriptionDetails,
previewAddSeatsSubscriptionChange,
createAddSeatsSubscriptionChange,
getGroupPlanUpgradePreview,
upgradeGroupPlan,
},
}