Просмотр исходного кода

Merge pull request #22224 from overleaf/em-group-ai-add-on

Give the Error Assistant to group admins with Error Assist

GitOrigin-RevId: 832d78968977a7c6f17a3a3c8409c506d96fdd48
Eric Mc Sween 1 год назад
Родитель
Сommit
e2478d14f1

+ 20 - 21
services/web/app/src/Features/Subscription/FeaturesUpdater.js

@@ -115,9 +115,22 @@ async function computeFeatures(userId) {
 }
 }
 
 
 async function _getIndividualFeatures(userId) {
 async function _getIndividualFeatures(userId) {
-  const sub =
-    await SubscriptionLocator.promises.getUserIndividualSubscription(userId)
-  return _subscriptionToFeatures(sub)
+  const subscription =
+    await SubscriptionLocator.promises.getUsersSubscription(userId)
+  if (subscription == null) {
+    return {}
+  }
+
+  const featureSets = []
+
+  // The plan doesn't apply to the group admin when the subscription
+  // is a group subscription
+  if (!subscription.groupPlan) {
+    featureSets.push(_subscriptionToFeatures(subscription))
+  }
+
+  featureSets.push(_aiAddOnFeatures(subscription))
+  return _.reduce(featureSets, FeaturesHelper.mergeFeatures, {})
 }
 }
 
 
 async function _getGroupFeatureSets(userId) {
 async function _getGroupFeatureSets(userId) {
@@ -148,20 +161,10 @@ async function _getV1Features(user) {
 }
 }
 
 
 function _subscriptionToFeatures(subscription) {
 function _subscriptionToFeatures(subscription) {
-  const addonFeatures = _subscriptionAddonsToFeatures(
-    subscription && subscription.addOns
-  )
-  const planFeatures = _planCodeToFeatures(
-    subscription && subscription.planCode
-  )
-  return FeaturesHelper.mergeFeatures(addonFeatures, planFeatures)
-}
-
-function _planCodeToFeatures(planCode) {
-  if (!planCode) {
+  if (!subscription?.planCode) {
     return {}
     return {}
   }
   }
-  const plan = PlansLocator.findLocalPlanInSettings(planCode)
+  const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
   if (!plan) {
   if (!plan) {
     return {}
     return {}
   } else {
   } else {
@@ -169,12 +172,8 @@ function _planCodeToFeatures(planCode) {
   }
   }
 }
 }
 
 
-function _subscriptionAddonsToFeatures(addOns) {
-  if (!addOns) {
-    return {}
-  }
-  const hasAiAddon = addOns.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE)
-  if (hasAiAddon) {
+function _aiAddOnFeatures(subscription) {
+  if (subscription?.addOns?.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE)) {
     return { aiErrorAssistant: true }
     return { aiErrorAssistant: true }
   } else {
   } else {
     return {}
     return {}

+ 142 - 5
services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js

@@ -2,6 +2,9 @@ const SandboxedModule = require('sandboxed-module')
 const { expect } = require('chai')
 const { expect } = require('chai')
 const sinon = require('sinon')
 const sinon = require('sinon')
 const { ObjectId } = require('mongodb-legacy')
 const { ObjectId } = require('mongodb-legacy')
+const {
+  AI_ADD_ON_CODE,
+} = require('../../../../app/src/Features/Subscription/RecurlyEntities')
 
 
 const MODULE_PATH = '../../../../app/src/Features/Subscription/FeaturesUpdater'
 const MODULE_PATH = '../../../../app/src/Features/Subscription/FeaturesUpdater'
 
 
@@ -13,11 +16,21 @@ describe('FeaturesUpdater', function () {
       features: {},
       features: {},
       overleaf: { id: this.v1UserId },
       overleaf: { id: this.v1UserId },
     }
     }
+    this.aiAddOn = { addOnCode: AI_ADD_ON_CODE, quantity: 1 }
     this.subscriptions = {
     this.subscriptions = {
       individual: { planCode: 'individual-plan' },
       individual: { planCode: 'individual-plan' },
-      group1: { planCode: 'group-plan-1' },
-      group2: { planCode: 'group-plan-2' },
+      group1: { planCode: 'group-plan-1', groupPlan: true },
+      group2: { planCode: 'group-plan-2', groupPlan: true },
       noDropbox: { planCode: 'no-dropbox' },
       noDropbox: { planCode: 'no-dropbox' },
+      individualPlusAiAddOn: {
+        planCode: 'individual-plan',
+        addOns: [this.aiAddOn],
+      },
+      groupPlusAiAddOn: {
+        planCode: 'group-plan-1',
+        groupPlan: true,
+        addOns: [this.aiAddOn],
+      },
     }
     }
 
 
     this.UserFeaturesUpdater = {
     this.UserFeaturesUpdater = {
@@ -30,11 +43,11 @@ describe('FeaturesUpdater', function () {
 
 
     this.SubscriptionLocator = {
     this.SubscriptionLocator = {
       promises: {
       promises: {
-        getUserIndividualSubscription: sinon.stub(),
+        getUsersSubscription: sinon.stub(),
         getGroupSubscriptionsMemberOf: sinon.stub(),
         getGroupSubscriptionsMemberOf: sinon.stub(),
       },
       },
     }
     }
-    this.SubscriptionLocator.promises.getUserIndividualSubscription
+    this.SubscriptionLocator.promises.getUsersSubscription
       .withArgs(this.user._id)
       .withArgs(this.user._id)
       .resolves(this.subscriptions.individual)
       .resolves(this.subscriptions.individual)
     this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
     this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
@@ -120,6 +133,130 @@ describe('FeaturesUpdater', function () {
     })
     })
   })
   })
 
 
+  describe('computeFeatures', function () {
+    beforeEach(function () {
+      this.SubscriptionLocator.promises.getUsersSubscription
+        .withArgs(this.user._id)
+        .resolves(null)
+      this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
+        .withArgs(this.user._id)
+        .resolves([])
+      this.ReferalFeatures.promises.getBonusFeatures.resolves({})
+      this.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
+        .withArgs(this.v1UserId)
+        .returns({})
+      this.InstitutionsFeatures.promises.getInstitutionsFeatures.resolves({})
+    })
+
+    describe('individual subscriber', function () {
+      beforeEach(function () {
+        this.SubscriptionLocator.promises.getUsersSubscription
+          .withArgs(this.user._id)
+          .resolves(this.subscriptions.individual)
+      })
+
+      it('returns the individual features', async function () {
+        const features = await this.FeaturesUpdater.promises.computeFeatures(
+          this.user._id
+        )
+        expect(features).to.deep.equal({
+          default: 'features',
+          individual: 'features',
+        })
+      })
+    })
+
+    describe('group admin', function () {
+      beforeEach(function () {
+        this.SubscriptionLocator.promises.getUsersSubscription
+          .withArgs(this.user._id)
+          .resolves(this.subscriptions.group1)
+      })
+
+      it("doesn't return the group features", async function () {
+        const features = await this.FeaturesUpdater.promises.computeFeatures(
+          this.user._id
+        )
+        expect(features).to.deep.equal({
+          default: 'features',
+        })
+      })
+    })
+
+    describe('group member', function () {
+      beforeEach(function () {
+        this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
+          .withArgs(this.user._id)
+          .resolves([this.subscriptions.group1])
+      })
+
+      it('returns the group features', async function () {
+        const features = await this.FeaturesUpdater.promises.computeFeatures(
+          this.user._id
+        )
+        expect(features).to.deep.equal({
+          default: 'features',
+          group1: 'features',
+        })
+      })
+    })
+
+    describe('individual subscription + AI add-on', function () {
+      beforeEach(function () {
+        this.SubscriptionLocator.promises.getUsersSubscription
+          .withArgs(this.user._id)
+          .resolves(this.subscriptions.individualPlusAiAddOn)
+      })
+
+      it('returns the individual features and the AI error assistant', async function () {
+        const features = await this.FeaturesUpdater.promises.computeFeatures(
+          this.user._id
+        )
+        expect(features).to.deep.equal({
+          default: 'features',
+          individual: 'features',
+          aiErrorAssistant: true,
+        })
+      })
+    })
+
+    describe('group admin + AI add-on', function () {
+      beforeEach(function () {
+        this.SubscriptionLocator.promises.getUsersSubscription
+          .withArgs(this.user._id)
+          .resolves(this.subscriptions.groupPlusAiAddOn)
+      })
+
+      it('returns the AI error assistant only', async function () {
+        const features = await this.FeaturesUpdater.promises.computeFeatures(
+          this.user._id
+        )
+        expect(features).to.deep.equal({
+          default: 'features',
+          aiErrorAssistant: true,
+        })
+      })
+    })
+
+    describe('group member + AI add-on', function () {
+      beforeEach(function () {
+        this.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
+          .withArgs(this.user._id)
+          .resolves([this.subscriptions.groupPlusAiAddOn])
+      })
+
+      it('returns the group features without the AI features', async function () {
+        const features = await this.FeaturesUpdater.promises.computeFeatures(
+          this.user._id
+        )
+        expect(features).to.deep.equal({
+          default: 'features',
+          group1: 'features',
+        })
+      })
+    })
+  })
+
   describe('refreshFeatures', function () {
   describe('refreshFeatures', function () {
     it('should return features and featuresChanged', async function () {
     it('should return features and featuresChanged', async function () {
       const { features, featuresChanged } =
       const { features, featuresChanged } =
@@ -176,7 +313,7 @@ describe('FeaturesUpdater', function () {
     describe('when losing dropbox feature', async function () {
     describe('when losing dropbox feature', async function () {
       beforeEach(async function () {
       beforeEach(async function () {
         this.user.features = { dropbox: true }
         this.user.features = { dropbox: true }
-        this.SubscriptionLocator.promises.getUserIndividualSubscription
+        this.SubscriptionLocator.promises.getUsersSubscription
           .withArgs(this.user._id)
           .withArgs(this.user._id)
           .resolves(this.subscriptions.noDropbox)
           .resolves(this.subscriptions.noDropbox)
         await this.FeaturesUpdater.promises.refreshFeatures(
         await this.FeaturesUpdater.promises.refreshFeatures(