From 1b80f172d72a3855bce2a0cdc3f32eb043198693 Mon Sep 17 00:00:00 2001 From: Miguel Serrano Date: Mon, 11 Nov 2024 11:18:06 +0100 Subject: [PATCH] [web] Prevent subscription downgrades (#19895) * Notify support when subscription deletion skipped GitOrigin-RevId: b0ff548b4e1bf5843a96885b3176fdf11a49a2e1 --- .../Subscription/SubscriptionUpdater.js | 77 +++++++++++++++- .../Subscription/SubscriptionUpdaterTests.js | 87 +++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) 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'