Merge pull request #22816 from overleaf/enable-group-plan-upgrade-for-legacy-plans

Enable group plan upgrade for legacy plans

GitOrigin-RevId: 9dde0371eeb791a6331ab50733fd457e28837ba9
This commit is contained in:
Liangjun Song
2025-01-29 09:05:13 +00:00
committed by Copybot
parent eba4418672
commit d893bb76cf
5 changed files with 101 additions and 25 deletions
@@ -221,22 +221,35 @@ class RecurlySubscription {
* Upgrade group plan with the plan code provided
*
* @param {string} newPlanCode
* @param {number} membersLimit
* @return {RecurlySubscriptionChangeRequest}
*/
getRequestForFlexibleLicensingGroupPlanUpgrade(newPlanCode) {
getRequestForGroupPlanUpgrade(newPlanCode, membersLimit) {
// Ensure all the existing add-ons are added to the new plan
const existingAddOns = this.addOns.map(
addOn =>
new RecurlySubscriptionAddOnUpdate({
code: addOn.code,
quantity: addOn.quantity,
})
// Except for the additional license, which will be added below
const addOns = this.addOns
.filter(addOn => addOn.code !== 'additional-license')
.map(
addOn =>
new RecurlySubscriptionAddOnUpdate({
code: addOn.code,
quantity: addOn.quantity,
})
)
// Get the number of licenses from the membersLimit field in the Subscription model
// This is necessary because legacy group plans do not fully use add-ons to represent seats
addOns.push(
new RecurlySubscriptionAddOnUpdate({
code: 'additional-license',
quantity: membersLimit,
})
)
return new RecurlySubscriptionChangeRequest({
subscription: this,
timeframe: 'now',
addOnUpdates: existingAddOns,
addOnUpdates: addOns,
planCode: newPlanCode,
})
}
@@ -8,11 +8,6 @@ const RecurlyClient = require('./RecurlyClient')
const PlansLocator = require('./PlansLocator')
const SubscriptionHandler = require('./SubscriptionHandler')
const PLAN_UPGRADE_MAP = {
group_collaborator: 'group_professional',
group_collaborator_educational: 'group_professional_educational',
}
async function removeUserFromGroup(subscriptionId, userIdToRemove) {
await SubscriptionUpdater.promises.removeUserFromGroup(
subscriptionId,
@@ -152,11 +147,16 @@ async function createAddSeatsSubscriptionChange(req) {
}
async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
if (!Object.keys(PLAN_UPGRADE_MAP).includes(subscription.planCode)) {
if (
subscription.planCode.includes('professional') ||
!subscription.groupPlan
) {
throw new Error('Not eligible for group plan upgrade')
}
return PLAN_UPGRADE_MAP[subscription.planCode]
return subscription.planCode.includes('educational')
? 'group_professional_educational'
: 'group_professional'
}
async function _getGroupPlanUpgradeChangeRequest(ownerId) {
@@ -168,11 +168,24 @@ async function _getGroupPlanUpgradeChangeRequest(ownerId) {
const recurlySubscription = await RecurlyClient.promises.getSubscription(
olSubscription.recurlySubscription_id
)
return recurlySubscription.getRequestForFlexibleLicensingGroupPlanUpgrade(
newPlanCode
return recurlySubscription.getRequestForGroupPlanUpgrade(
newPlanCode,
olSubscription.membersLimit
)
}
function _getPlanNameForDisplay(subscription) {
if (/^group_collaborator_\d+_enterprise$/.test(subscription.planCode)) {
return 'Overleaf Standard Group'
}
if (/^group_collaborator_\d+_educational$/.test(subscription.planCode)) {
return 'Overleaf Standard Group Educational'
}
return subscription.planName
}
async function getGroupPlanUpgradePreview(ownerId) {
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
const subscriptionChange =
@@ -182,7 +195,7 @@ async function getGroupPlanUpgradePreview(ownerId) {
{
type: 'group-plan-upgrade',
prevPlan: {
name: subscriptionChange.subscription.planName,
name: _getPlanNameForDisplay(subscriptionChange.subscription),
},
},
subscriptionChange,
+1 -1
View File
@@ -863,7 +863,7 @@
"group_managed_by_group_administrator": "User accounts in this group are managed by the group administrator.",
"group_plan_admins_can_easily_add_and_remove_users_from_a_group": "Group plan admins can easily add and remove users from a group. For site-wide plans, users are automatically upgraded when they register or add their email address to Overleaf (domain-based enrollment or SSO).",
"group_plan_tooltip": "You are on the __plan__ plan as a member of a group subscription. Click to find out how to make the most of your Overleaf premium features.",
"group_plan_upgrade_description": "Youre on the <0>__currentPlan__</0> plan and youre upgrading to the <0>__nextPlan__</0> plan. If youre interested in a site-wide Overleaf Commons plan please <1>get in touch</1>.",
"group_plan_upgrade_description": "Youre on the <0>__currentPlan__</0> plan and youre upgrading to the <0>__nextPlan__</0> plan. If youre interested in a site-wide Overleaf Commons plan, please <1>get in touch</1>.",
"group_plan_with_name_tooltip": "You are on the __plan__ plan as a member of a group subscription, __groupName__. Click to find out how to make the most of your Overleaf premium features.",
"group_professional": "Group Professional",
"group_sso_configuration_idp_metadata": "The information you provide here comes from your Identity Provider (IdP). This is often referred to as its <0>SAML metadata</0>. You can add this manually or click <1>Import IdP metadata</1> to import an XML file.",
@@ -246,6 +246,33 @@ describe('RecurlyEntities', function () {
})
})
describe('getRequestForGroupPlanUpgrade()', function () {
it('returns a correct change request', function () {
const changeRequest = this.subscription.getRequestForGroupPlanUpgrade(
'test_plan_code',
10
)
const addOns = [
new RecurlySubscriptionAddOnUpdate({
code: 'add-on-code',
quantity: 1,
}),
new RecurlySubscriptionAddOnUpdate({
code: 'additional-license',
quantity: 10,
}),
]
expect(changeRequest).to.deep.equal(
new RecurlySubscriptionChangeRequest({
subscription: this.subscription,
timeframe: 'now',
addOnUpdates: addOns,
planCode: 'test_plan_code',
})
)
})
})
describe('without add-ons', function () {
beforeEach(function () {
const { RecurlySubscription } = this.RecurlyEntities
@@ -42,9 +42,7 @@ describe('SubscriptionGroupHandler', function () {
},
],
getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest),
getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
.stub()
.returns(this.changeRequest),
getRequestForGroupPlanUpgrade: sinon.stub().returns(this.changeRequest),
}
this.SubscriptionLocator = {
@@ -108,7 +106,7 @@ describe('SubscriptionGroupHandler', function () {
}
this.PlansLocator = {
findLocalPlanInSettings: sinon.stub(this.localPlanInSettings),
findLocalPlanInSettings: sinon.stub().returns(this.localPlanInSettings),
}
this.SubscriptionHandler = {
@@ -373,7 +371,7 @@ describe('SubscriptionGroupHandler', function () {
})
describe('upgradeGroupPlan', function () {
it('should upgrade the subscription', async function () {
it('should upgrade the subscription for flexible licensing group plans', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({ groupPlan: true, planCode: 'group_collaborator' })
@@ -386,7 +384,23 @@ describe('SubscriptionGroupHandler', function () {
.should.equal(true)
})
it('should fail the upgrade if not eligible', async function () {
it('should upgrade the subscription for legacy group plans', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
groupPlan: true,
planCode: 'group_collaborator_10_educational',
})
await this.Handler.promises.upgradeGroupPlan(this.user_id)
this.RecurlyClient.promises.applySubscriptionChangeRequest
.calledWith(this.changeRequest)
.should.equal(true)
this.SubscriptionHandler.promises.syncSubscription
.calledWith({ uuid: this.changeRequest.subscription.id }, this.user_id)
.should.equal(true)
})
it('should fail the upgrade if is professional already', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({ groupPlan: true, planCode: 'group_professional' })
@@ -394,6 +408,15 @@ describe('SubscriptionGroupHandler', function () {
this.Handler.promises.upgradeGroupPlan(this.user_id)
).to.be.rejectedWith('Not eligible for group plan upgrade')
})
it('should fail the upgrade if not group plan', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({ groupPlan: false, planCode: 'test_plan_code' })
await expect(
this.Handler.promises.upgradeGroupPlan(this.user_id)
).to.be.rejectedWith('Not eligible for group plan upgrade')
})
})
describe('getGroupPlanUpgradePreview', function () {