Merge pull request #31678 from overleaf/rh-cio-subscription-status

Sync subscription type and features to customer.io

GitOrigin-RevId: 4c23a6b4ec9f103e73b26203b0d43f177e56bb6e
This commit is contained in:
roo hutton
2026-02-23 13:20:53 +00:00
committed by Copybot
parent f9ad6cf5d1
commit 8fb5b0ed05
2 changed files with 56 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ import UserGetter from '../User/UserGetter.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import Queues from '../../infrastructure/Queues.mjs'
import Modules from '../../infrastructure/Modules.mjs'
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.mjs'
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
import { fetchNothing } from '@overleaf/fetch-utils'
@@ -54,6 +55,20 @@ async function refreshFeatures(userId, reason) {
const { features: newFeatures, featuresChanged } =
await UserFeaturesUpdater.promises.updateFeatures(userId, features)
// TODO: this call is quite expensive, so ideally we'd update cio with something
// that doesn't require the best subscription to be computed, ie. the plan code (or type)
const bestSubscriptionType = await _getBestSubscriptionType(userId)
Modules.promises.hooks
.fire('setUserProperties', userId, {
features,
'best-subscription-type': bestSubscriptionType,
})
.catch(err => {
logger.error({ err, userId }, 'Failed to sync features to customer.io')
})
if (oldFeatures.dropbox === true && features.dropbox === false) {
logger.debug({ userId }, '[FeaturesUpdater] must unlink dropbox')
try {
@@ -101,6 +116,22 @@ async function refreshFeatures(userId, reason) {
return { features: newFeatures, featuresChanged }
}
async function _getBestSubscriptionType(userId) {
try {
const { bestSubscription } =
await SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails({
_id: userId,
})
return bestSubscription?.type || 'free'
} catch (err) {
logger.warn(
{ err, userId },
'Failed to calculate best-subscription-type for customer.io'
)
return 'free'
}
}
/**
* Return the features that the given user should have.
*/

View File

@@ -113,6 +113,13 @@ describe('FeaturesUpdater', function () {
ctx.Modules = {
promises: { hooks: { fire: sinon.stub().resolves() } },
}
ctx.SubscriptionViewModelBuilder = {
promises: {
getUsersSubscriptionDetails: sinon.stub().resolves({
bestSubscription: { type: 'individual' },
}),
},
}
ctx.Queues = {
getQueue: sinon.stub().returns({
add: sinon.stub().resolves(),
@@ -170,6 +177,13 @@ describe('FeaturesUpdater', function () {
default: ctx.Modules,
}))
vi.doMock(
'../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder',
() => ({
default: ctx.SubscriptionViewModelBuilder,
})
)
vi.doMock('../../../../app/src/infrastructure/Queues', () => ({
default: ctx.Queues,
}))
@@ -374,6 +388,17 @@ describe('FeaturesUpdater', function () {
ctx.AnalyticsManager.setUserPropertyForUserInBackground
).to.have.been.calledWith(ctx.user._id, 'feature-set', 'all')
})
it('should sync features to customer.io', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
{
features: ctx.Settings.features.all,
'best-subscription-type': 'individual',
}
)
})
})
describe('with a non-standard feature set', async function () {