Merge pull request #32871 from overleaf/rh-cio-comms-attributes

Expose remaining marketing properties to customer.io

GitOrigin-RevId: 6956e1faf90ecc650108404fe13b2f6de2eb4d0c
This commit is contained in:
roo hutton
2026-04-23 08:06:04 +00:00
committed by Copybot
parent 2e5d7675ba
commit dece22ba92
5 changed files with 313 additions and 1 deletions
@@ -449,10 +449,200 @@ describe('FeaturesUpdater', function () {
ai_plan: 'none',
next_renewal_date: Math.floor(ctx.renewalDate.getTime() / 1000),
expiry_date: '',
group: false,
commons: false,
individual_subscription: true,
payment_provider: 'recurly',
features: sinon.match.object,
})
)
})
it('should not set trial_end_date when no trial is active', function (ctx) {
const call = ctx.Modules.promises.hooks.fire
.getCalls()
.find(c => c.args[0] === 'setUserProperties')
expect(call).to.exist
expect(call.args[2]).to.not.have.property('trial_end_date')
})
})
describe('when the individual subscription is on a trial', function () {
beforeEach(async function (ctx) {
ctx.trialEndsAt = new Date('2099-05-01T00:00:00Z')
const trialingSubscription = {
...ctx.subscriptions.individual,
recurlyStatus: {
state: 'active',
trialEndsAt: ctx.trialEndsAt,
},
}
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'individual' },
individualSubscription: trialingSubscription,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [],
}
)
ctx.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecordPromise', trialingSubscription)
.resolves([
{
subscription: {
state: 'active',
periodEnd: ctx.renewalDate,
},
},
])
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should sync trial_end_date to customer.io', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
trial_end_date: Math.floor(ctx.trialEndsAt.getTime() / 1000),
})
)
})
})
describe('when the individual subscription uses stripe', function () {
beforeEach(async function (ctx) {
const stripeSubscription = {
...ctx.subscriptions.individual,
paymentProvider: { service: 'stripe-us', state: 'active' },
}
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'individual' },
individualSubscription: stripeSubscription,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [],
}
)
ctx.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecordPromise', stripeSubscription)
.resolves([
{
subscription: {
state: 'active',
periodEnd: ctx.renewalDate,
},
},
])
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should report stripe as the payment_provider', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
payment_provider: 'stripe',
individual_subscription: true,
})
)
})
})
describe('when the user has a commons institution licence', function () {
beforeEach(async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'commons' },
individualSubscription: null,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [],
currentInstitutionsWithLicence: [{ id: 1, name: 'Uni' }],
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should sync commons=true to customer.io', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
commons: true,
group: false,
individual_subscription: false,
})
)
})
})
describe('when the user has commons and an individual AI add-on', function () {
beforeEach(async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: {
type: 'individual',
plan: { planCode: 'individual-plan' },
},
individualSubscription: ctx.subscriptions.individual,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [],
currentInstitutionsWithLicence: [{ id: 1, name: 'Uni' }],
}
)
ctx.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecordPromise', ctx.subscriptions.individual)
.resolves([
{
subscription: {
state: 'active',
periodEnd: ctx.renewalDate,
addOns: [{ code: AI_ADD_ON_CODE }],
},
},
])
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should set commons, individual_subscription, and ai-assist-add-on together', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
commons: true,
individual_subscription: true,
group: false,
ai_plan: 'ai-assist-add-on',
})
)
})
})
describe('when the user has no subscription', function () {
beforeEach(async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: null,
individualSubscription: null,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [],
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should sync false subscription flags and no payment_provider', function (ctx) {
const call = ctx.Modules.promises.hooks.fire
.getCalls()
.find(c => c.args[0] === 'setUserProperties')
expect(call).to.exist
expect(call.args[2]).to.include({
group: false,
commons: false,
individual_subscription: false,
})
expect(call.args[2]).to.not.have.property('payment_provider')
expect(call.args[2]).to.not.have.property('trial_end_date')
})
})
describe('when the individual subscription has a pending cancellation', function () {
@@ -544,6 +734,10 @@ describe('FeaturesUpdater', function () {
group_size: 8,
next_renewal_date: '',
expiry_date: '',
group: true,
commons: false,
individual_subscription: false,
payment_provider: 'recurly',
features: sinon.match.object,
overleaf_id: ctx.user._id,
})
@@ -551,6 +745,49 @@ describe('FeaturesUpdater', function () {
})
})
describe('when the user is in a stripe group subscription', function () {
beforeEach(async function (ctx) {
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,
paymentProvider: { service: 'stripe-uk' },
},
],
managedGroupSubscriptions: [],
individualSubscription: null,
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
})
it('should derive payment_provider from the group subscription', function (ctx) {
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
payment_provider: 'stripe',
group: true,
individual_subscription: false,
})
)
})
})
describe('with a non-standard feature set', async function () {
beforeEach(async function (ctx) {
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf