Files
overleaf-cep/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js
ilkin-overleaf 72be034435 Merge pull request #23263 from overleaf/ii-flexible-licensing-subscription-group-handler
[web] FL check subscription existence

GitOrigin-RevId: b564d681245137955a8f1e7367b9bd1a6b404268
2025-02-05 09:05:45 +00:00

253 lines
8.0 KiB
JavaScript

const { callbackify } = require('util')
const SubscriptionUpdater = require('./SubscriptionUpdater')
const SubscriptionLocator = require('./SubscriptionLocator')
const SubscriptionController = require('./SubscriptionController')
const { Subscription } = require('../../models/Subscription')
const RecurlyClient = require('./RecurlyClient')
const PlansLocator = require('./PlansLocator')
const SubscriptionHandler = require('./SubscriptionHandler')
const GroupPlansData = require('./GroupPlansData')
const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./RecurlyEntities')
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 licensing')
}
}
async function getUsersGroupSubscriptionDetails(userId) {
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
if (!subscription) {
throw new Error('No subscription was found')
}
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(userId, adding) {
const { recurlySubscription, plan } =
await getUsersGroupSubscriptionDetails(userId)
await ensureFlexibleLicensingEnabled(plan)
const currentAddonQuantity =
recurlySubscription.addOns.find(
addOn => addOn.code === MEMBERS_LIMIT_ADD_ON_CODE
)?.quantity ?? 0
// Keeps only the new total quantity of addon
const nextAddonQuantity = currentAddonQuantity + adding
let changeRequest
if (recurlySubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)) {
// Not providing a custom price as once the subscription is locked
// to an add-on at a given price, it will use it for subsequent payments
changeRequest = recurlySubscription.getRequestForAddOnUpdate(
MEMBERS_LIMIT_ADD_ON_CODE,
nextAddonQuantity
)
} else {
let unitPrice
const newPlanPricesAppliedAt = new Date('2025-01-08T14:00:00Z')
const isLegacyPriceApplicable =
new Date(recurlySubscription.createdAt) < newPlanPricesAppliedAt
if (isLegacyPriceApplicable) {
const pattern =
/^group_(collaborator|professional)_(2|3|4|5|10|20|50)_(educational|enterprise)$/
const [, planCode, size, usage] = plan.planCode.match(pattern)
const currency = recurlySubscription.currency
const legacyPriceInCents =
GroupPlansData[usage][planCode][currency][size]
.additional_license_legacy_price_in_cents
if (legacyPriceInCents > 0) {
unitPrice = legacyPriceInCents / 100
}
}
changeRequest = recurlySubscription.getRequestForAddOnPurchase(
MEMBERS_LIMIT_ADD_ON_CODE,
nextAddonQuantity,
unitPrice
)
}
return {
changeRequest,
currentAddonQuantity,
recurlySubscription,
}
}
async function previewAddSeatsSubscriptionChange(userId, adding) {
const { changeRequest, currentAddonQuantity } =
await _addSeatsSubscriptionChange(userId, adding)
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: MEMBERS_LIMIT_ADD_ON_CODE,
quantity: subscriptionChange.nextAddOns.find(
addon => addon.code === MEMBERS_LIMIT_ADD_ON_CODE
).quantity,
prevQuantity: currentAddonQuantity,
},
},
subscriptionChange,
paymentMethod
)
return subscriptionChangePreview
}
async function createAddSeatsSubscriptionChange(userId, adding) {
const { changeRequest, recurlySubscription } =
await _addSeatsSubscriptionChange(userId, adding)
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
await SubscriptionHandler.promises.syncSubscription(
{ uuid: recurlySubscription.id },
userId
)
return { adding }
}
async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
if (
subscription.planCode.includes('professional') ||
!subscription.groupPlan
) {
throw new Error('Not eligible for group plan upgrade')
}
return subscription.planCode.replace('collaborator', '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)
}
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: SubscriptionController.getPlanNameForDisplay(
subscriptionChange.subscription.planName,
subscriptionChange.subscription.planCode
),
},
},
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,
},
}