feat: use discount info endpoint for streak discount information (#1763)
* feat: use discount info endpoint for streak discount information * feat: pass course run key to discount code info call * feat: move changes behind a flag * fix: use async IIFE inside useEffect * fix: fix line length * fix: remove default value in dev * fix: improve coverage by adding conditional test based on env value * refactor: move logic inside function * refactor: move functions to utils * fix: ignore merge config
This commit is contained in:
1
.env
1
.env
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL=''
|
|||||||
CSRF_TOKEN_API_PATH=''
|
CSRF_TOKEN_API_PATH=''
|
||||||
DISCOVERY_API_BASE_URL=''
|
DISCOVERY_API_BASE_URL=''
|
||||||
DISCUSSIONS_MFE_BASE_URL=''
|
DISCUSSIONS_MFE_BASE_URL=''
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL=''
|
ECOMMERCE_BASE_URL=''
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
|
|||||||
@@ -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'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
|
|||||||
@@ -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'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||||
|
DISCOUNT_CODE_INFO_URL=''
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
|
|||||||
@@ -166,11 +166,13 @@ subscribe(APP_INIT_ERROR, (error) => {
|
|||||||
initialize({
|
initialize({
|
||||||
handlers: {
|
handlers: {
|
||||||
config: () => {
|
config: () => {
|
||||||
|
/* istanbul ignore next */
|
||||||
mergeConfig({
|
mergeConfig({
|
||||||
CONTACT_URL: process.env.CONTACT_URL || null,
|
CONTACT_URL: process.env.CONTACT_URL || null,
|
||||||
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
||||||
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
|
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
|
||||||
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_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_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
||||||
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
|
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
|
||||||
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons';
|
import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +15,12 @@ import { useModel } from '../../generic/model-store';
|
|||||||
import StreakMobileImage from './assets/Streak_mobile.png';
|
import StreakMobileImage from './assets/Streak_mobile.png';
|
||||||
import StreakDesktopImage from './assets/Streak_desktop.png';
|
import StreakDesktopImage from './assets/Streak_desktop.png';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { recordModalClosing, recordStreakCelebration } from './utils';
|
import {
|
||||||
|
calculateVoucherDiscountPercentage,
|
||||||
|
getDiscountCodePercentage,
|
||||||
|
recordModalClosing,
|
||||||
|
recordStreakCelebration,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function getRandomFactoid(intl, streakLength) {
|
function getRandomFactoid(intl, streakLength) {
|
||||||
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
|
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
|
||||||
@@ -42,13 +46,6 @@ function getRandomFactoid(intl, streakLength) {
|
|||||||
return factoids[Math.floor(Math.random() * (factoids.length))];
|
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 }) => (
|
const CloseText = ({ intl }) => (
|
||||||
<span>
|
<span>
|
||||||
{intl.formatMessage(messages.streakButton)}
|
{intl.formatMessage(messages.streakButton)}
|
||||||
@@ -83,34 +80,38 @@ const StreakModal = ({
|
|||||||
|
|
||||||
// Ask ecommerce to calculate discount savings
|
// Ask ecommerce to calculate discount savings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (streakDiscountCouponEnabled && verifiedMode && getConfig().ECOMMERCE_BASE_URL) {
|
(async () => {
|
||||||
calculateVoucherDiscount(discountCode, verifiedMode.sku, username)
|
let streakDiscountPercentage = 0;
|
||||||
.then(
|
try {
|
||||||
(result) => {
|
if (streakDiscountCouponEnabled && verifiedMode) {
|
||||||
const { totalInclTax, totalInclTaxExclDiscounts } = result.data;
|
// If the discount service is available, use it to get the discount percentage
|
||||||
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
|
if (getConfig().DISCOUNT_CODE_INFO_URL) {
|
||||||
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
|
streakDiscountPercentage = await getDiscountCodePercentage(
|
||||||
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
|
discountCode,
|
||||||
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
|
courseId,
|
||||||
// multiplied by the calculated percentage.
|
);
|
||||||
setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts);
|
// If the discount service is not available, fall back to ecommerce to calculate the discount percentage
|
||||||
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
|
} else if (getConfig().ECOMMERCE_BASE_URL) {
|
||||||
course_id: courseId,
|
streakDiscountPercentage = await calculateVoucherDiscountPercentage(
|
||||||
sku: verifiedMode.sku,
|
discountCode,
|
||||||
});
|
verifiedMode.sku,
|
||||||
} else {
|
username,
|
||||||
setDiscountPercent(0);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
() => {
|
} catch {
|
||||||
// ignore any errors - we just won't show the discount to the user then
|
// ignore any errors - we just won't show the discount to the user then
|
||||||
setDiscountPercent(0);
|
} finally {
|
||||||
},
|
if (streakDiscountPercentage) {
|
||||||
);
|
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
|
||||||
} else {
|
course_id: courseId,
|
||||||
setDiscountPercent(0);
|
sku: verifiedMode.sku,
|
||||||
}
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}
|
||||||
|
setDiscountPercent(streakDiscountPercentage);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [streakDiscountCouponEnabled, username, verifiedMode]);
|
}, [streakDiscountCouponEnabled, username, verifiedMode]);
|
||||||
|
|
||||||
if (!isStreakCelebrationOpen) {
|
if (!isStreakCelebrationOpen) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Factory } from 'rosie';
|
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 { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { breakpoints } from '@openedx/paragon';
|
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() {
|
function setDiscountError() {
|
||||||
mockData.streakDiscountCouponEnabled = true;
|
mockData.streakDiscountCouponEnabled = true;
|
||||||
axiosMock.onGet(calculateUrl).reply(500);
|
axiosMock.onGet(calculateUrl).reply(500);
|
||||||
@@ -105,4 +118,22 @@ describe('Loaded Tab Page', () => {
|
|||||||
sku: mockData.verifiedMode.sku,
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
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';
|
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,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user