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..7f9cdfc709 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='' ECOMMERCE_BASE_URL='http://localhost:18130' ENABLE_JUMPNAV='true' ENABLE_NOTICES='' diff --git a/.env.test b/.env.test index b17621b0e3..42e4756f9e 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='' 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..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,13 +46,6 @@ 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)); -} - const CloseText = ({ intl }) => ( {intl.formatMessage(messages.streakButton)} @@ -83,34 +80,38 @@ const StreakModal = ({ // Ask ecommerce to calculate discount savings useEffect(() => { - if (streakDiscountCouponEnabled && verifiedMode && 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); - } - // eslint-disable-next-line react-hooks/exhaustive-deps + (async () => { + 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) { + 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) { + streakDiscountPercentage = await calculateVoucherDiscountPercentage( + discountCode, + verifiedMode.sku, + username, + ); + } + } + } catch { + // ignore any errors - we just won't show the discount to the user then + } finally { + 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 }, [streakDiscountCouponEnabled, username, verifiedMode]); if (!isStreakCelebrationOpen) { diff --git a/src/shared/streak-celebration/StreakCelebrationModal.test.jsx b/src/shared/streak-celebration/StreakCelebrationModal.test.jsx index 345f36a0ee..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'; @@ -34,6 +34,19 @@ describe('Loaded Tab Page', () => { }); } + 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, + }); + } + function setDiscountError() { mockData.streakDiscountCouponEnabled = true; axiosMock.onGet(calculateUrl).reply(500); @@ -105,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, + }); + }); }); 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, +};