Przeglądaj źródła

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

Enable group plan upgrade for legacy plans

GitOrigin-RevId: 9dde0371eeb791a6331ab50733fd457e28837ba9
Liangjun Song 1 rok temu
rodzic
commit
d893bb76cf

+ 21 - 8
services/web/app/src/Features/Subscription/RecurlyEntities.js

@@ -221,22 +221,35 @@ class RecurlySubscription {
    * Upgrade group plan with the plan code provided
    * Upgrade group plan with the plan code provided
    *
    *
    * @param {string} newPlanCode
    * @param {string} newPlanCode
+   * @param {number} membersLimit
    * @return {RecurlySubscriptionChangeRequest}
    * @return {RecurlySubscriptionChangeRequest}
    */
    */
-  getRequestForFlexibleLicensingGroupPlanUpgrade(newPlanCode) {
+  getRequestForGroupPlanUpgrade(newPlanCode, membersLimit) {
     // Ensure all the existing add-ons are added to the new plan
     // 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({
     return new RecurlySubscriptionChangeRequest({
       subscription: this,
       subscription: this,
       timeframe: 'now',
       timeframe: 'now',
-      addOnUpdates: existingAddOns,
+      addOnUpdates: addOns,
       planCode: newPlanCode,
       planCode: newPlanCode,
     })
     })
   }
   }

+ 23 - 10
services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js

@@ -8,11 +8,6 @@ const RecurlyClient = require('./RecurlyClient')
 const PlansLocator = require('./PlansLocator')
 const PlansLocator = require('./PlansLocator')
 const SubscriptionHandler = require('./SubscriptionHandler')
 const SubscriptionHandler = require('./SubscriptionHandler')
 
 
-const PLAN_UPGRADE_MAP = {
-  group_collaborator: 'group_professional',
-  group_collaborator_educational: 'group_professional_educational',
-}
-
 async function removeUserFromGroup(subscriptionId, userIdToRemove) {
 async function removeUserFromGroup(subscriptionId, userIdToRemove) {
   await SubscriptionUpdater.promises.removeUserFromGroup(
   await SubscriptionUpdater.promises.removeUserFromGroup(
     subscriptionId,
     subscriptionId,
@@ -152,11 +147,16 @@ async function createAddSeatsSubscriptionChange(req) {
 }
 }
 
 
 async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
 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')
     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) {
 async function _getGroupPlanUpgradeChangeRequest(ownerId) {
@@ -168,11 +168,24 @@ async function _getGroupPlanUpgradeChangeRequest(ownerId) {
   const recurlySubscription = await RecurlyClient.promises.getSubscription(
   const recurlySubscription = await RecurlyClient.promises.getSubscription(
     olSubscription.recurlySubscription_id
     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) {
 async function getGroupPlanUpgradePreview(ownerId) {
   const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
   const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
   const subscriptionChange =
   const subscriptionChange =
@@ -182,7 +195,7 @@ async function getGroupPlanUpgradePreview(ownerId) {
     {
     {
       type: 'group-plan-upgrade',
       type: 'group-plan-upgrade',
       prevPlan: {
       prevPlan: {
-        name: subscriptionChange.subscription.planName,
+        name: _getPlanNameForDisplay(subscriptionChange.subscription),
       },
       },
     },
     },
     subscriptionChange,
     subscriptionChange,

+ 1 - 1
services/web/locales/en.json

@@ -863,7 +863,7 @@
   "group_managed_by_group_administrator": "User accounts in this group are managed by the group administrator.",
   "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_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_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": "You’re on the <0>__currentPlan__</0> plan and you’re upgrading to the <0>__nextPlan__</0> plan. If you’re interested in a site-wide Overleaf Commons plan please <1>get in touch</1>.",
+  "group_plan_upgrade_description": "You’re on the <0>__currentPlan__</0> plan and you’re upgrading to the <0>__nextPlan__</0> plan. If you’re 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_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_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.",
   "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.",

+ 27 - 0
services/web/test/unit/src/Subscription/RecurlyEntitiesTest.js

@@ -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 () {
       describe('without add-ons', function () {
         beforeEach(function () {
         beforeEach(function () {
           const { RecurlySubscription } = this.RecurlyEntities
           const { RecurlySubscription } = this.RecurlyEntities

+ 29 - 6
services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js

@@ -42,9 +42,7 @@ describe('SubscriptionGroupHandler', function () {
         },
         },
       ],
       ],
       getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest),
       getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest),
-      getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
-        .stub()
-        .returns(this.changeRequest),
+      getRequestForGroupPlanUpgrade: sinon.stub().returns(this.changeRequest),
     }
     }
 
 
     this.SubscriptionLocator = {
     this.SubscriptionLocator = {
@@ -108,7 +106,7 @@ describe('SubscriptionGroupHandler', function () {
     }
     }
 
 
     this.PlansLocator = {
     this.PlansLocator = {
-      findLocalPlanInSettings: sinon.stub(this.localPlanInSettings),
+      findLocalPlanInSettings: sinon.stub().returns(this.localPlanInSettings),
     }
     }
 
 
     this.SubscriptionHandler = {
     this.SubscriptionHandler = {
@@ -373,7 +371,7 @@ describe('SubscriptionGroupHandler', function () {
   })
   })
 
 
   describe('upgradeGroupPlan', 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
       this.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves({ groupPlan: true, planCode: 'group_collaborator' })
         .resolves({ groupPlan: true, planCode: 'group_collaborator' })
@@ -386,7 +384,23 @@ describe('SubscriptionGroupHandler', function () {
         .should.equal(true)
         .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
       this.SubscriptionLocator.promises.getUsersSubscription = sinon
         .stub()
         .stub()
         .resolves({ groupPlan: true, planCode: 'group_professional' })
         .resolves({ groupPlan: true, planCode: 'group_professional' })
@@ -394,6 +408,15 @@ describe('SubscriptionGroupHandler', function () {
         this.Handler.promises.upgradeGroupPlan(this.user_id)
         this.Handler.promises.upgradeGroupPlan(this.user_id)
       ).to.be.rejectedWith('Not eligible for group plan upgrade')
       ).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 () {
   describe('getGroupPlanUpgradePreview', function () {