From e2478d14f1d72374bb2125d6afbf234274b3f313 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:19:54 -0500 Subject: [PATCH] Merge pull request #22224 from overleaf/em-group-ai-add-on Give the Error Assistant to group admins with Error Assist GitOrigin-RevId: 832d78968977a7c6f17a3a3c8409c506d96fdd48 --- .../Features/Subscription/FeaturesUpdater.js | 41 +++-- .../src/Subscription/FeaturesUpdaterTests.js | 147 +++++++++++++++++- 2 files changed, 162 insertions(+), 26 deletions(-) diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.js b/services/web/app/src/Features/Subscription/FeaturesUpdater.js index f4bfbcd492..8b188183eb 100644 --- a/services/web/app/src/Features/Subscription/FeaturesUpdater.js +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.js @@ -115,9 +115,22 @@ async function computeFeatures(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) { @@ -148,20 +161,10 @@ async function _getV1Features(user) { } 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 {} } - const plan = PlansLocator.findLocalPlanInSettings(planCode) + const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) if (!plan) { return {} } 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 } } else { return {} diff --git a/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js b/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js index d517099605..cf7d8d8683 100644 --- a/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js +++ b/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js @@ -2,6 +2,9 @@ const SandboxedModule = require('sandboxed-module') const { expect } = require('chai') const sinon = require('sinon') const { ObjectId } = require('mongodb-legacy') +const { + AI_ADD_ON_CODE, +} = require('../../../../app/src/Features/Subscription/RecurlyEntities') const MODULE_PATH = '../../../../app/src/Features/Subscription/FeaturesUpdater' @@ -13,11 +16,21 @@ describe('FeaturesUpdater', function () { features: {}, overleaf: { id: this.v1UserId }, } + this.aiAddOn = { addOnCode: AI_ADD_ON_CODE, quantity: 1 } this.subscriptions = { 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' }, + individualPlusAiAddOn: { + planCode: 'individual-plan', + addOns: [this.aiAddOn], + }, + groupPlusAiAddOn: { + planCode: 'group-plan-1', + groupPlan: true, + addOns: [this.aiAddOn], + }, } this.UserFeaturesUpdater = { @@ -30,11 +43,11 @@ describe('FeaturesUpdater', function () { this.SubscriptionLocator = { promises: { - getUserIndividualSubscription: sinon.stub(), + getUsersSubscription: sinon.stub(), getGroupSubscriptionsMemberOf: sinon.stub(), }, } - this.SubscriptionLocator.promises.getUserIndividualSubscription + this.SubscriptionLocator.promises.getUsersSubscription .withArgs(this.user._id) .resolves(this.subscriptions.individual) 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 () { it('should return features and featuresChanged', async function () { const { features, featuresChanged } = @@ -176,7 +313,7 @@ describe('FeaturesUpdater', function () { describe('when losing dropbox feature', async function () { beforeEach(async function () { this.user.features = { dropbox: true } - this.SubscriptionLocator.promises.getUserIndividualSubscription + this.SubscriptionLocator.promises.getUsersSubscription .withArgs(this.user._id) .resolves(this.subscriptions.noDropbox) await this.FeaturesUpdater.promises.refreshFeatures(