Merge pull request #33247 from overleaf/rh-cio-fix-ai-group-enabled

Base group ai enabled cio attribute on group policy

GitOrigin-RevId: 2b2411aec3ffc694d2570e6031e9a876a1575e2c
This commit is contained in:
roo hutton
2026-04-30 09:32:52 +01:00
committed by Copybot
parent a478c1a829
commit 970bc85b78
3 changed files with 92 additions and 8 deletions

View File

@@ -250,7 +250,8 @@ function hasPlanAiEnabled(plan) {
function getGroupAiEnabled(
memberGroupSubscriptions = [],
managedGroupSubscriptions = [],
userIsMemberOfGroupSubscription
userIsMemberOfGroupSubscription,
aiBlockedByPolicyId = new Map()
) {
if (!userIsMemberOfGroupSubscription) {
return null
@@ -261,12 +262,12 @@ function getGroupAiEnabled(
...managedGroupSubscriptions,
]
return allGroupSubscriptions.some(subscription => {
const plan = Settings.plans.find(
candidate => candidate.planCode === subscription.planCode
)
return hasPlanAiEnabled(plan)
const someBlocked = allGroupSubscriptions.some(subscription => {
const policyId = subscription.groupPolicy?.toString()
return policyId ? aiBlockedByPolicyId.get(policyId) : false
})
return !someBlocked
}
function getGroupSize(
@@ -373,6 +374,7 @@ function getPlanProperties({
userIsMemberOfGroupSubscription,
hasCommons,
writefullData,
aiBlockedByPolicyId,
}) {
const planType = normalizePlanType(bestSubscription)
const displayPlanType = getFriendlyPlanName(planType)
@@ -392,7 +394,8 @@ function getPlanProperties({
const groupAiEnabled = getGroupAiEnabled(
memberGroupSubscriptions,
managedGroupSubscriptions,
userIsMemberOfGroupSubscription
userIsMemberOfGroupSubscription,
aiBlockedByPolicyId
)
const nextRenewalDate = getNextRenewalDateFromPaymentRecord(
individualPaymentRecord

View File

@@ -17,6 +17,7 @@ import Queues from '../../infrastructure/Queues.mjs'
import Modules from '../../infrastructure/Modules.mjs'
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.mjs'
import CustomerIoPlanHelpers from './CustomerIoPlanHelpers.mjs'
import { GroupPolicy } from '../../models/GroupPolicy.mjs'
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
import { fetchNothing } from '@overleaf/fetch-utils'
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
@@ -169,6 +170,11 @@ async function _updateCustomerIoSubscriptionProperties(user, features) {
)
}
const aiBlockedByPolicyId = await _loadAiBlockedByPolicyId([
...memberGroupSubscriptions,
...managedGroupSubscriptions,
])
const planProperties = CustomerIoPlanHelpers.getPlanProperties({
bestSubscription,
individualSubscription,
@@ -178,6 +184,7 @@ async function _updateCustomerIoSubscriptionProperties(user, features) {
userIsMemberOfGroupSubscription,
hasCommons,
writefullData,
aiBlockedByPolicyId,
})
await Modules.promises.hooks.fire('setUserProperties', userId, {
@@ -188,6 +195,29 @@ async function _updateCustomerIoSubscriptionProperties(user, features) {
})
}
async function _loadAiBlockedByPolicyId(groupSubscriptions) {
const policyIds = [
...new Set(
groupSubscriptions.map(sub => sub.groupPolicy?.toString()).filter(Boolean)
),
]
if (policyIds.length === 0) {
return new Map()
}
const policies = await GroupPolicy.find(
{ _id: { $in: policyIds } },
{ _id: 1, userCannotUseAIFeatures: 1 }
).exec()
return new Map(
policies.map(policy => [
policy._id.toString(),
Boolean(policy.userCannotUseAIFeatures),
])
)
}
/**
* Return the features that the given user should have.
*/

View File

@@ -167,6 +167,10 @@ describe('FeaturesUpdater', function () {
},
}
ctx.GroupPolicy = {
find: sinon.stub().returns({ exec: sinon.stub().resolves([]) }),
}
vi.doMock(
'../../../../app/src/Features/Subscription/UserFeaturesUpdater',
() => ({
@@ -231,6 +235,10 @@ describe('FeaturesUpdater', function () {
vi.doMock('../../../../app/src/models/Subscription', () => ({}))
vi.doMock('../../../../app/src/models/GroupPolicy', () => ({
GroupPolicy: ctx.GroupPolicy,
}))
vi.doMock('@overleaf/fetch-utils', () => ({
fetchNothing: sinon.stub().resolves(),
}))
@@ -730,7 +738,7 @@ describe('FeaturesUpdater', function () {
display_plan_type: 'Group Standard',
plan_term_label: 'monthly',
ai_plan: 'none',
group_ai_enabled: false,
group_ai_enabled: true,
group_size: 8,
next_renewal_date: '',
expiry_date: '',
@@ -745,6 +753,49 @@ describe('FeaturesUpdater', function () {
})
})
describe('when the group subscription has a policy that blocks AI', function () {
beforeEach(async function (ctx) {
const policyId = new ObjectId()
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: {
type: 'group',
plan: {
planCode: 'group-plan-1',
groupPlan: true,
membersLimit: 5,
},
subscription: { teamName: 'Team Alpha' },
},
memberGroupSubscriptions: [
{
planCode: 'group-plan-1',
teamName: 'Team Alpha',
membersLimit: 8,
groupPolicy: policyId,
},
],
managedGroupSubscriptions: [],
individualSubscription: null,
}
)
ctx.GroupPolicy.find.returns({
exec: sinon
.stub()
.resolves([{ _id: policyId, userCannotUseAIFeatures: true }]),
})
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should set group_ai_enabled to false', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({ group_ai_enabled: false })
)
})
})
describe('when the user is in a stripe group subscription', function () {
beforeEach(async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(