diff --git a/services/web/app/src/Features/Subscription/RecurlyClient.js b/services/web/app/src/Features/Subscription/RecurlyClient.js index f66343d9f0..66d9492751 100644 --- a/services/web/app/src/Features/Subscription/RecurlyClient.js +++ b/services/web/app/src/Features/Subscription/RecurlyClient.js @@ -366,6 +366,8 @@ function computeImmediateCharge(subscriptionChange) { subscriptionChange.invoiceCollection?.chargeInvoice?.subtotal ?? 0 let tax = subscriptionChange.invoiceCollection?.chargeInvoice?.tax ?? 0 let total = subscriptionChange.invoiceCollection?.chargeInvoice?.total ?? 0 + let discount = + subscriptionChange.invoiceCollection?.chargeInvoice?.discount ?? 0 for (const creditInvoice of subscriptionChange.invoiceCollection ?.creditInvoices ?? []) { // The credit invoice numbers are already negative @@ -373,12 +375,13 @@ function computeImmediateCharge(subscriptionChange) { total = roundToTwoDecimal(total + (creditInvoice.total ?? 0)) // Tax rate can be different in credit invoice if a user relocates tax = roundToTwoDecimal(tax + (creditInvoice.tax ?? 0)) + discount = roundToTwoDecimal(discount + (creditInvoice.discount ?? 0)) } - return new RecurlyImmediateCharge({ subtotal, total, tax, + discount, }) } diff --git a/services/web/app/src/Features/Subscription/RecurlyEntities.js b/services/web/app/src/Features/Subscription/RecurlyEntities.js index bdd2446c30..7d66c4403a 100644 --- a/services/web/app/src/Features/Subscription/RecurlyEntities.js +++ b/services/web/app/src/Features/Subscription/RecurlyEntities.js @@ -341,7 +341,7 @@ class RecurlySubscriptionChange { this.nextAddOns = props.nextAddOns this.immediateCharge = props.immediateCharge ?? - new RecurlyImmediateCharge({ subtotal: 0, tax: 0, total: 0 }) + new RecurlyImmediateCharge({ subtotal: 0, tax: 0, total: 0, discount: 0 }) this.subtotal = this.nextPlanPrice for (const addOn of this.nextAddOns) { @@ -386,11 +386,13 @@ class RecurlyImmediateCharge { * @param {number} props.subtotal * @param {number} props.tax * @param {number} props.total + * @param {number} props.discount */ constructor(props) { this.subtotal = props.subtotal this.tax = props.tax this.total = props.total + this.discount = props.discount } } diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f53002342d..487c6aed81 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -387,6 +387,7 @@ "disable_stop_on_first_error": "", "disabling": "", "disconnected": "", + "discount": "", "discount_of": "", "dismiss_error_popup": "", "display_deleted_user": "", diff --git a/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx b/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx index 54ca9d1704..c6cae7f064 100644 --- a/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx +++ b/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx @@ -74,6 +74,19 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) { )} + {subscriptionChange.immediateCharge.discount !== 0 && ( + + {t('discount')} + + ( + {formatCurrency( + subscriptionChange.immediateCharge.discount, + subscriptionChange.currency + )} + ) + + + )} )} 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 d85560e8ba..921a4844a7 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 @@ -45,6 +45,19 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) { )} + {subscriptionChange.immediateCharge.discount !== 0 && ( + + {t('discount')} + + ( + {formatCurrency( + subscriptionChange.immediateCharge.discount, + subscriptionChange.currency + )} + ) + + + )} {t('sales_tax')} @@ -90,6 +103,8 @@ function UpgradeSummary({ subscriptionChange }: UpgradeSummaryProps) { date: formatTime(subscriptionChange.nextInvoice.date, 'MMMM D'), } )} + {subscriptionChange.immediateCharge.discount !== 0 && + ` ${t('coupons_not_included')}.`} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index d1443d5b6b..a87a2c3472 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -508,6 +508,7 @@ "disable_stop_on_first_error": "Disable “Stop on first error”", "disabling": "Disabling", "disconnected": "Disconnected", + "discount": "Discount", "discount_of": "Discount of __amount__", "discover_latex_templates_and_examples": "Discover LaTeX templates and examples to help with everything from writing a journal article to using a specific LaTeX package.", "discover_why_people_worldwide_trust_overleaf": "Discover why __count__ million people worldwide trust Overleaf with their work.", diff --git a/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx b/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx index a885eb284f..f266789e13 100644 --- a/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx @@ -2,7 +2,6 @@ import '../../../helpers/bootstrap-5' import AddSeats, { MAX_NUMBER_OF_USERS, } from '@/features/group-management/components/add-seats/add-seats' -import { SplitTestProvider } from '@/shared/context/split-test-context' describe('', function () { beforeEach(function () { @@ -15,11 +14,7 @@ describe('', function () { win.metaAttributesCache.set('ol-isProfessional', false) }) - cy.mount( - - - - ) + cy.mount() cy.findByRole('button', { name: /add users/i }) cy.findByTestId('add-more-users-group-form') @@ -88,11 +83,7 @@ describe('', function () { win.metaAttributesCache.set('ol-isProfessional', true) }) - cy.mount( - - - - ) + cy.mount() cy.findByRole('link', { name: /upgrade my plan/i }).should('not.exist') }) @@ -216,12 +207,6 @@ describe('', function () { describe('entered less than the maximum allowed number of users', function () { beforeEach(function () { this.adding = 1 - - cy.findByRole('button', { name: /add users/i }).as('addUsersBtn') - cy.findByRole('button', { name: /send request/i }).should('not.exist') - }) - - it('renders the preview data', function () { this.body = { change: { type: 'add-on-update', @@ -236,6 +221,7 @@ describe('', function () { subtotal: 100, tax: 20, total: 120, + discount: 0, }, nextInvoice: { date: '2025-12-01T00:00:00.000Z', @@ -252,6 +238,11 @@ describe('', function () { }, } + cy.findByRole('button', { name: /add users/i }).as('addUsersBtn') + cy.findByRole('button', { name: /send request/i }).should('not.exist') + }) + + it('renders the preview data', function () { cy.intercept('POST', '/user/subscription/group/add-users/preview', { statusCode: 200, body: this.body, @@ -289,6 +280,8 @@ describe('', function () { ) }) + cy.findByTestId('discount').should('not.exist') + cy.findByTestId('total').within(() => { cy.findByText(/total due today/i) cy.findByTestId('price').should( @@ -306,6 +299,26 @@ describe('', function () { }) }) + it('renders the preview data with discount', function () { + this.body.immediateCharge.discount = 50 + + cy.intercept('POST', '/user/subscription/group/add-users/preview', { + statusCode: 200, + body: this.body, + }).as('addUsersRequest') + cy.get('@input').type(this.adding.toString()) + + cy.findByTestId('cost-summary').within(() => { + cy.findByTestId('discount').within(() => { + cy.findByText(`($${this.body.immediateCharge.discount}.00)`) + }) + + cy.findByText( + /This does not include your current discounts, which will be applied automatically before your next payment/i + ) + }) + }) + describe('request', function () { afterEach(function () { cy.findByRole('button', { name: /go to subscriptions/i }).should( diff --git a/services/web/test/frontend/features/group-management/components/upgrade-subscription.spec.tsx b/services/web/test/frontend/features/group-management/components/upgrade-subscription.spec.tsx index 3c0bcffd88..1416368b22 100644 --- a/services/web/test/frontend/features/group-management/components/upgrade-subscription.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/upgrade-subscription.spec.tsx @@ -1,8 +1,15 @@ import '../../../helpers/bootstrap-5' import UpgradeSubscription from '@/features/group-management/components/upgrade-subscription/upgrade-subscription' -import { SplitTestProvider } from '@/shared/context/split-test-context' +import { SubscriptionChangePreview } from '../../../../../types/subscription/subscription-change-preview' describe('', function () { + const resetPreviewAndRemount = (preview: SubscriptionChangePreview) => { + cy.window().then(win => { + win.metaAttributesCache.set('ol-subscriptionChangePreview', preview) + }) + + cy.mount() + } beforeEach(function () { this.totalLicenses = 2 this.preview = { @@ -11,7 +18,12 @@ describe('', function () { prevPlan: { name: 'Overleaf Standard Group' }, }, currency: 'USD', - immediateCharge: { subtotal: 353.99, tax: 70.8, total: 424.79 }, + immediateCharge: { + subtotal: 353.99, + tax: 70.8, + total: 424.79, + discount: 0, + }, paymentMethod: 'Visa **** 1111', nextPlan: { annual: true }, nextInvoice: { @@ -35,14 +47,8 @@ describe('', function () { 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( - - - - ) + resetPreviewAndRemount(this.preview) }) it('shows the group name', function () { @@ -93,6 +99,31 @@ describe('', function () { cy.findByTestId('total').within(() => { cy.findByText('$424.79') }) + cy.findByTestId('discount').should('not.exist') + }) + + it('shows subtotal, discount, tax and total price', function () { + resetPreviewAndRemount({ + ...this.preview, + immediateCharge: { + subtotal: 353.99, + tax: 70.8, + total: 424.79, + discount: 50, + }, + }) + cy.findByTestId('subtotal').within(() => { + cy.findByText('$353.99') + }) + cy.findByTestId('tax').within(() => { + cy.findByText('$70.80') + }) + cy.findByTestId('total').within(() => { + cy.findByText('$424.79') + }) + cy.findByTestId('discount').within(() => { + cy.findByText('($50.00)') + }) }) it('shows total users', function () { diff --git a/services/web/types/subscription/subscription-change-preview.ts b/services/web/types/subscription/subscription-change-preview.ts index 669df0d19c..6476ebd7de 100644 --- a/services/web/types/subscription/subscription-change-preview.ts +++ b/services/web/types/subscription/subscription-change-preview.ts @@ -9,6 +9,7 @@ export type SubscriptionChangePreview = { subtotal: number tax: number total: number + discount: number } nextInvoice: { date: string