Răsfoiți Sursa

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

Base group ai enabled cio attribute on group policy

GitOrigin-RevId: 2b2411aec3ffc694d2570e6031e9a876a1575e2c
roo hutton 5 luni în urmă
părinte
comite
970bc85b78

+ 10 - 7
services/web/app/src/Features/Subscription/CustomerIoPlanHelpers.mjs

@@ -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

+ 30 - 0
services/web/app/src/Features/Subscription/FeaturesUpdater.mjs

@@ -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.
  */

+ 52 - 1
services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs

@@ -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(