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=''
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
DISCUSSIONS_MFE_BASE_URL=''
|
||||
DISCOUNT_CODE_INFO_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_JUMPNAV='true'
|
||||
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'
|
||||
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=''
|
||||
|
||||
@@ -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=''
|
||||
|
||||
@@ -166,11 +166,13 @@ subscribe(APP_INIT_ERROR, (error) => {
|
||||
initialize({
|
||||
handlers: {
|
||||
config: () => {
|
||||
/* istanbul ignore next */
|
||||
mergeConfig({
|
||||
CONTACT_URL: process.env.CONTACT_URL || null,
|
||||
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,
|
||||
|
||||
@@ -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 }) => (
|
||||
<span>
|
||||
{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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user