From 09cd43eb1732a36f4a622ed0adbc91dd6b9c5dec Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:39:41 -0500 Subject: [PATCH] Merge pull request #36011 from overleaf/jel-domain-capture-sales-email [web] Weekly domain capture digest email GitOrigin-RevId: 6682d1d7fcbb179615dbc009d2da8266113a3b4b --- .../Subscription/LimitationsManager.mjs | 29 ++++++- .../Subscription/LimitationsManager.test.mjs | 79 +++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/services/web/app/src/Features/Subscription/LimitationsManager.mjs b/services/web/app/src/Features/Subscription/LimitationsManager.mjs index 6c00579bc1..6e7ab80dbe 100644 --- a/services/web/app/src/Features/Subscription/LimitationsManager.mjs +++ b/services/web/app/src/Features/Subscription/LimitationsManager.mjs @@ -172,14 +172,38 @@ async function userIsMemberOfGroupSubscription(user) { /** * @param {any} subscription */ -function teamHasReachedMemberLimit(subscription) { - const currentTotal = +function currentGroupTotal(subscription) { + return ( (subscription.member_ids || []).length + (subscription.teamInvites || []).length + (subscription.invited_emails || []).length + ) +} + +/** + * @param {any} subscription + */ +function teamHasReachedMemberLimit(subscription) { + const currentTotal = currentGroupTotal(subscription) return currentTotal >= subscription.membersLimit } +/** + * Calculate the number of remaining seats for a group subscription. + * + * @param {any} subscriptionId - The subscription ID + * @returns {Promise} - Remaining seats (membersLimit - current members/invites) + */ +async function remainingSeatsForGroup(subscriptionId) { + const subscription = + await SubscriptionLocator.promises.getSubscription(subscriptionId) + if (!subscription) { + logger.warn({ subscriptionId }, 'no subscription found') + throw new Error('no subscription found') + } + const currentTotal = currentGroupTotal(subscription) + return subscription.membersLimit - currentTotal +} /** * @param {any} subscriptionId @@ -241,6 +265,7 @@ const LimitationsManager = { userHasSubscription, userIsMemberOfGroupSubscription, hasGroupMembersLimitReached, + remainingSeatsForGroup, }, } diff --git a/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs b/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs index cbc0f8a0c3..ff3e7e632c 100644 --- a/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs +++ b/services/web/test/unit/src/Subscription/LimitationsManager.test.mjs @@ -636,4 +636,83 @@ describe('LimitationsManager', function () { expect(limitReached).to.be.true }) }) + + describe('remainingSeatsForGroup', function () { + beforeEach(function (ctx) { + ctx.subscriptionId = '12312' + ctx.subscription = { + membersLimit: 10, + member_ids: ['u1', 'u2', 'u3'], + teamInvites: [ + { email: 'bob@example.com', sentAt: new Date(), token: 'hey' }, + ], + invited_emails: ['pending@example.com'], + } + }) + + it('should return the number of remaining seats', async function (ctx) { + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription + ) + // 10 - (3 members + 1 teamInvite + 1 invited_email) = 5 + const result = + await ctx.LimitationsManager.promises.remainingSeatsForGroup( + ctx.subscriptionId + ) + expect(result).to.equal(5) + }) + + it('should return 0 when the limit is exactly reached', async function (ctx) { + ctx.subscription.membersLimit = 5 + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription + ) + // 5 - (3 members + 1 teamInvite + 1 invited_email) = 0 + const result = + await ctx.LimitationsManager.promises.remainingSeatsForGroup( + ctx.subscriptionId + ) + expect(result).to.equal(0) + }) + + it('should return a negative number when the limit has been exceeded', async function (ctx) { + ctx.subscription.membersLimit = 4 + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription + ) + // 4 - (3 members + 1 teamInvite + 1 invited_email) = -1 + const result = + await ctx.LimitationsManager.promises.remainingSeatsForGroup( + ctx.subscriptionId + ) + expect(result).to.equal(-1) + }) + + it('should treat missing member_ids, teamInvites, and invited_emails as empty', async function (ctx) { + ctx.subscription.member_ids = undefined + ctx.subscription.teamInvites = undefined + ctx.subscription.invited_emails = undefined + ctx.SubscriptionLocator.promises.getSubscription.resolves( + ctx.subscription + ) + const result = + await ctx.LimitationsManager.promises.remainingSeatsForGroup( + ctx.subscriptionId + ) + expect(result).to.equal(10) + }) + + it('should throw when no subscription is found', async function (ctx) { + ctx.SubscriptionLocator.promises.getSubscription.resolves(null) + let error + try { + await ctx.LimitationsManager.promises.remainingSeatsForGroup( + ctx.subscriptionId + ) + } catch (e) { + error = e + } + expect(error?.message).to.equal('no subscription found') + }) + }) })