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:',
'
' +
- '- Add more licenses, or
' +
+ (opts.canUseFlexibleLicensing
+ ? '- Add more licenses, or
'
+ : '- Contact us to add more licenses, or
') +
'- Remove inactive users to free up licenses
' +
'
',
]
},
- 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:',
+ '' +
+ (opts.canUseFlexibleLicensing
+ ? '- Add more licenses, or
'
+ : '- Contact us to add more licenses, or
') +
+ '- Remove inactive users to free up licenses
' +
+ '
',
+ ]
+ },
+ 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 () {