diff --git a/.env.development b/.env.development
index 3c0bbe0461..444be43740 100644
--- a/.env.development
+++ b/.env.development
@@ -56,3 +56,4 @@ GETSMARTER_PRIVACY_POLICY_URL='https://www.getsmarter.com/privacy-policy'
GETSMARTER_LEARNER_DASHBOARD_URL='https://www.getsmarter.com/account'
FEATURE_CONTENT_HIGHLIGHTS='true'
FEATURE_ENABLE_EMET_REDEMPTION='true'
+FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT='true'
diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx
index 123c24efbc..eda170308c 100644
--- a/src/components/app/App.jsx
+++ b/src/components/app/App.jsx
@@ -21,6 +21,7 @@ import { EnterpriseInvitePage } from '../enterprise-invite';
import { ExecutiveEducation2UPage } from '../executive-education-2u';
import { ToastsProvider, Toasts } from '../Toasts';
import EnrollmentCompleted from '../executive-education-2u/EnrollmentCompleted';
+import { UserSubsidy } from '../enterprise-user-subsidy';
// Create a query client for @tanstack/react-query
const queryClient = new QueryClient();
@@ -60,7 +61,9 @@ const App = () => {
path="/:enterpriseSlug/executive-education-2u"
render={(routeProps) => (
-
+
+
+
)}
/>
@@ -69,7 +72,9 @@ const App = () => {
path="/:enterpriseSlug/executive-education-2u/enrollment-completed"
render={(routeProps) => (
-
+
+
+
)}
/>
diff --git a/src/components/course/CourseSidebarPrice.jsx b/src/components/course/CourseSidebarPrice.jsx
index 4825a2aa04..f80d0a43b1 100644
--- a/src/components/course/CourseSidebarPrice.jsx
+++ b/src/components/course/CourseSidebarPrice.jsx
@@ -8,8 +8,12 @@ import { numberWithPrecision } from './data/utils';
import { SubsidyRequestsContext } from '../enterprise-subsidy-requests';
import { ENTERPRISE_OFFER_SUBSIDY_TYPE, LEARNER_CREDIT_SUBSIDY_TYPE, LICENSE_SUBSIDY_TYPE } from './data/constants';
import { canUserRequestSubsidyForCourse } from './enrollment/utils';
+import { UserSubsidyContext } from '../enterprise-user-subsidy';
+import { useIsCourseAssigned } from './data/hooks';
+import { features } from '../../config';
export const INCLUDED_IN_SUBSCRIPTION_MESSAGE = 'Included in your subscription';
+export const ASSIGNED_COURSE_MESSAGE = 'This course is assigned to you. The price of this course is already covered by your organization.';
export const FREE_WHEN_APPROVED_MESSAGE = 'Free to me\n(when approved)';
export const COVERED_BY_ENTERPRISE_OFFER_MESSAGE = 'This course can be purchased with your organization\'s learner credit';
@@ -20,7 +24,15 @@ const CourseSidebarPrice = () => {
coursePrice,
currency,
subsidyRequestCatalogsApplicableToCourse,
+ state: {
+ course,
+ },
} = useContext(CourseContext);
+ const {
+ redeemableLearnerCreditPolicies,
+ } = useContext(UserSubsidyContext);
+ const isCourseAssigned = useIsCourseAssigned(redeemableLearnerCreditPolicies, course?.key);
+
const { subsidyRequestConfiguration } = useContext(SubsidyRequestsContext);
if (!coursePrice) {
@@ -35,6 +47,17 @@ const CourseSidebarPrice = () => {
);
+ if (features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && coursePrice && isCourseAssigned) {
+ return (
+ <>
+
+
{course.title}
+ {(features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && isCourseAssigned) && Assigned}
{course.shortDescription && (
{
hasSuccessfulRedemption,
]);
};
+
+export const useIsCourseAssigned = (redeemableLCPolicies, courseKey) => {
+ if (!redeemableLCPolicies || redeemableLCPolicies.length < 1) { return false; }
+
+ const learnerContentAssignmentsArray = redeemableLCPolicies.flatMap(item => item?.learnerContentAssignments || []);
+ if (!learnerContentAssignmentsArray) { return false; }
+
+ return learnerContentAssignmentsArray.some(assignment => assignment?.contentKey === courseKey);
+};
diff --git a/src/components/course/routes/tests/ExternalCourseEnrollmentConfirmation.test.jsx b/src/components/course/routes/tests/ExternalCourseEnrollmentConfirmation.test.jsx
index 38bd484f54..9de97f5d6f 100644
--- a/src/components/course/routes/tests/ExternalCourseEnrollmentConfirmation.test.jsx
+++ b/src/components/course/routes/tests/ExternalCourseEnrollmentConfirmation.test.jsx
@@ -6,6 +6,7 @@ import { AppContext } from '@edx/frontend-platform/react';
import ExternalCourseEnrollmentConfirmation from '../ExternalCourseEnrollmentConfirmation';
import { CourseContext } from '../../CourseContextProvider';
import { DISABLED_ENROLL_REASON_TYPES, LEARNER_CREDIT_SUBSIDY_TYPE } from '../../data/constants';
+import { UserSubsidyContext } from '../../../enterprise-user-subsidy';
jest.mock('@edx/frontend-platform/config', () => ({
...jest.requireActual('@edx/frontend-platform/config'),
@@ -59,12 +60,22 @@ const appContextValue = {
const ExternalCourseEnrollmentConfirmationWrapper = ({
courseContextValue = baseCourseContextValue,
+ initialUserSubsidyState = {
+ subscriptionLicense: null,
+ couponCodes: {
+ couponCodes: [{ discountValue: 90 }],
+ couponCodesCount: 0,
+ },
+ redeemableLearnerCreditPolicies: [],
+ },
}) => (
-
-
-
+
+
+
+
+
);
diff --git a/src/components/course/tests/CourseSidebarPrice.test.jsx b/src/components/course/tests/CourseSidebarPrice.test.jsx
index a40488cdd5..e3557e0698 100644
--- a/src/components/course/tests/CourseSidebarPrice.test.jsx
+++ b/src/components/course/tests/CourseSidebarPrice.test.jsx
@@ -12,6 +12,7 @@ import {
SUBSIDY_DISCOUNT_TYPE_MAP,
ENTERPRISE_OFFER_SUBSIDY_TYPE,
} from '../data/constants';
+import { UserSubsidyContext } from '../../enterprise-user-subsidy';
const appStateWithOrigPriceHidden = {
enterpriseConfig: {
@@ -95,12 +96,22 @@ const SidebarWithContext = ({
initialAppState = appStateWithOrigPriceHidden,
subsidyRequestsState = defaultSubsidyRequestsState,
courseContextProps = {},
+ initialUserSubsidyState = {
+ subscriptionLicense: null,
+ couponCodes: {
+ couponCodes: [{ discountValue: 90 }],
+ couponCodesCount: 0,
+ },
+ redeemableLearnerCreditPolicies: [],
+ },
}) => (
-
-
-
+
+
+
+
+
);
diff --git a/src/components/executive-education-2u/EnrollmentCompleted.jsx b/src/components/executive-education-2u/EnrollmentCompleted.jsx
index 8a5b0e6dc0..f06caf2cdf 100644
--- a/src/components/executive-education-2u/EnrollmentCompleted.jsx
+++ b/src/components/executive-education-2u/EnrollmentCompleted.jsx
@@ -23,7 +23,9 @@ const EnrollmentCompleted = () => {
courseMetadata={location.state.data}
enrollmentCompleted
/>
-
+
);
diff --git a/src/components/executive-education-2u/EnrollmentCompleted.test.jsx b/src/components/executive-education-2u/EnrollmentCompleted.test.jsx
index 0075c8e411..080e4c58c1 100644
--- a/src/components/executive-education-2u/EnrollmentCompleted.test.jsx
+++ b/src/components/executive-education-2u/EnrollmentCompleted.test.jsx
@@ -6,6 +6,8 @@ import { renderWithRouter } from '@edx/frontend-enterprise-utils';
import EnrollmentCompleted from './EnrollmentCompleted';
import { CURRENCY_USD } from '../course/data/constants';
+import { CourseContext } from '../course/CourseContextProvider';
+import { UserSubsidyContext } from '../enterprise-user-subsidy';
const enterpriseSlug = 'test-enterprise-slug';
const initialAppContextValue = {
@@ -44,14 +46,45 @@ jest.mock('@edx/frontend-platform/config', () => ({
getConfig: jest.fn(() => ({
GETSMARTER_STUDENT_TC_URL: 'https://example.url',
GETSMARTER_LEARNER_DASHBOARD_URL: 'https://getsmarter.example.com/account',
+ BASE_URL: 'http://enterprise.edx.org/',
})),
}));
+const mockCourseRunKey = 'course-run-key';
+const mockCourseRun = {
+ key: mockCourseRunKey,
+ uuid: 'course-run-uuid',
+};
+const mockCourseKey = 'course-key';
+const defaultCourseContext = {
+ state: {
+ availableCourseRuns: [mockCourseRun],
+ userEntitlements: [],
+ userEnrollments: [],
+ course: { key: mockCourseKey, entitlements: [] },
+ catalog: { catalogList: [] },
+ },
+ subsidyRequestCatalogsApplicableToCourse: [],
+ missingUserSubsidyReason: undefined,
+ redeemabilityPerContentKey: [],
+};
const EnrollmentCompletedWrapper = ({
appContextValue = initialAppContextValue,
+ initialUserSubsidyState = {
+ subscriptionLicense: null,
+ couponCodes: {
+ couponCodes: [{ discountValue: 90 }],
+ couponCodesCount: 0,
+ },
+ redeemableLearnerCreditPolicies: [],
+ },
}) => (
-
+
+
+
+
+
);
diff --git a/src/components/executive-education-2u/ExecutiveEducation2UPage.jsx b/src/components/executive-education-2u/ExecutiveEducation2UPage.jsx
index 09c67d34c4..53d32f601a 100644
--- a/src/components/executive-education-2u/ExecutiveEducation2UPage.jsx
+++ b/src/components/executive-education-2u/ExecutiveEducation2UPage.jsx
@@ -20,9 +20,15 @@ import CourseSummaryCard from './components/CourseSummaryCard';
import RegistrationSummaryCard from './components/RegistrationSummaryCard';
import { getActiveCourseRun, getCourseStartDate } from '../course/data/utils';
import { getCourseOrganizationDetails, getExecutiveEducationCoursePrice } from './utils';
+import { UserSubsidyContext } from '../enterprise-user-subsidy';
+import { useIsCourseAssigned } from '../course/data/hooks';
+import { features } from '../../config';
const ExecutiveEducation2UPage = () => {
const { enterpriseConfig } = useContext(AppContext);
+ const {
+ redeemableLearnerCreditPolicies,
+ } = useContext(UserSubsidyContext);
const activeQueryParams = useActiveQueryParams();
const history = useHistory();
@@ -30,12 +36,10 @@ const ExecutiveEducation2UPage = () => {
const hasRequiredQueryParams = (activeQueryParams.has('course_uuid') && activeQueryParams.has('sku'));
return enterpriseConfig.enableExecutiveEducation2UFulfillment && hasRequiredQueryParams;
}, [enterpriseConfig, activeQueryParams]);
-
const { isLoadingContentMetadata: isLoading, contentMetadata } = useExecutiveEducation2UContentMetadata({
courseUUID: activeQueryParams.get('course_uuid'),
isExecEd2UFulfillmentEnabled,
});
-
useEffect(() => {
if (!enterpriseConfig.enableExecutiveEducation2UFulfillment) {
logError(`Enterprise ${enterpriseConfig.uuid} does not have executive education (2U) fulfillment enabled.`);
@@ -55,6 +59,7 @@ const ExecutiveEducation2UPage = () => {
sku: activeQueryParams.get('sku'),
};
+ const isCourseAssigned = useIsCourseAssigned(redeemableLearnerCreditPolicies, contentMetadata?.key);
const courseMetadata = useMemo(() => {
if (contentMetadata) {
const activeCourseRun = getActiveCourseRun(contentMetadata);
@@ -77,6 +82,7 @@ const ExecutiveEducation2UPage = () => {
marketingUrl: organizationDetails.organizationMarketingUrl,
},
title: contentMetadata.title,
+ key: contentMetadata.key,
startDate: getCourseStartDate({ contentMetadata, courseRun: activeCourseRun }),
duration: getDuration(),
priceDetails: getExecutiveEducationCoursePrice(contentMetadata),
@@ -134,7 +140,9 @@ const ExecutiveEducation2UPage = () => {
Please ensure that the course details below are correct and confirm using Learner
Credit with a "Confirm registration" button.
- Your Learner Credit funds will be redeemed at this point.
+ {(features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && isCourseAssigned)
+ ? 'Your learning administrator already allocated funds towards this registration.'
+ : 'Your Learner Credit funds will be redeemed at this point.'}
diff --git a/src/components/executive-education-2u/ExecutiveEducation2UPage.test.jsx b/src/components/executive-education-2u/ExecutiveEducation2UPage.test.jsx
index 35ba332b69..39825e7604 100644
--- a/src/components/executive-education-2u/ExecutiveEducation2UPage.test.jsx
+++ b/src/components/executive-education-2u/ExecutiveEducation2UPage.test.jsx
@@ -12,6 +12,8 @@ import {
useExecutiveEducation2UContentMetadata,
} from './data';
import { CURRENCY_USD, PAID_EXECUTIVE_EDUCATION } from '../course/data/constants';
+import { UserSubsidyContext } from '../enterprise-user-subsidy';
+import { CourseContext } from '../course/CourseContextProvider';
const mockReceiptPageUrl = 'https://edx.org';
const courseTitle = 'edX Demonstration Course';
@@ -115,12 +117,35 @@ const initialAppContextValue = {
},
};
+const baseCourseContextValue = {
+ state: {
+ courseEntitlementProductSku: 'test-sku',
+ course: {
+ organizationShortCodeOverride: 'Test Org',
+ organizationLogoOverrideUrl: 'https://test.org/logo.png',
+ },
+ },
+ missingUserSubsidyReason: undefined,
+};
const ExecutiveEducation2UPageWrapper = ({
appContextValue = initialAppContextValue,
+ courseContextValue = baseCourseContextValue,
+ initialUserSubsidyState = {
+ subscriptionLicense: null,
+ couponCodes: {
+ couponCodes: [{ discountValue: 90 }],
+ couponCodesCount: 0,
+ },
+ redeemableLearnerCreditPolicies: [],
+ },
}) => (
-
+
+
+
+
+
);
diff --git a/src/components/executive-education-2u/components/CourseSummaryCard.jsx b/src/components/executive-education-2u/components/CourseSummaryCard.jsx
index ec9cfa82c4..6b13dcc3e2 100644
--- a/src/components/executive-education-2u/components/CourseSummaryCard.jsx
+++ b/src/components/executive-education-2u/components/CourseSummaryCard.jsx
@@ -86,6 +86,7 @@ CourseSummaryCard.propTypes = {
logoImgUrl: PropTypes.string,
}).isRequired,
title: PropTypes.string.isRequired,
+ key: PropTypes.string.isRequired,
startDate: PropTypes.string.isRequired,
duration: PropTypes.string.isRequired,
priceDetails: PropTypes.shape({
diff --git a/src/components/executive-education-2u/components/EnrollmentCompletedSummaryCard.jsx b/src/components/executive-education-2u/components/EnrollmentCompletedSummaryCard.jsx
index 7e6878570d..e5133471a4 100644
--- a/src/components/executive-education-2u/components/EnrollmentCompletedSummaryCard.jsx
+++ b/src/components/executive-education-2u/components/EnrollmentCompletedSummaryCard.jsx
@@ -1,22 +1,31 @@
import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
import {
Card, Col, Hyperlink, Row,
} from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform/config';
import { AppContext } from '@edx/frontend-platform/react';
import GetSmarterLogo from '../../../assets/icons/get-smarter-logo-black.svg';
+import { UserSubsidyContext } from '../../enterprise-user-subsidy';
+import { useIsCourseAssigned } from '../../course/data/hooks';
+import { features } from '../../../config';
-const EnrollmentCompletedSummaryCard = () => {
+const EnrollmentCompletedSummaryCard = ({ courseKey }) => {
const config = getConfig();
const {
- enterpriseConfig: { authOrgId },
+ enterpriseConfig: { authOrgId, slug },
} = useContext(AppContext);
-
+ const {
+ redeemableLearnerCreditPolicies,
+ } = useContext(UserSubsidyContext);
+ const isCourseAssigned = useIsCourseAssigned(redeemableLearnerCreditPolicies, courseKey);
const externalDashboardQueryParams = new URLSearchParams({
org_id: authOrgId,
});
const externalDashboardQueryString = externalDashboardQueryParams ? `?${externalDashboardQueryParams.toString()}` : '';
const externalDashboardUrl = `${config.GETSMARTER_LEARNER_DASHBOARD_URL}${externalDashboardQueryString ?? ''}`;
+ const enterpriseSlug = `/${slug}`;
+ const dashboardUrl = `${config.BASE_URL}${enterpriseSlug}`;
return (
@@ -39,10 +48,11 @@ const EnrollmentCompletedSummaryCard = () => {
GetSmarter will email you when your course starts. Alternatively, you can visit your{' '}
- GetSmarter learner dashboard
+ {(features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && isCourseAssigned) ? 'edX dashboard' : 'GetSmarter learner dashboard'}
{' '}for course status updates.
@@ -72,4 +82,8 @@ const EnrollmentCompletedSummaryCard = () => {
);
};
+EnrollmentCompletedSummaryCard.propTypes = {
+ courseKey: PropTypes.string.isRequired,
+};
+
export default EnrollmentCompletedSummaryCard;
diff --git a/src/config/constants.js b/src/config/constants.js
index 4eaf2c7024..0f1f98d0e1 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -8,6 +8,7 @@ export const FEATURE_ENABLE_MY_CAREER = 'ENABLE_MY_CAREER';
export const FEATURE_PROGRAM_TYPE_FACET = 'ENABLE_PROGRAM_TYPE_FACET';
export const FEATURE_ENABLE_AUTO_APPLIED_LICENSES = 'FEATURE_ENABLE_AUTO_APPLIED_LICENSES';
export const FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS = 'FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS';
+export const FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT = 'FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT';
// Subscription expiration constants
export const SUBSCRIPTION_DAYS_REMAINING_SEVERE = 60;
diff --git a/src/config/index.js b/src/config/index.js
index 9d7f64bdde..f000bb7fa9 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -10,6 +10,7 @@ import {
FEATURE_PROGRAM_TYPE_FACET,
FEATURE_ENABLE_AUTO_APPLIED_LICENSES,
FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS,
+ FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT,
} from './constants';
export const features = {
@@ -27,4 +28,6 @@ export const features = {
|| hasFeatureFlagEnabled(FEATURE_ENABLE_PATHWAY_PROGRESS),
FEATURE_ENABLE_MY_CAREER: process.env.FEATURE_ENABLE_MY_CAREER
|| hasFeatureFlagEnabled(FEATURE_ENABLE_MY_CAREER),
+ FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT: process.env.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT
+ || hasFeatureFlagEnabled(FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT),
};