diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
index fae13eec00..6ea5cd8108 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js
@@ -274,6 +274,27 @@ async function _notifySupportSubscriptionDeletionSkipped(
await Modules.promises.hooks.fire('sendSupportRequest', data)
}
+async function _notifySubscriptionDowngradeSkipped(
+ subscription,
+ recurlySubscription,
+ subject
+) {
+ const adminUrl = `${Settings.adminUrl + '/admin/user/' + subscription.admin_id}`
+ const groupUrl = `${Settings.adminUrl + '/admin/group/' + subscription._id}`
+ let message = `\n**Recurly account:** ${recurlySubscription?.account?.url}`
+ message += `\n**Group admin:** ${adminUrl}`
+ message += `\n**Group:** ${groupUrl}`
+
+ const data = {
+ subject,
+ inbox: 'support',
+ tags: 'Group subscription',
+ message,
+ }
+
+ await Modules.promises.hooks.fire('sendSupportRequest', data)
+}
+
async function updateSubscriptionFromRecurly(
recurlySubscription,
subscription,
@@ -353,6 +374,8 @@ async function updateSubscriptionFromRecurly(
return
}
+ const currentSubscriptionPlanCode = subscription.planCode
+
const addOns = recurlySubscription?.subscription_add_ons?.map(addOn => {
return {
addOnCode: addOn.add_on_code,
@@ -371,13 +394,65 @@ async function updateSubscriptionFromRecurly(
}
if (plan.groupPlan) {
+ if (Settings.preventSubscriptionPlanDowngrade) {
+ // We're preventing professional to standard downgrade.
+ // See https://github.com/overleaf/internal/issues/19852
+ const isProfessionalToStandardDowngrade =
+ currentSubscriptionPlanCode.includes('professional') &&
+ plan.planCode.includes('collaborator') &&
+ !plan.planCode.includes('_ibis') &&
+ !plan.planCode.includes('_heron') &&
+ plan.planCode !== 'collaborator-annual_free_trial'
+
+ if (isProfessionalToStandardDowngrade) {
+ try {
+ await _notifySubscriptionDowngradeSkipped(
+ subscription,
+ recurlySubscription,
+ 'Skipped professional to standard downgrade'
+ )
+ } catch (e) {
+ logger.warn(
+ { subscriptionId: subscription._id },
+ 'unable to send notification to support that professional to standard downgrade was skipped'
+ )
+ }
+ subscription.planCode = currentSubscriptionPlanCode
+ }
+ }
+
if (!subscription.groupPlan) {
subscription.member_ids = subscription.member_ids || []
subscription.member_ids.push(subscription.admin_id)
}
subscription.groupPlan = true
- subscription.membersLimit = plan.membersLimit
+
+ if (Settings.preventSubscriptionPlanDowngrade) {
+ // We're preventing automatically downgrading of group member limit
+ // See https://github.com/overleaf/internal/issues/19852
+ if (
+ !subscription.membersLimit ||
+ subscription.membersLimit < plan.membersLimit
+ ) {
+ subscription.membersLimit = plan.membersLimit
+ } else {
+ try {
+ await _notifySubscriptionDowngradeSkipped(
+ subscription,
+ recurlySubscription,
+ 'Skipped group size downgrade'
+ )
+ } catch (e) {
+ logger.warn(
+ { subscriptionId: subscription._id },
+ 'unable to send notification to support that group size downgrade was skipped'
+ )
+ }
+ }
+ } else {
+ subscription.membersLimit = plan.membersLimit
+ }
// Some plans allow adding more seats than the base plan provides.
// This is recorded as a subscription add on.
diff --git a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js
index e0e8d666bf..b9821f46ff 100644
--- a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js
+++ b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js
@@ -431,6 +431,93 @@ describe('SubscriptionUpdater', function () {
assert.notEqual(this.subscription.groupPlan, true)
})
+ describe('prevent subscription plan downgrade', function () {
+ describe('prevent professional to standard plan downgrade', function () {
+ beforeEach(function () {
+ this.recurlyPlan.groupPlan = true
+ this.recurlyPlan.planCode = 'collaborator'
+ this.recurlySubscription.plan.plan_code = 'collaborator'
+ this.groupSubscription.planCode = 'professional'
+ })
+ it('should not downgrade from professional to standard plan when preventSubscriptionPlanDowngrade=true', async function () {
+ this.Settings.preventSubscriptionPlanDowngrade = true
+ await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
+ this.recurlySubscription,
+ this.groupSubscription,
+ {}
+ )
+ this.groupSubscription.planCode.should.equal('professional')
+
+ const adminUrl = `${this.Settings.adminUrl + '/admin/user/' + this.groupSubscription.admin_id}`
+ const groupUrl = `${this.Settings.adminUrl + '/admin/group/' + this.groupSubscription._id}`
+ let message = `\n**Recurly account:** ${this.recurlySubscription?.account?.url}`
+ message += `\n**Group admin:** ${adminUrl}`
+ message += `\n**Group:** ${groupUrl}`
+ expect(this.Modules.promises.hooks.fire).to.have.been.calledOnce
+ expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
+ 'sendSupportRequest',
+ {
+ subject: 'Skipped professional to standard downgrade',
+ inbox: 'support',
+ tags: 'Group subscription',
+ message,
+ }
+ )
+ })
+ it('should downgrade from professional to standard plan when preventSubscriptionPlanDowngrade=false', async function () {
+ this.Settings.preventSubscriptionPlanDowngrade = false
+ await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
+ this.recurlySubscription,
+ this.groupSubscription,
+ {}
+ )
+ this.groupSubscription.planCode.should.equal('collaborator')
+ })
+ })
+
+ describe('prevent decreasing group size', function () {
+ beforeEach(function () {
+ this.groupSubscription.membersLimit = 3
+ this.recurlyPlan.groupPlan = true
+ this.recurlyPlan.membersLimit = 2
+ })
+ it('should not reduce member limits when preventSubscriptionPlanDowngrade=true', async function () {
+ this.Settings.preventSubscriptionPlanDowngrade = true
+ await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
+ this.recurlySubscription,
+ this.groupSubscription,
+ {}
+ )
+ this.groupSubscription.membersLimit.should.equal(3)
+
+ const adminUrl = `${this.Settings.adminUrl + '/admin/user/' + this.groupSubscription.admin_id}`
+ const groupUrl = `${this.Settings.adminUrl + '/admin/group/' + this.groupSubscription._id}`
+ let message = `\n**Recurly account:** ${this.recurlySubscription?.account?.url}`
+ message += `\n**Group admin:** ${adminUrl}`
+ message += `\n**Group:** ${groupUrl}`
+ expect(this.Modules.promises.hooks.fire).to.have.been.calledOnce
+ expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
+ 'sendSupportRequest',
+ {
+ subject: 'Skipped group size downgrade',
+ inbox: 'support',
+ tags: 'Group subscription',
+ message,
+ }
+ )
+ })
+ it('should reduce member limits when preventSubscriptionPlanDowngrade=false', async function () {
+ this.Settings.preventSubscriptionPlanDowngrade = false
+ await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
+ this.recurlySubscription,
+ this.groupSubscription,
+ {}
+ )
+ this.groupSubscription.membersLimit.should.equal(2)
+ })
+ })
+ })
+
describe('when the plan allows adding more seats', function () {
beforeEach(function () {
this.membersLimitAddOn = 'add_on1'