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