Skip to content

Commit

Permalink
fix: hide assignment card if existing in-progress enrollment card is …
Browse files Browse the repository at this point in the history
…potentially upgradeable (#1138)
  • Loading branch information
adamstankiewicz authored Jul 31, 2024
1 parent 7981e83 commit 70d9015
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 16 deletions.
14 changes: 14 additions & 0 deletions src/components/app/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,3 +733,17 @@ export const getSubsidyToApplyForCourse = ({

return undefined;
};

/**
* Determines whether the course enrollment can be upgraded to verified enrollment.
*
* @param {Object} enrollment Metadata about a course enrollment, containing the course mode and enrollment deadline.
* @returns {boolean} Whether the course enrollment can be upgraded to verified enrollment.
*/
export function isEnrollmentUpgradeable(enrollment) {
// Determine whether the course enrollment can be upgraded to verified enrollment, based
// on the course mode and enrollment deadline (if any).
const isEnrollByLapsed = enrollment.enrollBy ? dayjs().isAfter(dayjs(enrollment.enrollBy)) : false;
const canUpgradeToVerifiedEnrollment = enrollment.mode === COURSE_MODES_MAP.AUDIT && !isEnrollByLapsed;
return canUpgradeToVerifiedEnrollment;
}
4 changes: 2 additions & 2 deletions src/components/course/data/utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { PROGRAM_TYPE_MAP } from '../../program/data/constants';
import { programIsMicroMasters, programIsProfessionalCertificate } from '../../program/data/utils';
import { hasValidStartExpirationDates } from '../../../utils/common';
import { LICENSE_STATUS } from '../../enterprise-user-subsidy/data/constants';
import { COURSE_MODES_MAP, findHighestLevelEntitlementSku, findHighestLevelSkuByEntityModeType } from '../../app/data';
import { findHighestLevelEntitlementSku, findHighestLevelSkuByEntityModeType, isEnrollmentUpgradeable } from '../../app/data';

export function hasCourseStarted(start) {
const today = new Date();
Expand Down Expand Up @@ -320,7 +320,7 @@ export function shouldUpgradeUserEnrollment({
subscriptionLicense,
enrollmentUrl,
}) {
const isAuditEnrollment = userEnrollment?.mode === COURSE_MODES_MAP.AUDIT;
const isAuditEnrollment = isEnrollmentUpgradeable(userEnrollment);
return !!(isAuditEnrollment && isActiveSubscriptionLicense(subscriptionLicense) && enrollmentUrl);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const CourseEnrollments = ({ children }) => {
handleAcknowledgeExpiringAssignments,
handleAcknowledgeAssignments,
isAcknowledgingAssignments,
} = useContentAssignments(allEnrollmentsByStatus.assigned);
} = useContentAssignments();

const isFirstVisit = useIsFirstDashboardVisit();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

import { COURSE_STATUSES } from '../../../../constants';
import { COURSE_SECTION_TITLES } from '../../data/constants';
import { COURSE_MODES_MAP, useEnterpriseCustomer } from '../../../app/data';
import { COURSE_MODES_MAP, isEnrollmentUpgradeable, useEnterpriseCustomer } from '../../../app/data';
import DelayedFallbackContainer from '../../../DelayedFallback/DelayedFallbackContainer';

const CARD_COMPONENT_BY_COURSE_STATUS = {
Expand Down Expand Up @@ -103,8 +103,8 @@ const CourseSection = ({

const renderCourseCards = () => courseRuns.map(courseRun => {
const Component = CARD_COMPONENT_BY_COURSE_STATUS[courseRun.courseRunStatus];
const isAuditOrHonorEnrollment = [COURSE_MODES_MAP.AUDIT, COURSE_MODES_MAP.HONOR].includes(courseRun.mode);
if (isAuditOrHonorEnrollment && courseRun.courseRunStatus === COURSE_STATUSES.inProgress) {
const isAuditEnrollment = isEnrollmentUpgradeable(courseRun);
if (isAuditEnrollment && courseRun.courseRunStatus === COURSE_STATUSES.inProgress) {
return (
<Suspense
key={courseRun.courseRunId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const InProgressCourseCard = ({
notifications,
courseRunStatus,
startDate,
enrollBy,
resumeCourseRunUrl,
mode,
...rest
Expand All @@ -76,7 +77,11 @@ export const InProgressCourseCard = ({
subsidyForCourse,
hasUpgradeAndConfirm,
courseRunPrice,
} = useCourseUpgradeData({ courseRunKey: courseRunId, mode });
} = useCourseUpgradeData({
courseRunKey: courseRunId,
enrollBy,
mode,
});
const [isMarkCompleteModalOpen, setIsMarkCompleteModalOpen] = useState(false);
const { courseCards } = useContext(AppContext);
const { data: enterpriseCustomer } = useEnterpriseCustomer();
Expand All @@ -96,6 +101,7 @@ export const InProgressCourseCard = ({
title={title}
courseRunKey={courseRunId}
mode={mode}
enrollBy={enrollBy}
/>
)}
<ContinueLearningButton
Expand Down Expand Up @@ -234,6 +240,7 @@ export const InProgressCourseCard = ({
courseRunId={courseRunId}
mode={mode}
startDate={startDate}
enrollBy={enrollBy}
courseUpgradePrice={renderCourseUpgradePrice()}
{...rest}
>
Expand Down Expand Up @@ -261,12 +268,14 @@ InProgressCourseCard.propTypes = {
title: PropTypes.string.isRequired,
courseRunStatus: PropTypes.string.isRequired,
startDate: PropTypes.string,
enrollBy: PropTypes.string,
mode: PropTypes.string,
resumeCourseRunUrl: PropTypes.string,
};

InProgressCourseCard.defaultProps = {
startDate: null,
enrollBy: null,
mode: null,
resumeCourseRunUrl: null,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const UpgradeCourseButton = ({
variant,
courseRunKey,
mode,
enrollBy,
}) => {
const intl = useIntl();
const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down Expand Up @@ -104,6 +105,7 @@ const UpgradeCourseButton = ({
} = useCourseUpgradeData({
courseRunKey,
mode,
enrollBy,
onRedeem: handleRedeem,
onRedeemSuccess: handleRedemptionSuccess,
onRedeemError: handleRedemptionError,
Expand Down Expand Up @@ -169,11 +171,13 @@ UpgradeCourseButton.propTypes = {
title: PropTypes.string.isRequired,
courseRunKey: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired,
enrollBy: PropTypes.string,
};

UpgradeCourseButton.defaultProps = {
className: undefined,
variant: 'outline-primary',
enrollBy: undefined,
};

export default UpgradeCourseButton;
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { getExpiringAssignmentsAcknowledgementState, getHasUnacknowledgedAssignm
import { ASSIGNMENT_TYPES } from '../../../../enterprise-user-subsidy/enterprise-offers/data/constants';
import {
COUPON_CODE_SUBSIDY_TYPE,
COURSE_MODES_MAP,
getSubsidyToApplyForCourse,
groupCourseEnrollmentsByStatus,
isEnrollmentUpgradeable,
LEARNER_CREDIT_SUBSIDY_TYPE,
LICENSE_SUBSIDY_TYPE,
queryEnterpriseCourseEnrollments,
Expand Down Expand Up @@ -140,13 +140,13 @@ export const useCourseEnrollments = ({
export const useCourseUpgradeData = ({
courseRunKey,
mode,
enrollBy,
onRedeem,
onRedeemSuccess,
onRedeemError,
}) => {
const location = useLocation();
// Determine whether the course mode is such that it can be upgraded
const canUpgradeToVerifiedEnrollment = [COURSE_MODES_MAP.AUDIT, COURSE_MODES_MAP.HONOR].includes(mode);
const canUpgradeToVerifiedEnrollment = isEnrollmentUpgradeable({ mode, enrollBy });
const { authenticatedUser } = useContext(AppContext);
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { data: customerContainsContent } = useEnterpriseCustomerContainsContent([courseRunKey], {
Expand Down Expand Up @@ -434,22 +434,40 @@ export function useContentAssignments() {
expiredAssignments,
} = allEnrollmentsByStatus.assigned;

const upgradeableAuditEnrollmentCourseKeys = [...allEnrollmentsByStatus.inProgress]
.filter(enrollment => isEnrollmentUpgradeable(enrollment))
.map((enrollment) => enrollment.courseKey);

// Filter out any assignments that have a corresponding potentially upgradeable
// audit enrollment. Note: all enrollment cards currently assume content key is
// a course run id despite the current assignment's content key referring to a
// top-level course key.
const filteredAssignmentsForDisplay = assignmentsForDisplay.filter((assignment) => (
!upgradeableAuditEnrollmentCourseKeys.includes(assignment.courseRunId)
));

// Sort and transform the list of assignments for display.
const sortedAssignmentsForDisplay = sortAssignmentsByAssignmentStatus(assignmentsForDisplay);
const sortedAssignmentsForDisplay = sortAssignmentsByAssignmentStatus(filteredAssignmentsForDisplay);
setAssignments(sortedAssignmentsForDisplay);

// Determine whether there are expiring assignments. If so, display alert.
const { hasUnacknowledgedExpiringAssignments } = getExpiringAssignmentsAcknowledgementState(assignmentsForDisplay);
setShowExpiringAssignmentsAlert(hasUnacknowledgedExpiringAssignments);

// Determine whether there are unacknowledged canceled assignments. If so, display alert.
const hasUnacknowledgedCanceledAssignments = getHasUnacknowledgedAssignments(canceledAssignments);
const filteredCanceledAssignments = canceledAssignments.filter((assignment) => (
!upgradeableAuditEnrollmentCourseKeys.includes(assignment.courseRunId)
));
const hasUnacknowledgedCanceledAssignments = getHasUnacknowledgedAssignments(filteredCanceledAssignments);
setShowCanceledAssignmentsAlert(hasUnacknowledgedCanceledAssignments);

// Determine whether there are unacknowledged expired assignments. If so, display alert.
const hasUnacknowledgedExpiredAssignments = getHasUnacknowledgedAssignments(expiredAssignments);
const filteredExpiredAssignments = expiredAssignments.filter((assignment) => (
!upgradeableAuditEnrollmentCourseKeys.includes(assignment.courseRunId)
));
const hasUnacknowledgedExpiredAssignments = getHasUnacknowledgedAssignments(filteredExpiredAssignments);
setShowExpiredAssignmentsAlert(hasUnacknowledgedExpiredAssignments);
}, [allEnrollmentsByStatus.assigned, enterpriseCustomer.slug]);
}, [allEnrollmentsByStatus.assigned, allEnrollmentsByStatus.inProgress, enterpriseCustomer.slug]);

return {
assignments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,8 @@ describe('useContentAssignments', () => {
},
};

function mockUseEnterpriseCourseEnrollments(policies) {
function mockUseEnterpriseCourseEnrollments(policies, options = {}) {
const otherEnrollmentsByStatus = options.otherEnrollmentsByStatus || {};
useEnterpriseCourseEnrollments.mockReturnValue({
data: {
allEnrollmentsByStatus: {
Expand All @@ -641,6 +642,8 @@ describe('useContentAssignments', () => {
mockEnterpriseCustomer.slug,
)),
},
inProgress: [],
...otherEnrollmentsByStatus,
},
},
});
Expand Down Expand Up @@ -854,6 +857,79 @@ describe('useContentAssignments', () => {
expect(acknowledgedExpiringAssignments).toEqual([mockExpiringAssignment.uuid]);
expect(result.current.showExpiringAssignmentsAlert).toBe(false);
});

it.each([
// Audit, no enrollBy date
{
mode: COURSE_MODES_MAP.AUDIT,
enrollBy: undefined,
isAssignmentExcluded: true,
},
// Audit, with elapsed enrollBy date
{
mode: COURSE_MODES_MAP.AUDIT,
enrollBy: dayjs().subtract(1, 'd').toISOString(), // yesterday
isAssignmentExcluded: false,
},
// Audit, with not-yet-elapsed enrollBy date
{
mode: COURSE_MODES_MAP.AUDIT,
enrollBy: dayjs().add(1, 'd').toISOString(), // tomorrow
isAssignmentExcluded: true,
},
// Verified, no enrollBy date
{
mode: COURSE_MODES_MAP.VERIFIED,
enrollBy: undefined,
isAssignmentExcluded: false,
},
// Verified, with elapsed enrollBy date
{
mode: COURSE_MODES_MAP.VERIFIED,
enrollBy: dayjs().subtract(1, 'd').toISOString(), // yesterday
isAssignmentExcluded: false,
},
// Verified, with not-yet-elapsed enrollBy date
{
mode: COURSE_MODES_MAP.VERIFIED,
enrollBy: dayjs().add(1, 'd').toISOString(), // tomorrow
isAssignmentExcluded: false,
},
])('should exclude assignments that have an upgradeable in-progress course enrollment (%s)', ({
mode,
enrollBy,
isAssignmentExcluded,
}) => {
const mockEnrollment = {
...mockTransformedMockCourseEnrollment,
mode,
enrollBy,
};
const mockAssignmentForExistingEnrollment = {
...mockAllocatedAssignment,
contentKey: mockEnrollment.courseRunKey,
};
const mockPoliciesWithInProgressEnrollment = {
...mockPoliciesWithAssignments,
learnerContentAssignments: {
...mockPoliciesWithAssignments.learnerContentAssignments,
assignmentsForDisplay: [
mockAssignmentForExistingEnrollment,
],
},
};
mockUseEnterpriseCourseEnrollments(mockPoliciesWithInProgressEnrollment, {
otherEnrollmentsByStatus: {
inProgress: [mockEnrollment],
},
});
const { result } = renderHook(() => useContentAssignments(), { wrapper });
if (isAssignmentExcluded) {
expect(result.current.assignments).toHaveLength(0);
} else {
expect(result.current.assignments).toHaveLength(1);
}
});
});

describe('useCourseEnrollmentsBySection', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dayjs from 'dayjs';

/**
* TODO
* Sorts list of enrollments by the enrollment date (i.e., when the enrollment record was created).
* @param {*} enrollments
* @returns
*/
Expand Down

0 comments on commit 70d9015

Please sign in to comment.