Skip to content

Commit 9cbc227

Browse files
authored
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
1 parent 4c8aa7c commit 9cbc227

File tree

7 files changed

+117
-41
lines changed

7 files changed

+117
-41
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL=''
1212
CSRF_TOKEN_API_PATH=''
1313
DISCOVERY_API_BASE_URL=''
1414
DISCUSSIONS_MFE_BASE_URL=''
15+
DISCOUNT_CODE_INFO_URL=''
1516
ECOMMERCE_BASE_URL=''
1617
ENABLE_JUMPNAV='true'
1718
ENABLE_NOTICES=''

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co
1212
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
1313
DISCOVERY_API_BASE_URL='http://localhost:18381'
1414
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
15+
DISCOUNT_CODE_INFO_URL=''
1516
ECOMMERCE_BASE_URL='http://localhost:18130'
1617
ENABLE_JUMPNAV='true'
1718
ENABLE_NOTICES=''

.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co
1212
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
1313
DISCOVERY_API_BASE_URL='http://localhost:18381'
1414
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
15+
DISCOUNT_CODE_INFO_URL=''
1516
ECOMMERCE_BASE_URL='http://localhost:18130'
1617
ENABLE_JUMPNAV='true'
1718
ENABLE_NOTICES=''

src/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,13 @@ subscribe(APP_INIT_ERROR, (error) => {
166166
initialize({
167167
handlers: {
168168
config: () => {
169+
/* istanbul ignore next */
169170
mergeConfig({
170171
CONTACT_URL: process.env.CONTACT_URL || null,
171172
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
172173
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
173174
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
175+
DISCOUNT_CODE_INFO_URL: process.env.DISCOUNT_CODE_INFO_URL || null,
174176
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
175177
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
176178
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,

src/shared/streak-celebration/StreakCelebrationModal.jsx

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/* eslint-disable react/prop-types */
22
import React, { useEffect, useState } from 'react';
33
import PropTypes from 'prop-types';
4-
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
4+
import { getConfig } from '@edx/frontend-platform';
55
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
6-
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
76
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
87
import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons';
98
import {
@@ -16,7 +15,12 @@ import { useModel } from '../../generic/model-store';
1615
import StreakMobileImage from './assets/Streak_mobile.png';
1716
import StreakDesktopImage from './assets/Streak_desktop.png';
1817
import messages from './messages';
19-
import { recordModalClosing, recordStreakCelebration } from './utils';
18+
import {
19+
calculateVoucherDiscountPercentage,
20+
getDiscountCodePercentage,
21+
recordModalClosing,
22+
recordStreakCelebration,
23+
} from './utils';
2024

2125
function getRandomFactoid(intl, streakLength) {
2226
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
@@ -42,13 +46,6 @@ function getRandomFactoid(intl, streakLength) {
4246
return factoids[Math.floor(Math.random() * (factoids.length))];
4347
}
4448

45-
async function calculateVoucherDiscount(voucher, sku, username) {
46-
const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`;
47-
const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`;
48-
return getAuthenticatedHttpClient().get(url)
49-
.then(res => camelCaseObject(res));
50-
}
51-
5249
const CloseText = ({ intl }) => (
5350
<span>
5451
{intl.formatMessage(messages.streakButton)}
@@ -83,34 +80,38 @@ const StreakModal = ({
8380

8481
// Ask ecommerce to calculate discount savings
8582
useEffect(() => {
86-
if (streakDiscountCouponEnabled && verifiedMode && getConfig().ECOMMERCE_BASE_URL) {
87-
calculateVoucherDiscount(discountCode, verifiedMode.sku, username)
88-
.then(
89-
(result) => {
90-
const { totalInclTax, totalInclTaxExclDiscounts } = result.data;
91-
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
92-
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
93-
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
94-
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
95-
// multiplied by the calculated percentage.
96-
setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts);
97-
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
98-
course_id: courseId,
99-
sku: verifiedMode.sku,
100-
});
101-
} else {
102-
setDiscountPercent(0);
103-
}
104-
},
105-
() => {
106-
// ignore any errors - we just won't show the discount to the user then
107-
setDiscountPercent(0);
108-
},
109-
);
110-
} else {
111-
setDiscountPercent(0);
112-
}
113-
// eslint-disable-next-line react-hooks/exhaustive-deps
83+
(async () => {
84+
let streakDiscountPercentage = 0;
85+
try {
86+
if (streakDiscountCouponEnabled && verifiedMode) {
87+
// If the discount service is available, use it to get the discount percentage
88+
if (getConfig().DISCOUNT_CODE_INFO_URL) {
89+
streakDiscountPercentage = await getDiscountCodePercentage(
90+
discountCode,
91+
courseId,
92+
);
93+
// If the discount service is not available, fall back to ecommerce to calculate the discount percentage
94+
} else if (getConfig().ECOMMERCE_BASE_URL) {
95+
streakDiscountPercentage = await calculateVoucherDiscountPercentage(
96+
discountCode,
97+
verifiedMode.sku,
98+
username,
99+
);
100+
}
101+
}
102+
} catch {
103+
// ignore any errors - we just won't show the discount to the user then
104+
} finally {
105+
if (streakDiscountPercentage) {
106+
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
107+
course_id: courseId,
108+
sku: verifiedMode.sku,
109+
});
110+
}
111+
setDiscountPercent(streakDiscountPercentage);
112+
}
113+
})();
114+
// eslint-disable-next-line react-hooks/exhaustive-deps
114115
}, [streakDiscountCouponEnabled, username, verifiedMode]);
115116

116117
if (!isStreakCelebrationOpen) {

src/shared/streak-celebration/StreakCelebrationModal.test.jsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { Factory } from 'rosie';
3-
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
3+
import { camelCaseObject, getConfig, mergeConfig } from '@edx/frontend-platform';
44
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
55
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
66
import { breakpoints } from '@openedx/paragon';
@@ -34,6 +34,19 @@ describe('Loaded Tab Page', () => {
3434
});
3535
}
3636

37+
function setDiscountViaDiscountCodeInfo(percent) {
38+
const discountURLParams = new URLSearchParams();
39+
discountURLParams.append('code', 'ZGY11119949');
40+
discountURLParams.append('course_run_key', courseMetadata.id);
41+
const discountURL = `${getConfig().DISCOUNT_CODE_INFO_URL}?${discountURLParams.toString()}`;
42+
43+
mockData.streakDiscountCouponEnabled = true;
44+
axiosMock.onGet(discountURL).reply(200, {
45+
isApplicable: true,
46+
discountPercentage: percent / 100,
47+
});
48+
}
49+
3750
function setDiscountError() {
3851
mockData.streakDiscountCouponEnabled = true;
3952
axiosMock.onGet(calculateUrl).reply(500);
@@ -105,4 +118,22 @@ describe('Loaded Tab Page', () => {
105118
sku: mockData.verifiedMode.sku,
106119
});
107120
});
121+
122+
it('shows discount version of streak celebration modal when discount available and info fetched using DISCOUNT_CODE_INFO_URL', async () => {
123+
mergeConfig({ DISCOUNT_CODE_INFO_URL: 'http://localhost:8140/lms/discount-code-info/' });
124+
125+
global.innerWidth = breakpoints.extraSmall.maxWidth;
126+
setDiscountViaDiscountCodeInfo(14);
127+
await renderModal();
128+
129+
const endDateText = `Ends ${new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toLocaleDateString({ timeZone: 'UTC' })}.`;
130+
expect(screen.getByText('You’ve unlocked a 14% off discount when you upgrade this course for a limited time only.', { exact: false })).toBeInTheDocument();
131+
expect(screen.getByText(endDateText, { exact: false })).toBeInTheDocument();
132+
expect(screen.getByText('Continue with course')).toBeInTheDocument();
133+
expect(screen.queryByText('Keep it up')).not.toBeInTheDocument();
134+
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.course.streak_discount_enabled', {
135+
course_id: mockData.courseId,
136+
sku: mockData.verifiedMode.sku,
137+
});
138+
});
108139
});

src/shared/streak-celebration/utils.jsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
2-
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
2+
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
3+
import {
4+
getAuthenticatedHttpClient,
5+
getAuthenticatedUser,
6+
} from '@edx/frontend-platform/auth';
37

48
import { updateModel } from '../../generic/model-store';
59

@@ -24,4 +28,39 @@ function recordModalClosing(celebrations, org, courseId, dispatch) {
2428
}));
2529
}
2630

27-
export { recordStreakCelebration, recordModalClosing };
31+
async function calculateVoucherDiscountPercentage(voucher, sku, username) {
32+
const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`;
33+
const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`;
34+
35+
const result = await getAuthenticatedHttpClient().get(url);
36+
const { totalInclTax, totalInclTaxExclDiscounts } = camelCaseObject(result).data;
37+
38+
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
39+
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
40+
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
41+
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
42+
// multiplied by the calculated percentage.
43+
return 1 - totalInclTax / totalInclTaxExclDiscounts;
44+
}
45+
46+
return 0;
47+
}
48+
49+
async function getDiscountCodePercentage(code, courseId) {
50+
const params = new URLSearchParams();
51+
params.append('code', code);
52+
params.append('course_run_key', courseId);
53+
const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`;
54+
55+
const result = await getAuthenticatedHttpClient().get(url);
56+
const { isApplicable, discountPercentage } = camelCaseObject(result).data;
57+
58+
return isApplicable ? +discountPercentage : 0;
59+
}
60+
61+
export {
62+
calculateVoucherDiscountPercentage,
63+
getDiscountCodePercentage,
64+
recordModalClosing,
65+
recordStreakCelebration,
66+
};

0 commit comments

Comments
 (0)