Skip to content

Commit

Permalink
feat: enrollment flow text revisions when course is assigned (openedx…
Browse files Browse the repository at this point in the history
…#864)

* feat: enrollment flow text revisions when course is assigned

* fix: handle subsidy context issue
  • Loading branch information
jajjibhai008 authored Nov 8, 2023
1 parent 72bf40c commit 5f29710
Show file tree
Hide file tree
Showing 15 changed files with 177 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -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'
9 changes: 7 additions & 2 deletions src/components/app/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -60,7 +61,9 @@ const App = () => {
path="/:enterpriseSlug/executive-education-2u"
render={(routeProps) => (
<AuthenticatedPage>
<ExecutiveEducation2UPage {...routeProps} />
<UserSubsidy>
<ExecutiveEducation2UPage {...routeProps} />
</UserSubsidy>
</AuthenticatedPage>
)}
/>
Expand All @@ -69,7 +72,9 @@ const App = () => {
path="/:enterpriseSlug/executive-education-2u/enrollment-completed"
render={(routeProps) => (
<AuthenticatedPage>
<EnrollmentCompleted {...routeProps} />
<UserSubsidy>
<EnrollmentCompleted {...routeProps} />
</UserSubsidy>
</AuthenticatedPage>
)}
/>
Expand Down
23 changes: 23 additions & 0 deletions src/components/course/CourseSidebarPrice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand All @@ -35,6 +47,17 @@ const CourseSidebarPrice = () => {
</del>
);

if (features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && coursePrice && isCourseAssigned) {
return (
<>
<div>
{crossedOutOriginalPrice} <strong>$0</strong>
</div>
<span className="small">{ASSIGNED_COURSE_MESSAGE}</span>
</>
);
}

// Case 1: License subsidy found
if (userSubsidyApplicableToCourse?.subsidyType === LICENSE_SUBSIDY_TYPE) {
return (
Expand Down
13 changes: 11 additions & 2 deletions src/components/course/course-header/CourseHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Container,
Row,
Col,
Badge,
} from '@edx/paragon';
import { Link } from 'react-router-dom';
import { AppContext } from '@edx/frontend-platform/react';
Expand All @@ -18,12 +19,14 @@ import {
getDefaultProgram,
formatProgramType,
} from '../data/utils';
import { useCoursePartners } from '../data/hooks';
import { useCoursePartners, useIsCourseAssigned } from '../data/hooks';
import LicenseRequestedAlert from '../LicenseRequestedAlert';
import SubsidyRequestButton from '../SubsidyRequestButton';
import CourseReview from '../CourseReview';

import CoursePreview from './CoursePreview';
import { UserSubsidyContext } from '../../enterprise-user-subsidy';
import { features } from '../../../config';

const CourseHeader = () => {
const { enterpriseConfig } = useContext(AppContext);
Expand All @@ -34,6 +37,11 @@ const CourseHeader = () => {
},
isPolicyRedemptionEnabled,
} = useContext(CourseContext);
const {
redeemableLearnerCreditPolicies,
} = useContext(UserSubsidyContext);
const isCourseAssigned = useIsCourseAssigned(redeemableLearnerCreditPolicies, course?.key);

const [partners] = useCoursePartners(course);

const defaultProgram = useMemo(
Expand Down Expand Up @@ -80,8 +88,9 @@ const CourseHeader = () => {
))}
</div>
)}
<div className={classNames({ 'mb-4': !course.shortDescription })}>
<div className={classNames({ 'mb-4': !course.shortDescription, 'd-flex': true, 'align-items-center': true })}>
<h2>{course.title}</h2>
{(features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && isCourseAssigned) && <Badge variant="info" className="ml-4">Assigned</Badge>}
</div>
{course.shortDescription && (
<div
Expand Down
9 changes: 9 additions & 0 deletions src/components/course/data/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,12 @@ export const useExternalEnrollmentFailureReason = () => {
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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -59,12 +60,22 @@ const appContextValue = {

const ExternalCourseEnrollmentConfirmationWrapper = ({
courseContextValue = baseCourseContextValue,
initialUserSubsidyState = {
subscriptionLicense: null,
couponCodes: {
couponCodes: [{ discountValue: 90 }],
couponCodesCount: 0,
},
redeemableLearnerCreditPolicies: [],
},
}) => (
<IntlProvider locale="en">
<AppContext.Provider value={appContextValue}>
<CourseContext.Provider value={courseContextValue}>
<ExternalCourseEnrollmentConfirmation />
</CourseContext.Provider>
<UserSubsidyContext.Provider value={initialUserSubsidyState}>
<CourseContext.Provider value={courseContextValue}>
<ExternalCourseEnrollmentConfirmation />
</CourseContext.Provider>
</UserSubsidyContext.Provider>
</AppContext.Provider>
</IntlProvider>
);
Expand Down
17 changes: 14 additions & 3 deletions src/components/course/tests/CourseSidebarPrice.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -95,12 +96,22 @@ const SidebarWithContext = ({
initialAppState = appStateWithOrigPriceHidden,
subsidyRequestsState = defaultSubsidyRequestsState,
courseContextProps = {},
initialUserSubsidyState = {
subscriptionLicense: null,
couponCodes: {
couponCodes: [{ discountValue: 90 }],
couponCodesCount: 0,
},
redeemableLearnerCreditPolicies: [],
},
}) => (
<AppContext.Provider value={initialAppState}>
<SubsidyRequestsContext.Provider value={subsidyRequestsState}>
<CourseContextProvider {...courseContextProps}>
<CourseSidebarPrice />
</CourseContextProvider>
<UserSubsidyContext.Provider value={initialUserSubsidyState}>
<CourseContextProvider {...courseContextProps}>
<CourseSidebarPrice />
</CourseContextProvider>
</UserSubsidyContext.Provider>
</SubsidyRequestsContext.Provider>
</AppContext.Provider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const EnrollmentCompleted = () => {
courseMetadata={location.state.data}
enrollmentCompleted
/>
<EnrollmentCompletedSummaryCard />
<EnrollmentCompletedSummaryCard
courseKey={location?.state?.data?.key}
/>
</Container>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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: [],
},
}) => (
<AppContext.Provider value={appContextValue}>
<EnrollmentCompleted />
<UserSubsidyContext.Provider value={initialUserSubsidyState}>
<CourseContext.Provider value={defaultCourseContext}>
<EnrollmentCompleted />
</CourseContext.Provider>
</UserSubsidyContext.Provider>
</AppContext.Provider>
);

Expand Down
14 changes: 11 additions & 3 deletions src/components/executive-education-2u/ExecutiveEducation2UPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,26 @@ 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();

const isExecEd2UFulfillmentEnabled = useMemo(() => {
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.`);
Expand All @@ -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);
Expand All @@ -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),
Expand Down Expand Up @@ -134,7 +140,9 @@ const ExecutiveEducation2UPage = () => {
</strong>
&nbsp; Please ensure that the course details below are correct and confirm using Learner
Credit with a &quot;Confirm registration&quot; 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.'}
</p>
</Col>
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: [],
},
}) => (
<IntlProvider locale="en">
<AppContext.Provider value={appContextValue}>
<ExecutiveEducation2UPage />
<UserSubsidyContext.Provider value={initialUserSubsidyState}>
<CourseContext.Provider value={courseContextValue}>
<ExecutiveEducation2UPage />
</CourseContext.Provider>
</UserSubsidyContext.Provider>
</AppContext.Provider>
</IntlProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading

0 comments on commit 5f29710

Please sign in to comment.