diff --git a/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details.tsx b/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details.tsx index 2bcf4901fd..6c87023e97 100644 --- a/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details.tsx +++ b/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details.tsx @@ -28,7 +28,7 @@ function UpgradeSubscriptionPlanDetails() { {preview.nextInvoice.plan.name} - + {formatCurrencyLocalized( licenseUnitPrice, diff --git a/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-upgrade-summary.tsx b/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-upgrade-summary.tsx index 0993a60c38..f20022b0df 100644 --- a/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-upgrade-summary.tsx +++ b/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-upgrade-summary.tsx @@ -38,7 +38,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) { {subscriptionChange.nextInvoice.plan.name} x {totalLicenses}{' '} {t('users')} - + {formatCurrencyLocalized( subscriptionChange.immediateCharge.subtotal, subscriptionChange.currency @@ -47,7 +47,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) { {t('sales_tax')} - + {formatCurrencyLocalized( subscriptionChange.immediateCharge.tax, subscriptionChange.currency @@ -56,7 +56,7 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) { {t('total_due_today')} - + {formatCurrencyLocalized( subscriptionChange.immediateCharge.total, subscriptionChange.currency diff --git a/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription.tsx b/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription.tsx index 191f3558dc..28431fd95e 100644 --- a/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription.tsx +++ b/services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription.tsx @@ -10,6 +10,7 @@ import RequestStatus from '../request-status' import UpgradeSummary, { SubscriptionChange, } from './upgrade-subscription-upgrade-summary' +import { debugConsole } from '@/utils/debugging' function UpgradeSubscription() { const { t } = useTranslation() @@ -17,7 +18,9 @@ function UpgradeSubscription() { const preview = getMeta('ol-subscriptionChangePreview') as SubscriptionChange const { isError, runAsync, isSuccess, isLoading } = useAsync() const onSubmit = () => { - runAsync(postJSON('/user/subscription/group/upgrade-subscription')) + runAsync(postJSON('/user/subscription/group/upgrade-subscription')).catch( + debugConsole.error + ) } if (isSuccess) { @@ -51,7 +54,7 @@ function UpgradeSubscription() {
-
+
', 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) + }) + }) })