mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-30 20:31:34 +02:00
[web] Hide flexible licensing buttons for pending plans (fix) GitOrigin-RevId: ce5b4ce4138ed7a029b840a87c5498227e3204f4
321 lines
10 KiB
JavaScript
321 lines
10 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')
|
|
const { ManuallyCollectedError, PendingChangeError } = require('./Errors')
|
|
|
|
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 ensureSubscriptionIsActive(subscription) {
|
|
if (subscription?.recurlyStatus?.state !== 'active') {
|
|
throw new Error('The subscription is not active')
|
|
}
|
|
}
|
|
|
|
async function ensureSubscriptionCollectionMethodIsNotManual(
|
|
recurlySubscription
|
|
) {
|
|
if (recurlySubscription.isCollectionMethodManual) {
|
|
throw new ManuallyCollectedError(
|
|
'This subscription is being collected manually',
|
|
{
|
|
recurlySubscription_id: recurlySubscription.id,
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
async function ensureSubscriptionHasNoPendingChanges(recurlySubscription) {
|
|
if (recurlySubscription.pendingChange) {
|
|
throw new PendingChangeError('This subscription has a pending change', {
|
|
recurlySubscription_id: recurlySubscription.id,
|
|
})
|
|
}
|
|
}
|
|
|
|
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 {
|
|
userId,
|
|
subscription,
|
|
recurlySubscription,
|
|
plan,
|
|
}
|
|
}
|
|
|
|
async function _addSeatsSubscriptionChange(userId, adding) {
|
|
const { subscription, recurlySubscription, plan } =
|
|
await getUsersGroupSubscriptionDetails(userId)
|
|
await ensureFlexibleLicensingEnabled(plan)
|
|
await ensureSubscriptionIsActive(subscription)
|
|
await ensureSubscriptionCollectionMethodIsNotManual(recurlySubscription)
|
|
await ensureSubscriptionHasNoPendingChanges(recurlySubscription)
|
|
|
|
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 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 planPriceInCents =
|
|
GroupPlansData[usage][planCode][currency][size].price_in_cents
|
|
const legacyUnitPriceInCents =
|
|
GroupPlansData[usage][planCode][currency][size]
|
|
.additional_license_legacy_price_in_cents
|
|
|
|
if (
|
|
_shouldUseLegacyPricing(
|
|
recurlySubscription.planPrice,
|
|
planPriceInCents / 100,
|
|
usage,
|
|
size
|
|
)
|
|
) {
|
|
unitPrice = legacyUnitPriceInCents / 100
|
|
}
|
|
|
|
changeRequest = recurlySubscription.getRequestForAddOnPurchase(
|
|
MEMBERS_LIMIT_ADD_ON_CODE,
|
|
nextAddonQuantity,
|
|
unitPrice
|
|
)
|
|
}
|
|
|
|
return {
|
|
changeRequest,
|
|
currentAddonQuantity,
|
|
recurlySubscription,
|
|
}
|
|
}
|
|
|
|
function _shouldUseLegacyPricing(
|
|
actualPlanPrice,
|
|
currentPlanPrice,
|
|
usage,
|
|
size
|
|
) {
|
|
// For small educational groups (5 or fewer members)
|
|
// 2025 pricing is cheaper than legacy pricing
|
|
if (size <= 5 && usage === 'educational') {
|
|
return currentPlanPrice < actualPlanPrice
|
|
}
|
|
|
|
// For all other scenarios
|
|
// 2025 pricing is more expensive than legacy pricing
|
|
return currentPlanPrice > actualPlanPrice
|
|
}
|
|
|
|
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)
|
|
|
|
await ensureSubscriptionIsActive(olSubscription)
|
|
|
|
const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
|
|
const recurlySubscription = await RecurlyClient.promises.getSubscription(
|
|
olSubscription.recurlySubscription_id
|
|
)
|
|
|
|
await ensureSubscriptionCollectionMethodIsNotManual(recurlySubscription)
|
|
await ensureSubscriptionHasNoPendingChanges(recurlySubscription)
|
|
|
|
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),
|
|
ensureSubscriptionIsActive: callbackify(ensureSubscriptionIsActive),
|
|
ensureSubscriptionCollectionMethodIsNotManual: callbackify(
|
|
ensureSubscriptionCollectionMethodIsNotManual
|
|
),
|
|
ensureSubscriptionHasNoPendingChanges: callbackify(
|
|
ensureSubscriptionHasNoPendingChanges
|
|
),
|
|
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
|
|
isUserPartOfGroup: callbackify(isUserPartOfGroup),
|
|
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
|
|
upgradeGroupPlan: callbackify(upgradeGroupPlan),
|
|
promises: {
|
|
removeUserFromGroup,
|
|
replaceUserReferencesInGroups,
|
|
ensureFlexibleLicensingEnabled,
|
|
ensureSubscriptionIsActive,
|
|
ensureSubscriptionCollectionMethodIsNotManual,
|
|
ensureSubscriptionHasNoPendingChanges,
|
|
getTotalConfirmedUsersInGroup,
|
|
isUserPartOfGroup,
|
|
getUsersGroupSubscriptionDetails,
|
|
previewAddSeatsSubscriptionChange,
|
|
createAddSeatsSubscriptionChange,
|
|
getGroupPlanUpgradePreview,
|
|
upgradeGroupPlan,
|
|
},
|
|
}
|