From 281b5fe25c08c73081c75c8b38f5caeec15122ad Mon Sep 17 00:00:00 2001 From: Kate Crichton Date: Thu, 30 Jul 2026 13:05:54 +0100 Subject: [PATCH] Merge pull request #34835 from overleaf/kc-email-member-limit-reached [web] Email group admin when group reaches member limit GitOrigin-RevId: 6b7b12a2c309e7d9b4314e8db51969d73864b7ed --- .../app/src/Features/Email/EmailBuilder.mjs | 52 +++++++++++-- services/web/app/src/models/Subscription.mjs | 1 + .../test/unit/src/Email/EmailBuilder.test.mjs | 74 ++++++++++++++++++- 3 files changed, 119 insertions(+), 8 deletions(-) diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs index 919a0f7c8f..190560e746 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.mjs +++ b/services/web/app/src/Features/Email/EmailBuilder.mjs @@ -1192,10 +1192,10 @@ templates.taxIdInvalidNonVat = taxIdInvalidTemplate({ templates.groupMemberLimitWarning = ctaTemplate({ subject(opts) { - return `Action needed: Your Overleaf group is nearly out of licenses` + return `Action needed: your Overleaf group is nearly out of licenses` }, title(opts) { - return `Action needed: Your Overleaf group is nearly out of licenses` + return `Action needed: your Overleaf group is nearly out of licenses` }, greeting(opts) { return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,' @@ -1209,16 +1209,54 @@ templates.groupMemberLimitWarning = ctaTemplate({ 'Once all licenses are used, new users won’t be able to join.', 'What you can do now:', '', ] }, - ctaText() { - return 'Add licenses' + ctaText(opts) { + return opts.canUseFlexibleLicensing ? 'Add licenses' : 'Contact us' }, - ctaURL() { - return `${settings.siteUrl}/user/subscription/group/add-users` + ctaURL(opts) { + return opts.canUseFlexibleLicensing + ? `${settings.siteUrl}/user/subscription/group/add-users` + : `${settings.siteUrl}/contact` + }, +}) + +templates.groupMemberLimitReached = ctaTemplate({ + subject(opts) { + return `Action needed: your Overleaf group is out of licenses` + }, + title(opts) { + return `Action needed: your Overleaf group is out of licenses` + }, + greeting(opts) { + return opts.firstName ? `Hi ${opts.firstName},` : 'Hi there,' + }, + message(opts) { + return [ + `Your Overleaf group ${opts.groupName} has used all ${opts.membersLimit} of its licenses.`, + 'Because domain capture is enabled, new users from your domain can no longer ' + + 'join automatically.', + 'What you can do now:', + '', + ] + }, + ctaText(opts) { + return opts.canUseFlexibleLicensing ? 'Add licenses' : 'Contact us' + }, + ctaURL(opts) { + return opts.canUseFlexibleLicensing + ? `${settings.siteUrl}/user/subscription/group/add-users` + : `${settings.siteUrl}/contact` }, }) diff --git a/services/web/app/src/models/Subscription.mjs b/services/web/app/src/models/Subscription.mjs index 2e811b450d..fcd886d058 100644 --- a/services/web/app/src/models/Subscription.mjs +++ b/services/web/app/src/models/Subscription.mjs @@ -58,6 +58,7 @@ export const SubscriptionSchema = new Schema( managedUsersEnabled: { type: Boolean, default: false }, membersLimit: { type: Number, default: 0 }, membersLimitNotificationSent: { type: Boolean, default: false }, + membersLimitReachedNotificationSent: { type: Boolean, default: false }, customAccount: Boolean, features: { managedUsers: { type: Boolean, default: true }, diff --git a/services/web/test/unit/src/Email/EmailBuilder.test.mjs b/services/web/test/unit/src/Email/EmailBuilder.test.mjs index ee8afdcbe8..3280ed1b10 100644 --- a/services/web/test/unit/src/Email/EmailBuilder.test.mjs +++ b/services/web/test/unit/src/Email/EmailBuilder.test.mjs @@ -995,6 +995,7 @@ describe('EmailBuilder', function () { currentMembers: 9, membersLimit: 10, remainingSeats: 1, + canUseFlexibleLicensing: true, } ctx.email = ctx.EmailBuilder.buildEmail( 'groupMemberLimitWarning', @@ -1013,7 +1014,7 @@ describe('EmailBuilder', function () { const dom = cheerio.load(ctx.email.html) const plainText = dom.text() expect(ctx.email.subject).to.equal( - 'Action needed: Your Overleaf group is nearly out of licenses' + 'Action needed: your Overleaf group is nearly out of licenses' ) expect(ctx.email.html).to.exist expect(ctx.email.html).to.contain( @@ -1048,6 +1049,77 @@ describe('EmailBuilder', function () { expect(ctx.email.text).to.contain(ctx.expectedUrl) }) }) + + describe('without flexible licensing', function () { + it('should point the CTA at support instead of add-users', function (ctx) { + ctx.opts.canUseFlexibleLicensing = false + ctx.email = ctx.EmailBuilder.buildEmail( + 'groupMemberLimitWarning', + ctx.opts + ) + const dom = cheerio.load(ctx.email.html) + const buttonLink = dom('a:contains("Contact us")') + expect(buttonLink.attr('href')).to.equal( + `${ctx.settings.siteUrl}/contact` + ) + expect(ctx.email.html).to.not.contain(ctx.expectedUrl) + }) + }) + }) + + describe('groupMemberLimitReached', function () { + beforeEach(function (ctx) { + ctx.opts = { + to: 'example@overleaf.com', + groupName: 'Example Group', + firstName: 'Joe', + membersLimit: 10, + canUseFlexibleLicensing: true, + } + ctx.email = ctx.EmailBuilder.buildEmail( + 'groupMemberLimitReached', + ctx.opts + ) + ctx.expectedUrl = `${ctx.settings.siteUrl}/user/subscription/group/add-users` + }) + + it('should build the email', function (ctx) { + expect(ctx.email.html).to.exist + expect(ctx.email.text).to.exist + }) + + it('should describe the at-capacity state and CTA', function (ctx) { + const dom = cheerio.load(ctx.email.html) + expect(ctx.email.subject).to.equal( + 'Action needed: your Overleaf group is out of licenses' + ) + expect(ctx.email.html).to.contain( + `Your Overleaf group ${ctx.opts.groupName} has used all ${ctx.opts.membersLimit} of its licenses.` + ) + expect(ctx.email.html).to.contain( + 'new users from your domain can no longer' + ) + expect(ctx.email.html).to.contain('What you can do now:') + const buttonLink = dom('a:contains("Add licenses")') + expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl) + expect(ctx.email.text).to.contain(ctx.expectedUrl) + }) + + describe('without flexible licensing', function () { + it('should point the CTA at support instead of add-users', function (ctx) { + ctx.opts.canUseFlexibleLicensing = false + ctx.email = ctx.EmailBuilder.buildEmail( + 'groupMemberLimitReached', + ctx.opts + ) + const dom = cheerio.load(ctx.email.html) + const buttonLink = dom('a:contains("Contact us")') + expect(buttonLink.attr('href')).to.equal( + `${ctx.settings.siteUrl}/contact` + ) + expect(ctx.email.html).to.not.contain(ctx.expectedUrl) + }) + }) }) describe('groupDomainCapturedByGroupChanged', function () {