From e6ede390ff7a51ffd360a2a6b0f421168cca744c Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Thu, 10 Jul 2025 13:25:57 +0500 Subject: [PATCH 1/9] feat: use discount info endpoint for streak discount information --- .env | 1 + .env.development | 1 + .env.test | 1 + src/index.jsx | 1 + .../StreakCelebrationModal.jsx | 21 +++++++++---------- .../StreakCelebrationModal.test.jsx | 6 +++--- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.env b/.env index 7832763df9..b0129d312a 100644 --- a/.env +++ b/.env @@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='' CSRF_TOKEN_API_PATH='' DISCOVERY_API_BASE_URL='' DISCUSSIONS_MFE_BASE_URL='' +DISCOUNT_CODE_INFO_URL='' ECOMMERCE_BASE_URL='' ENABLE_JUMPNAV='true' ENABLE_NOTICES='' diff --git a/.env.development b/.env.development index 5dd532769a..2dab9273a5 100644 --- a/.env.development +++ b/.env.development @@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co CSRF_TOKEN_API_PATH='/csrf/api/v1/token' DISCOVERY_API_BASE_URL='http://localhost:18381' DISCUSSIONS_MFE_BASE_URL='http://localhost:2002' +DISCOUNT_CODE_INFO_URL='http://localhost:8140/lms/discount-code-info/' ECOMMERCE_BASE_URL='http://localhost:18130' ENABLE_JUMPNAV='true' ENABLE_NOTICES='' diff --git a/.env.test b/.env.test index b17621b0e3..fb754e84b1 100644 --- a/.env.test +++ b/.env.test @@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co CSRF_TOKEN_API_PATH='/csrf/api/v1/token' DISCOVERY_API_BASE_URL='http://localhost:18381' DISCUSSIONS_MFE_BASE_URL='http://localhost:2002' +DISCOUNT_CODE_INFO_URL='http://localhost:8140/lms/discount-code-info/' ECOMMERCE_BASE_URL='http://localhost:18130' ENABLE_JUMPNAV='true' ENABLE_NOTICES='' diff --git a/src/index.jsx b/src/index.jsx index b3748ca688..1b5cacc2af 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -171,6 +171,7 @@ initialize({ CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null, CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null, DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null, + DISCOUNT_CODE_INFO_URL: process.env.DISCOUNT_CODE_INFO_URL || null, ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null, ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null, ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null, diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index f3b33b5feb..cac13133f2 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -42,9 +42,8 @@ function getRandomFactoid(intl, streakLength) { return factoids[Math.floor(Math.random() * (factoids.length))]; } -async function calculateVoucherDiscount(voucher, sku, username) { - const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`; - const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`; +async function getDiscountCodeInfo(code) { + const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?code=${code}`; return getAuthenticatedHttpClient().get(url) .then(res => camelCaseObject(res)); } @@ -83,17 +82,17 @@ const StreakModal = ({ // Ask ecommerce to calculate discount savings useEffect(() => { - if (streakDiscountCouponEnabled && verifiedMode && getConfig().ECOMMERCE_BASE_URL) { - calculateVoucherDiscount(discountCode, verifiedMode.sku, username) + if (streakDiscountCouponEnabled && verifiedMode && getConfig().DISCOUNT_CODE_INFO_URL) { + getDiscountCodeInfo(discountCode) .then( (result) => { - const { totalInclTax, totalInclTaxExclDiscounts } = result.data; - if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { - // Just store the percent (rather than using these values directly), because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume + const { isApplicable, discountPercentage } = result.data; + if (isApplicable) { + // Just store the percent, because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And we don't want to assume // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the calculated percentage. - setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts); + // multiplied by the received percentage. + setDiscountPercent(discountPercentage); sendTrackEvent('edx.bi.course.streak_discount_enabled', { course_id: courseId, sku: verifiedMode.sku, diff --git a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx index 345f36a0ee..2ef4aa247e 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx @@ -22,15 +22,15 @@ describe('Loaded Tab Page', () => { let mockData; let testStore; let axiosMock; - const calculateUrl = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate/?code=ZGY11119949&sku=8CF08E5&username=MockUser`; + const calculateUrl = `${getConfig().DISCOUNT_CODE_INFO_URL}?code=ZGY11119949`; const courseMetadata = Factory.build('courseMetadata'); const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { streak_length_to_celebrate: 3 } }); function setDiscount(percent) { mockData.streakDiscountCouponEnabled = true; axiosMock.onGet(calculateUrl).reply(200, { - total_incl_tax: 100 - percent, - total_incl_tax_excl_discounts: 100, + isApplicable: true, + discountPercentage: percent / 100, }); } From 9fde9b1797125492c87b462d4cb6f0bdba129d28 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Tue, 15 Jul 2025 12:05:49 +0500 Subject: [PATCH 2/9] feat: pass course run key to discount code info call --- .../streak-celebration/StreakCelebrationModal.jsx | 10 +++++++--- .../streak-celebration/StreakCelebrationModal.test.jsx | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index cac13133f2..3fd6e46558 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -42,8 +42,12 @@ function getRandomFactoid(intl, streakLength) { return factoids[Math.floor(Math.random() * (factoids.length))]; } -async function getDiscountCodeInfo(code) { - const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?code=${code}`; +async function getDiscountCodeInfo(code, courseId) { + const params = new URLSearchParams(); + params.append('code', code); + params.append('course_run_key', courseId); + + const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; return getAuthenticatedHttpClient().get(url) .then(res => camelCaseObject(res)); } @@ -83,7 +87,7 @@ const StreakModal = ({ // Ask ecommerce to calculate discount savings useEffect(() => { if (streakDiscountCouponEnabled && verifiedMode && getConfig().DISCOUNT_CODE_INFO_URL) { - getDiscountCodeInfo(discountCode) + getDiscountCodeInfo(discountCode, courseId) .then( (result) => { const { isApplicable, discountPercentage } = result.data; diff --git a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx index 2ef4aa247e..e4292acbf2 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx @@ -22,9 +22,12 @@ describe('Loaded Tab Page', () => { let mockData; let testStore; let axiosMock; - const calculateUrl = `${getConfig().DISCOUNT_CODE_INFO_URL}?code=ZGY11119949`; const courseMetadata = Factory.build('courseMetadata'); const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { streak_length_to_celebrate: 3 } }); + const params = new URLSearchParams(); + params.append('code', 'ZGY11119949'); + params.append('course_run_key', courseMetadata.id); + const calculateUrl = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; function setDiscount(percent) { mockData.streakDiscountCouponEnabled = true; From b77cb609a3213243d021e3aebcad9c61cc72e5f9 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Wed, 16 Jul 2025 21:00:13 +0500 Subject: [PATCH 3/9] feat: move changes behind a flag --- .../StreakCelebrationModal.jsx | 85 +++++++++++++------ 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index 3fd6e46558..9d8bd31781 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -42,12 +42,19 @@ function getRandomFactoid(intl, streakLength) { return factoids[Math.floor(Math.random() * (factoids.length))]; } +async function calculateVoucherDiscount(voucher, sku, username) { + const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`; + const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`; + return getAuthenticatedHttpClient().get(url) + .then(res => camelCaseObject(res)); +} + async function getDiscountCodeInfo(code, courseId) { const params = new URLSearchParams(); params.append('code', code); params.append('course_run_key', courseId); - const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; + return getAuthenticatedHttpClient().get(url) .then(res => camelCaseObject(res)); } @@ -86,34 +93,62 @@ const StreakModal = ({ // Ask ecommerce to calculate discount savings useEffect(() => { - if (streakDiscountCouponEnabled && verifiedMode && getConfig().DISCOUNT_CODE_INFO_URL) { - getDiscountCodeInfo(discountCode, courseId) - .then( - (result) => { - const { isApplicable, discountPercentage } = result.data; - if (isApplicable) { - // Just store the percent, because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And we don't want to assume - // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the received percentage. - setDiscountPercent(discountPercentage); - sendTrackEvent('edx.bi.course.streak_discount_enabled', { - course_id: courseId, - sku: verifiedMode.sku, - }); - } else { + if (streakDiscountCouponEnabled && verifiedMode) { + if (getConfig().DISCOUNT_CODE_INFO_URL) { + getDiscountCodeInfo(discountCode, courseId) + .then( + (result) => { + const { isApplicable, discountPercentage } = result.data; + if (isApplicable) { + // Just store the percent, because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And we don't want to assume + // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just + // multiplied by the received percentage. + setDiscountPercent(discountPercentage); + sendTrackEvent('edx.bi.course.streak_discount_enabled', { + course_id: courseId, + sku: verifiedMode.sku, + }); + } else { + setDiscountPercent(0); + } + }, + () => { + // ignore any errors - we just won't show the discount to the user then + setDiscountPercent(0); + }, + ); + } else if (getConfig().ECOMMERCE_BASE_URL) { + calculateVoucherDiscount(discountCode, verifiedMode.sku, username) + .then( + (result) => { + const { totalInclTax, totalInclTaxExclDiscounts } = result.data; + if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { + // Just store the percent (rather than using these values directly), because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume + // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just + // multiplied by the calculated percentage. + setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts); + sendTrackEvent('edx.bi.course.streak_discount_enabled', { + course_id: courseId, + sku: verifiedMode.sku, + }); + } else { + setDiscountPercent(0); + } + }, + () => { + // ignore any errors - we just won't show the discount to the user then setDiscountPercent(0); - } - }, - () => { - // ignore any errors - we just won't show the discount to the user then - setDiscountPercent(0); - }, - ); + }, + ); + } else { + setDiscountPercent(0); + } } else { setDiscountPercent(0); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [streakDiscountCouponEnabled, username, verifiedMode]); if (!isStreakCelebrationOpen) { From e835842ce7b09ed1c82c291485900624e16b8a7f Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Wed, 16 Jul 2025 21:26:29 +0500 Subject: [PATCH 4/9] fix: use async IIFE inside useEffect --- .../StreakCelebrationModal.jsx | 93 ++++++++----------- 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index 9d8bd31781..cf7211c674 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -93,61 +93,46 @@ const StreakModal = ({ // Ask ecommerce to calculate discount savings useEffect(() => { - if (streakDiscountCouponEnabled && verifiedMode) { - if (getConfig().DISCOUNT_CODE_INFO_URL) { - getDiscountCodeInfo(discountCode, courseId) - .then( - (result) => { - const { isApplicable, discountPercentage } = result.data; - if (isApplicable) { - // Just store the percent, because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And we don't want to assume - // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the received percentage. - setDiscountPercent(discountPercentage); - sendTrackEvent('edx.bi.course.streak_discount_enabled', { - course_id: courseId, - sku: verifiedMode.sku, - }); - } else { - setDiscountPercent(0); - } - }, - () => { - // ignore any errors - we just won't show the discount to the user then - setDiscountPercent(0); - }, - ); - } else if (getConfig().ECOMMERCE_BASE_URL) { - calculateVoucherDiscount(discountCode, verifiedMode.sku, username) - .then( - (result) => { - const { totalInclTax, totalInclTaxExclDiscounts } = result.data; - if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { - // Just store the percent (rather than using these values directly), because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume - // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the calculated percentage. - setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts); - sendTrackEvent('edx.bi.course.streak_discount_enabled', { - course_id: courseId, - sku: verifiedMode.sku, - }); - } else { - setDiscountPercent(0); - } - }, - () => { - // ignore any errors - we just won't show the discount to the user then - setDiscountPercent(0); - }, - ); - } else { - setDiscountPercent(0); + (async () => { + let newDiscountPercentage = 0; + try { + if (streakDiscountCouponEnabled && verifiedMode) { + if (getConfig().DISCOUNT_CODE_INFO_URL) { + const result = await getDiscountCodeInfo(discountCode, courseId); + const { isApplicable, discountPercentage } = result.data; + if (isApplicable) { + // Just store the percent, because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And we don't want to assume + // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just + // multiplied by the received percentage. + newDiscountPercentage = discountPercentage; + sendTrackEvent('edx.bi.course.streak_discount_enabled', { + course_id: courseId, + sku: verifiedMode.sku, + }); + } + } else if (getConfig().ECOMMERCE_BASE_URL) { + const result = await calculateVoucherDiscount(discountCode, verifiedMode.sku, username); + const { totalInclTax, totalInclTaxExclDiscounts } = result.data; + if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { + // Just store the percent (rather than using these values directly), because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume + // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just + // multiplied by the calculated percentage. + newDiscountPercentage = (1 - totalInclTax / totalInclTaxExclDiscounts); + sendTrackEvent('edx.bi.course.streak_discount_enabled', { + course_id: courseId, + sku: verifiedMode.sku, + }); + } + } + } + } catch { + // ignore any errors - we just won't show the discount to the user then + } finally { + setDiscountPercent(newDiscountPercentage); } - } else { - setDiscountPercent(0); - } + })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [streakDiscountCouponEnabled, username, verifiedMode]); From ffe119f7392fc80156ae9738d5ca647f904c4404 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Wed, 16 Jul 2025 21:31:12 +0500 Subject: [PATCH 5/9] fix: fix line length --- .../streak-celebration/StreakCelebrationModal.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index cf7211c674..4845aaaec0 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -101,10 +101,11 @@ const StreakModal = ({ const result = await getDiscountCodeInfo(discountCode, courseId); const { isApplicable, discountPercentage } = result.data; if (isApplicable) { - // Just store the percent, because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And we don't want to assume - // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the received percentage. + // Just store the percent, because ecommerce doesn't give us the + // currency symbol to use, so we want to use the symbol that LMS + // gives us. And we don't want to assume ecommerce's currency is + // the same as the LMS. So we'll keep using the values in + // verifiedMode, just multiplied by the received percentage. newDiscountPercentage = discountPercentage; sendTrackEvent('edx.bi.course.streak_discount_enabled', { course_id: courseId, @@ -119,7 +120,7 @@ const StreakModal = ({ // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just // multiplied by the calculated percentage. - newDiscountPercentage = (1 - totalInclTax / totalInclTaxExclDiscounts); + newDiscountPercentage = 1 - totalInclTax / totalInclTaxExclDiscounts; sendTrackEvent('edx.bi.course.streak_discount_enabled', { course_id: courseId, sku: verifiedMode.sku, From 4f60da9063dec8591f52f60538fc00cf15221d89 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Thu, 17 Jul 2025 19:47:52 +0500 Subject: [PATCH 6/9] fix: remove default value in dev --- .env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.development b/.env.development index 2dab9273a5..7f9cdfc709 100644 --- a/.env.development +++ b/.env.development @@ -12,7 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co CSRF_TOKEN_API_PATH='/csrf/api/v1/token' DISCOVERY_API_BASE_URL='http://localhost:18381' DISCUSSIONS_MFE_BASE_URL='http://localhost:2002' -DISCOUNT_CODE_INFO_URL='http://localhost:8140/lms/discount-code-info/' +DISCOUNT_CODE_INFO_URL='' ECOMMERCE_BASE_URL='http://localhost:18130' ENABLE_JUMPNAV='true' ENABLE_NOTICES='' From c222bd08a586f02f3620a1c0fd9648a472f81c10 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Mon, 21 Jul 2025 17:27:54 +0500 Subject: [PATCH 7/9] fix: improve coverage by adding conditional test based on env value --- .env.test | 2 +- .../StreakCelebrationModal.test.jsx | 38 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.env.test b/.env.test index fb754e84b1..42e4756f9e 100644 --- a/.env.test +++ b/.env.test @@ -12,7 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co CSRF_TOKEN_API_PATH='/csrf/api/v1/token' DISCOVERY_API_BASE_URL='http://localhost:18381' DISCUSSIONS_MFE_BASE_URL='http://localhost:2002' -DISCOUNT_CODE_INFO_URL='http://localhost:8140/lms/discount-code-info/' +DISCOUNT_CODE_INFO_URL='' ECOMMERCE_BASE_URL='http://localhost:18130' ENABLE_JUMPNAV='true' ENABLE_NOTICES='' diff --git a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx index e4292acbf2..c71b9946a4 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Factory } from 'rosie'; -import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { camelCaseObject, getConfig, mergeConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { breakpoints } from '@openedx/paragon'; @@ -22,16 +22,26 @@ describe('Loaded Tab Page', () => { let mockData; let testStore; let axiosMock; + const calculateUrl = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate/?code=ZGY11119949&sku=8CF08E5&username=MockUser`; const courseMetadata = Factory.build('courseMetadata'); const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { streak_length_to_celebrate: 3 } }); - const params = new URLSearchParams(); - params.append('code', 'ZGY11119949'); - params.append('course_run_key', courseMetadata.id); - const calculateUrl = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; function setDiscount(percent) { mockData.streakDiscountCouponEnabled = true; axiosMock.onGet(calculateUrl).reply(200, { + total_incl_tax: 100 - percent, + total_incl_tax_excl_discounts: 100, + }); + } + + function setDiscountViaDiscountCodeInfo(percent) { + const discountURLParams = new URLSearchParams(); + discountURLParams.append('code', 'ZGY11119949'); + discountURLParams.append('course_run_key', courseMetadata.id); + const discountURL = `${getConfig().DISCOUNT_CODE_INFO_URL}?${discountURLParams.toString()}`; + + mockData.streakDiscountCouponEnabled = true; + axiosMock.onGet(discountURL).reply(200, { isApplicable: true, discountPercentage: percent / 100, }); @@ -108,4 +118,22 @@ describe('Loaded Tab Page', () => { sku: mockData.verifiedMode.sku, }); }); + + it('shows discount version of streak celebration modal when discount available and info fetched using DISCOUNT_CODE_INFO_URL', async () => { + mergeConfig({ DISCOUNT_CODE_INFO_URL: 'http://localhost:8140/lms/discount-code-info/' }); + + global.innerWidth = breakpoints.extraSmall.maxWidth; + setDiscountViaDiscountCodeInfo(14); + await renderModal(); + + const endDateText = `Ends ${new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toLocaleDateString({ timeZone: 'UTC' })}.`; + expect(screen.getByText('You’ve unlocked a 14% off discount when you upgrade this course for a limited time only.', { exact: false })).toBeInTheDocument(); + expect(screen.getByText(endDateText, { exact: false })).toBeInTheDocument(); + expect(screen.getByText('Continue with course')).toBeInTheDocument(); + expect(screen.queryByText('Keep it up')).not.toBeInTheDocument(); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.course.streak_discount_enabled', { + course_id: mockData.courseId, + sku: mockData.verifiedMode.sku, + }); + }); }); From b14617ed8ee947b192ad3c1e2ad26def8c14fef9 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Mon, 28 Jul 2025 16:49:17 +0500 Subject: [PATCH 8/9] refactor: move logic inside function --- .../StreakCelebrationModal.jsx | 73 ++++++++++--------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index 4845aaaec0..4b71456ec9 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -42,21 +42,34 @@ function getRandomFactoid(intl, streakLength) { return factoids[Math.floor(Math.random() * (factoids.length))]; } -async function calculateVoucherDiscount(voucher, sku, username) { +async function calculateVoucherDiscountPercentage(voucher, sku, username) { const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`; const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`; - return getAuthenticatedHttpClient().get(url) - .then(res => camelCaseObject(res)); + + const result = await getAuthenticatedHttpClient().get(url); + const { totalInclTax, totalInclTaxExclDiscounts } = camelCaseObject(result).data; + + if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { + // Just store the percent (rather than using these values directly), because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume + // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just + // multiplied by the calculated percentage. + return 1 - totalInclTax / totalInclTaxExclDiscounts; + } + + return 0; } -async function getDiscountCodeInfo(code, courseId) { +async function getDiscountCodePercentage(code, courseId) { const params = new URLSearchParams(); params.append('code', code); params.append('course_run_key', courseId); const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; - return getAuthenticatedHttpClient().get(url) - .then(res => camelCaseObject(res)); + const result = await getAuthenticatedHttpClient().get(url); + const { isApplicable, discountPercentage } = camelCaseObject(result).data; + + return isApplicable ? +discountPercentage : 0; } const CloseText = ({ intl }) => ( @@ -94,44 +107,34 @@ const StreakModal = ({ // Ask ecommerce to calculate discount savings useEffect(() => { (async () => { - let newDiscountPercentage = 0; + let streakDiscountPercentage = 0; try { if (streakDiscountCouponEnabled && verifiedMode) { + // If the discount service is available, use it to get the discount percentage if (getConfig().DISCOUNT_CODE_INFO_URL) { - const result = await getDiscountCodeInfo(discountCode, courseId); - const { isApplicable, discountPercentage } = result.data; - if (isApplicable) { - // Just store the percent, because ecommerce doesn't give us the - // currency symbol to use, so we want to use the symbol that LMS - // gives us. And we don't want to assume ecommerce's currency is - // the same as the LMS. So we'll keep using the values in - // verifiedMode, just multiplied by the received percentage. - newDiscountPercentage = discountPercentage; - sendTrackEvent('edx.bi.course.streak_discount_enabled', { - course_id: courseId, - sku: verifiedMode.sku, - }); - } + streakDiscountPercentage = await getDiscountCodePercentage( + discountCode, + courseId, + ); + // If the discount service is not available, fall back to ecommerce to calculate the discount percentage } else if (getConfig().ECOMMERCE_BASE_URL) { - const result = await calculateVoucherDiscount(discountCode, verifiedMode.sku, username); - const { totalInclTax, totalInclTaxExclDiscounts } = result.data; - if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { - // Just store the percent (rather than using these values directly), because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume - // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the calculated percentage. - newDiscountPercentage = 1 - totalInclTax / totalInclTaxExclDiscounts; - sendTrackEvent('edx.bi.course.streak_discount_enabled', { - course_id: courseId, - sku: verifiedMode.sku, - }); - } + streakDiscountPercentage = await calculateVoucherDiscountPercentage( + discountCode, + verifiedMode.sku, + username, + ); } } } catch { // ignore any errors - we just won't show the discount to the user then } finally { - setDiscountPercent(newDiscountPercentage); + if (streakDiscountPercentage) { + sendTrackEvent('edx.bi.course.streak_discount_enabled', { + course_id: courseId, + sku: verifiedMode.sku, + }); + } + setDiscountPercent(streakDiscountPercentage); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps From 5d6fdf09e6642afde5dc98267a85ed1683a81de3 Mon Sep 17 00:00:00 2001 From: Nawfal Ahmed Date: Mon, 28 Jul 2025 17:29:41 +0500 Subject: [PATCH 9/9] refactor: move functions to utils --- .../StreakCelebrationModal.jsx | 40 +++-------------- src/shared/streak-celebration/utils.jsx | 43 ++++++++++++++++++- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/shared/streak-celebration/StreakCelebrationModal.jsx b/src/shared/streak-celebration/StreakCelebrationModal.jsx index 4b71456ec9..b5764cf7f4 100644 --- a/src/shared/streak-celebration/StreakCelebrationModal.jsx +++ b/src/shared/streak-celebration/StreakCelebrationModal.jsx @@ -1,9 +1,8 @@ /* eslint-disable react/prop-types */ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons'; import { @@ -16,7 +15,12 @@ import { useModel } from '../../generic/model-store'; import StreakMobileImage from './assets/Streak_mobile.png'; import StreakDesktopImage from './assets/Streak_desktop.png'; import messages from './messages'; -import { recordModalClosing, recordStreakCelebration } from './utils'; +import { + calculateVoucherDiscountPercentage, + getDiscountCodePercentage, + recordModalClosing, + recordStreakCelebration, +} from './utils'; function getRandomFactoid(intl, streakLength) { const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection); @@ -42,36 +46,6 @@ function getRandomFactoid(intl, streakLength) { return factoids[Math.floor(Math.random() * (factoids.length))]; } -async function calculateVoucherDiscountPercentage(voucher, sku, username) { - const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`; - const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`; - - const result = await getAuthenticatedHttpClient().get(url); - const { totalInclTax, totalInclTaxExclDiscounts } = camelCaseObject(result).data; - - if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { - // Just store the percent (rather than using these values directly), because ecommerce doesn't give us - // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume - // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just - // multiplied by the calculated percentage. - return 1 - totalInclTax / totalInclTaxExclDiscounts; - } - - return 0; -} - -async function getDiscountCodePercentage(code, courseId) { - const params = new URLSearchParams(); - params.append('code', code); - params.append('course_run_key', courseId); - const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; - - const result = await getAuthenticatedHttpClient().get(url); - const { isApplicable, discountPercentage } = camelCaseObject(result).data; - - return isApplicable ? +discountPercentage : 0; -} - const CloseText = ({ intl }) => ( {intl.formatMessage(messages.streakButton)} diff --git a/src/shared/streak-celebration/utils.jsx b/src/shared/streak-celebration/utils.jsx index a96b4fbfd4..d0610b2bb9 100644 --- a/src/shared/streak-celebration/utils.jsx +++ b/src/shared/streak-celebration/utils.jsx @@ -1,5 +1,9 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { + getAuthenticatedHttpClient, + getAuthenticatedUser, +} from '@edx/frontend-platform/auth'; import { updateModel } from '../../generic/model-store'; @@ -24,4 +28,39 @@ function recordModalClosing(celebrations, org, courseId, dispatch) { })); } -export { recordStreakCelebration, recordModalClosing }; +async function calculateVoucherDiscountPercentage(voucher, sku, username) { + const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`; + const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`; + + const result = await getAuthenticatedHttpClient().get(url); + const { totalInclTax, totalInclTaxExclDiscounts } = camelCaseObject(result).data; + + if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) { + // Just store the percent (rather than using these values directly), because ecommerce doesn't give us + // the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume + // ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just + // multiplied by the calculated percentage. + return 1 - totalInclTax / totalInclTaxExclDiscounts; + } + + return 0; +} + +async function getDiscountCodePercentage(code, courseId) { + const params = new URLSearchParams(); + params.append('code', code); + params.append('course_run_key', courseId); + const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`; + + const result = await getAuthenticatedHttpClient().get(url); + const { isApplicable, discountPercentage } = camelCaseObject(result).data; + + return isApplicable ? +discountPercentage : 0; +} + +export { + calculateVoucherDiscountPercentage, + getDiscountCodePercentage, + recordModalClosing, + recordStreakCelebration, +};