From f959329337f5120aa65be8b8603a03c5659e7549 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Fri, 5 Dec 2025 16:27:43 +0200 Subject: [PATCH] Merge pull request #30002 from overleaf/ii-domain-capture-send-email-to-admin [web] Send member limit notification email to group admin GitOrigin-RevId: 520fef0dd3eab52bfb766f6f116cb4b8533b4cae --- .../app/src/Features/Email/EmailBuilder.mjs | 30 +++++++++ services/web/app/src/models/Subscription.mjs | 1 + .../test/unit/src/Email/EmailBuilder.test.mjs | 66 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs index 751abf11c0..3cececcfcb 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.mjs +++ b/services/web/app/src/Features/Email/EmailBuilder.mjs @@ -1023,6 +1023,36 @@ templates.taxExemptCertificateRequired = NoCTAEmailTemplate({ }, }) +templates.groupMemberLimitWarning = ctaTemplate({ + subject(opts) { + return `${opts.groupName || 'Your group'} is approaching its member limit` + }, + title(opts) { + return `${opts.groupName || 'Your group'} is approaching its member limit` + }, + greeting(opts) { + return opts.firstName ? `Hi ${opts.firstName},` : 'Hi,' + }, + message(opts) { + return [ + `Your group "${opts.groupName}" is approaching its member limit.`, + `Current usage: ${opts.currentMembers} of ${opts.membersLimit} licenses used (${opts.remainingSeats} remaining)`, + 'With domain capture enabled, users with verified email addresses from your domain can automatically join the group via SSO. Once the member limit is reached, new users will be blocked from joining.', + ] + }, + secondaryMessage() { + return [ + 'To ensure uninterrupted access for your users, consider adding more licenses or removing inactive members.', + ] + }, + ctaText() { + return 'Add licenses' + }, + ctaURL() { + return `${settings.siteUrl}/user/subscription/group/add-users` + }, +}) + function _formatUserNameAndEmail(user, placeholder) { if (user.first_name && user.last_name) { const fullName = `${user.first_name} ${user.last_name}` diff --git a/services/web/app/src/models/Subscription.mjs b/services/web/app/src/models/Subscription.mjs index 74e6c1684d..9134541192 100644 --- a/services/web/app/src/models/Subscription.mjs +++ b/services/web/app/src/models/Subscription.mjs @@ -39,6 +39,7 @@ export const SubscriptionSchema = new Schema( domainCaptureEnabled: { type: Boolean, default: false }, managedUsersEnabled: { type: Boolean, default: false }, membersLimit: { type: Number, default: 0 }, + membersLimitNotificationSent: { 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 607a706530..9e010f57b9 100644 --- a/services/web/test/unit/src/Email/EmailBuilder.test.mjs +++ b/services/web/test/unit/src/Email/EmailBuilder.test.mjs @@ -976,6 +976,72 @@ describe('EmailBuilder', function () { }) }) }) + + describe('groupMemberLimitWarning', function () { + beforeEach(function (ctx) { + ctx.emailAddress = 'example@overleaf.com' + ctx.opts = { + to: ctx.emailAddress, + groupName: 'Example Group', + firstName: 'Joe', + currentMembers: 9, + membersLimit: 10, + remainingSeats: 1, + } + ctx.email = ctx.EmailBuilder.buildEmail( + 'groupMemberLimitWarning', + 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 + }) + + describe('HTML email', function () { + it('should include a CTA button and a fallback CTA link', function (ctx) { + const dom = cheerio.load(ctx.email.html) + const plainText = dom.text() + expect(ctx.email.subject).to.equal( + `${ctx.opts.groupName} is approaching its member limit` + ) + expect(ctx.email.html).to.exist + expect(ctx.email.html).to.contain( + `Your group "${ctx.opts.groupName}" is approaching its member limit.` + ) + expect(plainText).to.contain( + `Current usage: ${ctx.opts.currentMembers} of ` + + `${ctx.opts.membersLimit} licenses used ` + + `(${ctx.opts.remainingSeats} remaining)` + ) + expect(ctx.email.html).to.contain( + 'With domain capture enabled, users with verified email ' + + 'addresses from your domain can automatically join the group ' + + 'via SSO. Once the member limit is reached, new users will ' + + 'be blocked from joining.' + ) + expect(ctx.email.html).to.contain( + 'To ensure uninterrupted access for your users, consider ' + + 'adding more licenses or removing inactive members.' + ) + const buttonLink = dom('td a') + expect(buttonLink).to.exist + expect(buttonLink.attr('href')).to.equal(ctx.expectedUrl) + const fallback = dom('.force-overleaf-style').last() + expect(fallback).to.exist + const fallbackLink = fallback.html().replace(/&/g, '&') + expect(fallbackLink).to.contain(ctx.expectedUrl) + }) + }) + + describe('plain text email', function () { + it('should contain the CTA link', function (ctx) { + expect(ctx.email.text).to.contain(ctx.expectedUrl) + }) + }) + }) }) }) })