Merge pull request #33345 from overleaf/rh-split-group-role

Update group_role in customer.io when changed

GitOrigin-RevId: d21866a9fe324a0468de74a45b6932dda27de8a1
This commit is contained in:
roo hutton
2026-05-28 08:06:43 +00:00
committed by Copybot
parent 5d0becf76b
commit 4f192564f2
10 changed files with 348 additions and 14 deletions
@@ -478,6 +478,7 @@ describe('ProjectListController', function () {
{
planCode: 'group_professional',
membersLimit: 12,
admin_id: { _id: ctx.user._id },
},
],
}
@@ -495,6 +496,63 @@ describe('ProjectListController', function () {
)
})
it('should send groupRole to customer.io for group managers', async function (ctx) {
ctx.Features.hasFeature.withArgs('saas').returns(true)
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'free' },
individualSubscription: null,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [
{
planCode: 'group_professional',
membersLimit: 12,
admin_id: { _id: new ObjectId() },
},
],
}
)
ctx.res.render = () => {}
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
group_role: 'manager',
})
)
})
it('should send groupRole to customer.io for group members', async function (ctx) {
ctx.Features.hasFeature.withArgs('saas').returns(true)
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'free' },
individualSubscription: null,
memberGroupSubscriptions: [
{
planCode: 'group_professional',
userIsGroupManager: false,
},
],
managedGroupSubscriptions: [],
}
)
ctx.res.render = () => {}
await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({
group_role: 'member',
})
)
})
it('should show INR Banner for Indian users with free account', async function (ctx) {
// usersBestSubscription is only available when saas feature is present
ctx.Features.hasFeature.withArgs('saas').returns(true)
@@ -839,6 +839,76 @@ describe('FeaturesUpdater', function () {
})
})
describe('group_role property', function () {
it("should set group_role to '' when the user is not in any group", async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'free' },
individualSubscription: null,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [],
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({ group_role: '' })
)
})
it("should set group_role to 'member' when the user only belongs to a group as a member", async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'free' },
individualSubscription: null,
memberGroupSubscriptions: [{ _id: new ObjectId() }],
managedGroupSubscriptions: [],
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({ group_role: 'member' })
)
})
it("should set group_role to 'manager' when the user manages a group they don't own", async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'free' },
individualSubscription: null,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [{ admin_id: { _id: new ObjectId() } }],
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({ group_role: 'manager' })
)
})
it("should set group_role to 'admin' when the user owns a group", async function (ctx) {
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
{
bestSubscription: { type: 'free' },
individualSubscription: null,
memberGroupSubscriptions: [],
managedGroupSubscriptions: [{ admin_id: { _id: ctx.user._id } }],
}
)
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.user._id,
sinon.match({ group_role: 'admin' })
)
})
})
describe('with a non-standard feature set', async function () {
beforeEach(async function (ctx) {
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
@@ -91,6 +91,7 @@ describe('SubscriptionUpdater', function () {
getUsersSubscription: sinon.stub(),
getGroupSubscriptionMemberOf: sinon.stub(),
getMemberSubscriptions: sinon.stub().resolves([]),
getManagedGroupSubscriptions: sinon.stub().resolves([]),
getSubscription: sinon.stub(),
},
}
@@ -283,6 +284,44 @@ describe('SubscriptionUpdater', function () {
ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
})
it("should fire setUserProperties for both old and new admin's group_role on group subscriptions", async function (ctx) {
ctx.subscription.groupPlan = true
ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
.withArgs(ctx.otherUserId)
.resolves([{ admin_id: { _id: ctx.otherUserId } }])
ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
.withArgs(ctx.adminUser._id)
.resolves([{ admin_id: { _id: ctx.otherUserId } }])
await ctx.SubscriptionUpdater.promises.updateAdmin(
ctx.subscription,
ctx.otherUserId
)
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.adminUser._id,
{ group_role: 'manager' }
)
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.otherUserId,
{ group_role: 'admin' }
)
})
it('should not fire setUserProperties for group_role on non-group subscriptions', async function (ctx) {
await ctx.SubscriptionUpdater.promises.updateAdmin(
ctx.subscription,
ctx.otherUserId
)
expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
'setUserProperties',
sinon.match.any,
sinon.match.has('group_role')
)
})
it('should remove the manager for non-group subscriptions', async function (ctx) {
await ctx.SubscriptionUpdater.promises.updateAdmin(
ctx.subscription,
@@ -353,6 +392,46 @@ describe('SubscriptionUpdater', function () {
ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
})
it("should fire setUserProperties for both old and new admin's group_role on group subscriptions", async function (ctx) {
ctx.subscription.groupPlan = true
ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
.withArgs(ctx.otherUserId)
.resolves([{ admin_id: { _id: ctx.otherUserId } }])
ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
.withArgs(ctx.adminUser._id)
.resolves([{ admin_id: { _id: ctx.otherUserId } }])
await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
ctx.subscription,
ctx.otherUserId,
false
)
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.adminUser._id,
{ group_role: 'manager' }
)
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
'setUserProperties',
ctx.otherUserId,
{ group_role: 'admin' }
)
})
it('should not fire setUserProperties for group_role on non-group subscriptions', async function (ctx) {
await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
ctx.subscription,
ctx.otherUserId,
false
)
expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
'setUserProperties',
sinon.match.any,
sinon.match.has('group_role')
)
})
it('should clear previousPaymentProvider when clearPreviousPaymentProvider is true', async function (ctx) {
ctx.subscription.paymentProvider = {
id: 'stripe-123',
@@ -101,6 +101,12 @@ describe('UserMembershipHandler', function () {
}),
}
ctx.SubscriptionUpdater = {
promises: {
sendGroupRoleUserProperty: vi.fn().mockResolvedValue(),
},
}
vi.doMock('mongodb-legacy', () => ({
default: { ObjectId },
}))
@@ -136,6 +142,13 @@ describe('UserMembershipHandler', function () {
default: ctx.mongoose,
}))
vi.doMock(
'../../../../app/src/Features/Subscription/SubscriptionUpdater',
() => ({
default: ctx.SubscriptionUpdater,
})
)
ctx.UserMembershipHandler = (await import(modulePath)).default
})