+
', function () {
+ beforeEach(function () {
+ this.totalLicenses = 2
+ this.preview = {
+ change: {
+ type: 'group-plan-upgrade',
+ prevPlan: { name: 'Overleaf Standard Group' },
+ },
+ currency: 'USD',
+ immediateCharge: { subtotal: 353.99, tax: 70.8, total: 424.79 },
+ paymentMethod: 'Visa **** 1111',
+ nextPlan: { annual: true },
+ nextInvoice: {
+ date: '2025-11-05T11:35:32.000Z',
+ plan: { name: 'Overleaf Professional Group', amount: 0 },
+ addOns: [
+ {
+ code: 'additional-license',
+ name: 'Seat',
+ quantity: 2,
+ unitAmount: 399,
+ amount: 798,
+ },
+ ],
+ subtotal: 798,
+ tax: { rate: 0.2, amount: 159.6 },
+ total: 957.6,
+ },
+ }
+
+ cy.window().then(win => {
+ win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
+ win.metaAttributesCache.set('ol-totalLicenses', this.totalLicenses)
+ win.metaAttributesCache.set('ol-subscriptionChangePreview', this.preview)
+ })
+
+ cy.mount(
+
+
+
+ )
+ })
+
+ it('shows the group name', function () {
+ cy.findByTestId('group-heading').within(() => {
+ cy.findByRole('heading', { name: 'My Awesome Team' })
+ })
+ })
+
+ it('shows the "Add more users to my plan" label', function () {
+ cy.findByText(/add more users to my plan/i).should(
+ 'have.attr',
+ 'href',
+ '/user/subscription/group/add-users'
+ )
+ })
+
+ it('shows the "Upgrade" and "Cancel" buttons', function () {
+ cy.findByRole('button', { name: /upgrade/i })
+ cy.findByRole('button', { name: /cancel/i }).should(
+ 'have.attr',
+ 'href',
+ '/user/subscription'
+ )
+ })
+
+ describe('shows plan details', function () {
+ it('shows per user price', function () {
+ cy.findByTestId('per-user-price').within(() => {
+ cy.findByText('$399')
+ })
+ })
+
+ it('shows additional features', function () {
+ cy.findByText(/unlimited collaborators per project/i)
+ cy.findByText(/sso/i)
+ cy.findByText(/managed user accounts/i)
+ })
+ })
+
+ describe('shows upgrade summary', function () {
+ it('shows subtotal, tax and total price', function () {
+ cy.findByTestId('subtotal').within(() => {
+ cy.findByText('$353.99')
+ })
+ cy.findByTestId('tax').within(() => {
+ cy.findByText('$70.80')
+ })
+ cy.findByTestId('total').within(() => {
+ cy.findByText('$424.79')
+ })
+ })
+
+ it('shows total users', function () {
+ cy.findByText(/you have 2 users on your subscription./i)
+ })
+ })
+
+ describe('submit upgrade request', function () {
+ it('request succeeded', function () {
+ cy.intercept('POST', '/user/subscription/group/upgrade-subscription', {
+ statusCode: 200,
+ }).as('upgradeRequest')
+ cy.findByRole('button', { name: /upgrade/i }).click()
+ cy.findByText(/you’ve upgraded your plan!/i)
+ })
+
+ it('request failed', function () {
+ cy.intercept('POST', '/user/subscription/group/upgrade-subscription', {
+ statusCode: 400,
+ }).as('upgradeRequest')
+ cy.findByRole('button', { name: /upgrade/i }).click()
+ cy.findByText(/something went wrong/i)
+ })
+ })
+})
diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs
index 11771f3224..22944f0683 100644
--- a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs
+++ b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs
@@ -56,6 +56,9 @@ describe('SubscriptionGroupController', function () {
.stub()
.resolves(this.createSubscriptionChangeData),
ensureFlexibleLicensingEnabled: sinon.stub().resolves(),
+ getGroupPlanUpgradePreview: sinon
+ .stub()
+ .resolves(this.previewSubscriptionChangeData),
},
}
@@ -496,4 +499,77 @@ describe('SubscriptionGroupController', function () {
this.Controller.flexibleLicensingSplitTest(this.req, res, next, done)
})
})
+
+ describe('subscriptionUpgradePage', function () {
+ it('should render "subscription upgrade" page', function (done) {
+ const olSubscription = { membersLimit: 1, teamName: 'test team' }
+ this.SubscriptionModel.Subscription.findOne = () => {
+ return {
+ exec: () => olSubscription,
+ }
+ }
+
+ const res = {
+ render: (page, data) => {
+ this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview
+ .calledWith(this.req.session.user._id)
+ .should.equal(true)
+ page.should.equal('subscriptions/upgrade-group-subscription-react')
+ data.totalLicenses.should.equal(olSubscription.membersLimit)
+ data.groupName.should.equal(olSubscription.teamName)
+ data.changePreview.should.equal(this.previewSubscriptionChangeData)
+ done()
+ },
+ }
+
+ this.Controller.subscriptionUpgradePage(this.req, res)
+ })
+
+ it('should redirect if failed to generate preview', function (done) {
+ this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
+ .stub()
+ .rejects()
+
+ const res = {
+ redirect: url => {
+ url.should.equal('/user/subscription')
+ done()
+ },
+ }
+
+ this.Controller.subscriptionUpgradePage(this.req, res)
+ })
+ })
+
+ describe('upgradeSubscription', function () {
+ it('should send 200 response', function (done) {
+ this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
+ .stub()
+ .resolves()
+
+ const res = {
+ sendStatus: code => {
+ code.should.equal(200)
+ done()
+ },
+ }
+
+ this.Controller.upgradeSubscription(this.req, res)
+ })
+
+ it('should send 500 response', function (done) {
+ this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon
+ .stub()
+ .rejects()
+
+ const res = {
+ sendStatus: code => {
+ code.should.equal(500)
+ done()
+ },
+ }
+
+ this.Controller.upgradeSubscription(this.req, res)
+ })
+ })
})
diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js
index a9d06130fd..9ac472b9e3 100644
--- a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js
+++ b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js
@@ -28,6 +28,9 @@ describe('SubscriptionGroupHandler', function () {
this.changeRequest = {
timeframe: 'now',
+ subscription: {
+ id: 'test_id',
+ },
}
this.recurlySubscription = {
@@ -39,6 +42,9 @@ describe('SubscriptionGroupHandler', function () {
},
],
getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest),
+ getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
+ .stub()
+ .returns(this.changeRequest),
}
this.SubscriptionLocator = {
@@ -81,6 +87,9 @@ describe('SubscriptionGroupHandler', function () {
quantity: this.recurlySubscription.addOns[0].quantity + this.adding,
},
],
+ subscription: {
+ planName: 'test plan',
+ },
}
this.applySubscriptionChange = {}
@@ -362,4 +371,44 @@ describe('SubscriptionGroupHandler', function () {
})
).to.not.be.rejected
})
+
+ describe('upgradeGroupPlan', function () {
+ it('should upgrade the subscription', async function () {
+ this.SubscriptionLocator.promises.getUsersSubscription = sinon
+ .stub()
+ .resolves({ groupPlan: true, planCode: 'group_collaborator' })
+ 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 not eligible', async function () {
+ this.SubscriptionLocator.promises.getUsersSubscription = sinon
+ .stub()
+ .resolves({ groupPlan: true, planCode: 'group_professional' })
+ await expect(
+ this.Handler.promises.upgradeGroupPlan(this.user_id)
+ ).to.be.rejectedWith('Not eligible for group plan upgrade')
+ })
+ })
+
+ describe('getGroupPlanUpgradePreview', function () {
+ it('should generate preview for subscription upgrade', async function () {
+ this.SubscriptionLocator.promises.getUsersSubscription = sinon
+ .stub()
+ .resolves({ groupPlan: true, planCode: 'group_collaborator' })
+ const result = await this.Handler.promises.getGroupPlanUpgradePreview(
+ this.user_id
+ )
+ this.RecurlyClient.promises.previewSubscriptionChange
+ .calledWith(this.changeRequest)
+ .should.equal(true)
+
+ result.should.equal(this.changePreview)
+ })
+ })
})