Files
overleaf-cep/services/web/app/src/Features/Subscription/LimitationsManager.js
T
M Fahru 8b89c31eed Merge pull request #25166 from overleaf/kh-cancel-subscription
[web] support canceling Stripe subscription

GitOrigin-RevId: a72ccb20fbef9b6662cdfa1dcffacbd76dcb694c
2025-05-02 08:06:02 +00:00

220 lines
6.5 KiB
JavaScript

// @ts-check
const logger = require('@overleaf/logger')
const ProjectGetter = require('../Project/ProjectGetter')
const UserGetter = require('../User/UserGetter')
const SubscriptionLocator = require('./SubscriptionLocator')
const Settings = require('@overleaf/settings')
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
const CollaboratorsInvitesGetter = require('../Collaborators/CollaboratorsInviteGetter')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const {
callbackify,
callbackifyMultiResult,
} = require('@overleaf/promise-utils')
async function allowedNumberOfCollaboratorsInProject(projectId) {
const project = await ProjectGetter.promises.getProject(projectId, {
owner_ref: true,
})
return await allowedNumberOfCollaboratorsForUser(project.owner_ref)
}
async function allowedNumberOfCollaboratorsForUser(userId) {
const user = await UserGetter.promises.getUser(userId, { features: 1 })
if (user.features && user.features.collaborators) {
return user.features.collaborators
} else {
return Settings.defaultFeatures.collaborators
}
}
async function canAcceptEditCollaboratorInvite(projectId) {
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
if (allowedNumber < 0) {
return true // -1 means unlimited
}
const currentEditors =
await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
projectId
)
return currentEditors + 1 <= allowedNumber
}
async function canAddXEditCollaborators(
projectId,
numberOfNewEditCollaborators
) {
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
if (allowedNumber < 0) {
return true // -1 means unlimited
}
const currentEditors =
await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
projectId
)
const editInviteCount =
await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
return (
currentEditors + editInviteCount + numberOfNewEditCollaborators <=
allowedNumber
)
}
/**
* Check whether a collaborator can be switched to the given privilege level
*
* @param {string} projectId
* @param {string} userId
* @param {'readOnly' | 'review' | 'readAndWrite'} privilegeLevel
* @return {Promise<boolean>}
*/
async function canChangeCollaboratorPrivilegeLevel(
projectId,
userId,
privilegeLevel
) {
if (privilegeLevel === PrivilegeLevels.READ_ONLY) {
return true
}
const currentPrivilegeLevel =
await CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
userId,
projectId
)
if (
currentPrivilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
currentPrivilegeLevel === PrivilegeLevels.REVIEW
) {
// Current collaborator already takes a slot, so changing the privilege
// level won't increase the collaborator count
return true
}
const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
if (allowedNumber < 0) {
// -1 means unlimited
return true
}
const slotsTaken =
await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
projectId
)
const inviteCount =
await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
return slotsTaken + inviteCount < allowedNumber
}
async function hasPaidSubscription(user) {
const { hasSubscription, subscription } = await userHasSubscription(user)
const { isMember } = await userIsMemberOfGroupSubscription(user)
return {
hasPaidSubscription: hasSubscription || isMember,
subscription,
}
}
// alias for backward-compatibility with modules. Use `haspaidsubscription` instead
async function userHasSubscriptionOrIsGroupMember(user) {
return await hasPaidSubscription(user)
}
async function userHasSubscription(user) {
const subscription = await SubscriptionLocator.promises.getUsersSubscription(
user._id
)
let hasValidSubscription = false
if (subscription) {
if (
subscription.recurlySubscription_id ||
subscription.paymentProvider?.subscriptionId ||
subscription.customAccount
) {
hasValidSubscription = true
}
}
return {
hasSubscription: hasValidSubscription,
subscription,
}
}
async function userIsMemberOfGroupSubscription(user) {
const subscriptions =
(await SubscriptionLocator.promises.getMemberSubscriptions(user._id)) || []
return { isMember: subscriptions.length > 0, subscriptions }
}
function teamHasReachedMemberLimit(subscription) {
const currentTotal =
(subscription.member_ids || []).length +
(subscription.teamInvites || []).length +
(subscription.invited_emails || []).length
return currentTotal >= subscription.membersLimit
}
async function hasGroupMembersLimitReached(subscriptionId, callback) {
const subscription =
await SubscriptionLocator.promises.getSubscription(subscriptionId)
if (!subscription) {
logger.warn({ subscriptionId }, 'no subscription found')
throw new Error('no subscription found')
}
const limitReached = teamHasReachedMemberLimit(subscription)
return { limitReached, subscription }
}
const LimitationsManager = {
allowedNumberOfCollaboratorsInProject: callbackify(
allowedNumberOfCollaboratorsInProject
),
allowedNumberOfCollaboratorsForUser: callbackify(
allowedNumberOfCollaboratorsForUser
),
canAddXEditCollaborators: callbackify(canAddXEditCollaborators),
canChangeCollaboratorPrivilegeLevel: callbackify(
canChangeCollaboratorPrivilegeLevel
),
hasPaidSubscription: callbackifyMultiResult(hasPaidSubscription, [
'hasPaidSubscription',
'subscription',
]),
userHasSubscriptionOrIsGroupMember: callbackifyMultiResult(
userHasSubscriptionOrIsGroupMember,
['hasPaidSubscription', 'subscription']
),
userHasSubscription: callbackifyMultiResult(userHasSubscription, [
'hasSubscription',
'subscription',
]),
userIsMemberOfGroupSubscription: callbackifyMultiResult(
userIsMemberOfGroupSubscription,
['isMember', 'subscriptions']
),
hasGroupMembersLimitReached: callbackifyMultiResult(
hasGroupMembersLimitReached,
['limitReached', 'subscription']
),
teamHasReachedMemberLimit,
promises: {
allowedNumberOfCollaboratorsInProject,
allowedNumberOfCollaboratorsForUser,
canAcceptEditCollaboratorInvite,
canAddXEditCollaborators,
canChangeCollaboratorPrivilegeLevel,
hasPaidSubscription,
userHasSubscriptionOrIsGroupMember,
userHasSubscription,
userIsMemberOfGroupSubscription,
hasGroupMembersLimitReached,
},
}
module.exports = LimitationsManager