mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-08 08:39:03 +02:00
Feat: Allow Ai Bundle for Commons Licenses (#29413)
* allowing for writefullCommonsAccount within v1 affiliates to signal a commons ai bundle * feat: update wf when features change * feat: replace call to wf with metric to give them estimate of number of calls * fix: acceptance tests for GroupDomainCaptureTests rely on features outlined in settings, where aiErrorAssistant feature is not listed since it is delivered through a module hook GitOrigin-RevId: 8c2470c7e73b8a1e080bfc977469d35e66ca9db4
This commit is contained in:
committed by
Copybot
parent
4186321ed7
commit
2aa2862a77
@@ -2,11 +2,28 @@ import { callbackifyAll } from '@overleaf/promise-utils'
|
||||
import UserGetter from '../User/UserGetter.mjs'
|
||||
import PlansLocator from '../Subscription/PlansLocator.mjs'
|
||||
import Settings from '@overleaf/settings'
|
||||
import InstitutionsGetter from './InstitutionsGetter.mjs'
|
||||
import FeaturesHelper from '../Subscription/FeaturesHelper.mjs'
|
||||
|
||||
async function _getInstitutionsAddons(userId) {
|
||||
const affiliates =
|
||||
await InstitutionsGetter.promises.getCurrentAffiliations(userId)
|
||||
// currently only addOn available to institutions is assist/WF bundle,
|
||||
// which is denoted by the presence of writefullCommonsAccount on the institution
|
||||
const hasAssistBundle = affiliates.some(
|
||||
affiliate => affiliate?.institution?.writefullCommonsAccount === true
|
||||
)
|
||||
return hasAssistBundle ? { aiErrorAssistant: true } : {}
|
||||
}
|
||||
|
||||
async function getInstitutionsFeatures(userId) {
|
||||
const planCode = await getInstitutionsPlan(userId)
|
||||
const plan = planCode && PlansLocator.findLocalPlanInSettings(planCode)
|
||||
const features = plan && plan.features
|
||||
let features = plan && plan.features
|
||||
|
||||
const addOns = await _getInstitutionsAddons(userId)
|
||||
features = FeaturesHelper.mergeFeatures(features, addOns)
|
||||
|
||||
return features || {}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
|
||||
import Queues from '../../infrastructure/Queues.mjs'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
|
||||
// import { fetchNothing } from '@overleaf/fetch-utils'
|
||||
import metrics from '@overleaf/metrics'
|
||||
|
||||
/**
|
||||
* Enqueue a job for refreshing features for the given user
|
||||
@@ -42,7 +44,7 @@ async function refreshFeatures(userId, reason) {
|
||||
})
|
||||
const oldFeatures = _.clone(user.features)
|
||||
const features = await computeFeatures(userId)
|
||||
logger.debug({ userId, features }, 'updating user features')
|
||||
logger.debug({ userId, features, reason }, 'updating user features')
|
||||
|
||||
const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features)
|
||||
AnalyticsManager.setUserPropertyForUserInBackground(
|
||||
@@ -71,6 +73,33 @@ async function refreshFeatures(userId, reason) {
|
||||
}
|
||||
}
|
||||
|
||||
// only update Writefull if the user's features have changed,
|
||||
// skip if they are the reason we are refreshing features (they'd already be up to date)
|
||||
if (featuresChanged && reason !== 'writefullEntitlementSynced') {
|
||||
try {
|
||||
// update WF with the current feature set for the user
|
||||
// await fetchNothing(
|
||||
// `${Settings.writefull.overleafApiUrl}/api/user/status/update-overleaf-status`,
|
||||
// {
|
||||
// headers: {
|
||||
// 'x-api-key': Settings.writefull.overleafApiKey,
|
||||
// },
|
||||
// json: {
|
||||
// userOverleafId: userId,
|
||||
// hasAiAssist: newFeatures.aiErrorAssistant,
|
||||
// },
|
||||
// method: 'POST',
|
||||
// }
|
||||
// )
|
||||
// increment a metric instead of calling WF so we cna give them an idea of the # of requests they will recieve
|
||||
metrics.inc('feature_sync_called_to_wf')
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{ userId, reason },
|
||||
'failed to sync entitlement to Writefull after a feature refresh'
|
||||
)
|
||||
}
|
||||
}
|
||||
return { features: newFeatures, featuresChanged }
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ describe('InstitutionsFeatures', function () {
|
||||
}
|
||||
ctx.PlansLocator = { findLocalPlanInSettings: sinon.stub() }
|
||||
ctx.institutionPlanCode = 'institution_plan_code'
|
||||
ctx.InstitutionsGetter = {
|
||||
promises: { getCurrentAffiliations: sinon.stub().resolves([]) },
|
||||
}
|
||||
|
||||
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
|
||||
default: ctx.UserGetter,
|
||||
@@ -29,10 +32,30 @@ describe('InstitutionsFeatures', function () {
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Institutions/InstitutionsGetter',
|
||||
() => ({
|
||||
default: ctx.InstitutionsGetter,
|
||||
})
|
||||
)
|
||||
|
||||
ctx.InstitutionsFeatures = (await import(modulePath)).default
|
||||
ctx.emailDataWithLicense = [{ emailHasInstitutionLicence: true }]
|
||||
ctx.emailDataWithoutLicense = [{ emailHasInstitutionLicence: false }]
|
||||
ctx.userId = '12345abcde'
|
||||
ctx.affiliateWithAiBundle = {
|
||||
institution: { writefullCommonsAccount: true },
|
||||
}
|
||||
ctx.affiliateWithoutAiBundle = {
|
||||
institution: { writefullCommonsAccount: false },
|
||||
}
|
||||
ctx.testFeatures = { features: { institution: 'all' } }
|
||||
ctx.testFeaturesWithAiAddon = {
|
||||
features: { institution: 'all', aiErrorAssistant: true },
|
||||
}
|
||||
ctx.testFeaturesWithNoAddon = {
|
||||
features: { institution: 'all', aiErrorAssistant: false },
|
||||
}
|
||||
})
|
||||
|
||||
describe('hasLicence', function () {
|
||||
@@ -87,7 +110,7 @@ describe('InstitutionsFeatures', function () {
|
||||
).to.be.rejected
|
||||
})
|
||||
|
||||
it('should return no feaures if user has no plan code', async function (ctx) {
|
||||
it('should return no features if user has no plan code', async function (ctx) {
|
||||
ctx.UserGetter.promises.getUserFullEmails.resolves(
|
||||
ctx.emailDataWithoutLicense
|
||||
)
|
||||
@@ -98,6 +121,21 @@ describe('InstitutionsFeatures', function () {
|
||||
expect(features).to.deep.equal({})
|
||||
})
|
||||
|
||||
it('should return ai features if user has any affiliation with add-on bundle', async function (ctx) {
|
||||
ctx.InstitutionsGetter.promises.getCurrentAffiliations = sinon
|
||||
.stub()
|
||||
.resolves([ctx.affiliateWithoutAiBundle, ctx.affiliateWithAiBundle])
|
||||
ctx.UserGetter.promises.getUserFullEmails.resolves(
|
||||
ctx.emailDataWithLicense
|
||||
)
|
||||
|
||||
const features =
|
||||
await ctx.InstitutionsFeatures.promises.getInstitutionsFeatures(
|
||||
ctx.userId
|
||||
)
|
||||
expect(features).to.deep.equal(ctx.testFeaturesWithAiAddon.features)
|
||||
})
|
||||
|
||||
it('should return feaures if user has affiliations plan code', async function (ctx) {
|
||||
ctx.UserGetter.promises.getUserFullEmails.resolves(
|
||||
ctx.emailDataWithLicense
|
||||
|
||||
@@ -72,6 +72,9 @@ describe('FeaturesUpdater', function () {
|
||||
bonus: 'features',
|
||||
},
|
||||
},
|
||||
writefull: {
|
||||
overleafApiUrl: 'https://www.writefull.com',
|
||||
},
|
||||
}
|
||||
|
||||
ctx.ReferalFeatures = {
|
||||
@@ -173,6 +176,10 @@ describe('FeaturesUpdater', function () {
|
||||
|
||||
vi.doMock('../../../../app/src/models/Subscription', () => ({}))
|
||||
|
||||
vi.doMock('@overleaf/fetch-utils', () => ({
|
||||
fetchNothing: sinon.stub().resolves(),
|
||||
}))
|
||||
|
||||
ctx.FeaturesUpdater = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user