diff --git a/__mocks__/react-instantsearch-dom.jsx b/__mocks__/react-instantsearch-dom.jsx index 4104e5d318..f557854755 100644 --- a/__mocks__/react-instantsearch-dom.jsx +++ b/__mocks__/react-instantsearch-dom.jsx @@ -31,6 +31,7 @@ const fakeHits = [ start: mockCurrentStartDate, end: dayjs(mockCurrentStartDate).add(1, 'year').toISOString(), enroll_by: dayjs(mockCurrentStartDate).unix(), + has_enroll_by: true, is_active: true, max_effort: 5, min_effort: 1, @@ -47,6 +48,7 @@ const fakeHits = [ start: mockCurrentStartDate, end: dayjs(mockCurrentStartDate).add(1, 'year').toISOString(), enroll_by: dayjs(mockCurrentStartDate).unix(), + has_enroll_by: true, is_active: true, max_effort: 5, min_effort: 1, @@ -60,6 +62,7 @@ const fakeHits = [ start: '2020-09-09T04:00:00Z', end: dayjs('2020-09-09T04:00:00Z').add(1, 'year').toISOString(), enroll_by: dayjs('2020-09-09T04:00:00Z').unix(), + has_enroll_by: true, is_active: true, max_effort: 5, min_effort: 1, @@ -82,6 +85,7 @@ const fakeHits = [ start: '2022-10-09T04:00:00Z', end: dayjs('2022-10-09T04:00:00Z').add(1, 'year').toISOString(), enroll_by: dayjs('2022-10-09T04:00:00Z').unix(), + has_enroll_by: true, is_active: true, max_effort: 5, min_effort: 1, @@ -98,6 +102,7 @@ const fakeHits = [ start: '2022-10-09T04:00:00Z', end: dayjs('2022-10-09T04:00:00Z').add(1, 'year').toISOString(), enroll_by: dayjs('2022-10-09T04:00:00Z').unix(), + has_enroll_by: true, is_active: true, max_effort: 5, min_effort: 1, @@ -111,6 +116,7 @@ const fakeHits = [ start: '2020-09-09T04:00:00Z', end: dayjs('2020-09-09T04:00:00Z').add(1, 'year').toISOString(), enroll_by: dayjs('2020-09-09T04:00:00Z').unix(), + has_enroll_by: true, is_active: true, max_effort: 5, min_effort: 1, diff --git a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx index c17fe90e41..e5abef6d00 100644 --- a/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx +++ b/src/components/learner-credit-management/cards/data/useCourseCardMetadata.jsx @@ -62,7 +62,6 @@ const useCourseCardMetadata = ({ // Extracts the content price from assignable course runs const formattedPrice = getContentPriceDisplay({ courseRuns: assignableCourseRuns }); - const imageSrc = cardImageUrl || cardFallbackImg; let logoSrc; diff --git a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx index ead0bcebc6..1bc68e8534 100644 --- a/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx +++ b/src/components/learner-credit-management/cards/tests/CourseCard.test.jsx @@ -76,6 +76,7 @@ const originalData = { upgrade_deadline: 1892678399, pacing_type: 'self_paced', enroll_by: enrollByTimestamp, + has_enroll_by: true, is_active: true, weeks_to_complete: 60, end: dayjs().add(1, 'years').toISOString(), @@ -88,6 +89,7 @@ const originalData = { upgrade_deadline: 1892678399, pacing_type: 'self_paced', enroll_by: enrollByTimestamp, + has_enroll_by: true, is_active: true, weeks_to_complete: 60, end: dayjs().add(1, 'year').toISOString(), @@ -119,7 +121,8 @@ const execEdData = { start: futureStartDate, upgrade_deadline: 1892678399, pacing_type: 'instructor_paced', - enroll_by: 1892678399, + enroll_by: enrollByTimestamp, + has_enroll_by: true, is_active: true, weeks_to_complete: 60, end: dayjs().add(1, 'year').toISOString(), @@ -131,7 +134,8 @@ const execEdData = { start: futureStartDate, upgrade_deadline: 1892678399, pacing_type: 'instructor_paced', - enroll_by: 1892678399, + enroll_by: enrollByTimestamp, + has_enroll_by: true, is_active: true, weeks_to_complete: 60, end: dayjs().add(1, 'year').toISOString(), @@ -287,7 +291,7 @@ describe('Course card works as expected', () => { }); test('executive education card renders', () => { - const enrollByDate = getNormalizedEnrollByDate({ enrollBy: dayjs('Dec 22, 2029') }); + const enrollByDate = getNormalizedEnrollByDate({ enrollBy: dayjs.unix(enrollByTimestamp).toISOString() }); const formattedEnrollBy = dayjs(enrollByDate).format(SHORT_MONTH_DATE_FORMAT); renderWithRouter(); expect(screen.queryByText('$999')).toBeInTheDocument(); diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js index ff4b0b4654..cfa73244d3 100644 --- a/src/components/learner-credit-management/data/constants.js +++ b/src/components/learner-credit-management/data/constants.js @@ -111,6 +111,9 @@ export const START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS = 14; // Default empty content_price value export const EMPTY_CONTENT_PRICE_VALUE = 0; +// +export const LATE_ENROLLMENTS_BUFFER_DAYS = 30; + // Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`. // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const learnerCreditManagementQueryKeys = { diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 41cb622987..6d09b8165b 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -7,6 +7,7 @@ import { ASSIGNMENT_ENROLLMENT_DEADLINE, COURSE_PACING_MAP, DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, + LATE_ENROLLMENTS_BUFFER_DAYS, LOW_REMAINING_BALANCE_PERCENT_THRESHOLD, MAX_ALLOWABLE_REFUND_THRESHOLD_DAYS, NO_BALANCE_REMAINING_DOLLAR_THRESHOLD, @@ -565,9 +566,16 @@ export const isLmsBudget = ( export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); /** - * Returns assignable course runs within the threshold of within the subsidies expiration date - * offset by the DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION constant. It sorts it from the soonest expiring - * enroll-by date and the enroll-by date and upgrade deadline has been normalized to ISO format. + * Filters assignable course runs based on the following criteria: + * - If hasEnrollBy, we return the soonest of two dates: The subsidy expiration date - refund threshold OR today + * offset by the 90-day allocation threshold for an assignment denoted as isEligibleForEnrollment + * - If isLateRedemptionEnabled, we consider only the isLateEnrollmentEligible field returned by Algolia for + * each run. + * + * Based on the above criteria, if isLateRedemptionAllowed is false, filter on if the course run isActive AND + * isEligibleForEnrollment + * + * Furthermore, we return assignable course runs sorted by the enrollBy date (soonest to latest) * * @param courseRuns * @param subsidyExpirationDatetime @@ -577,20 +585,27 @@ export const hasCourseStarted = (start) => dayjs(start).isBefore(dayjs()); export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, isLateRedemptionAllowed }) => { const clonedCourseRuns = courseRuns.map(courseRun => ({ ...courseRun, - enrollBy: dayjs.unix(courseRun.enrollBy).toISOString(), + enrollBy: courseRun.hasEnrollBy ? dayjs.unix(courseRun.enrollBy).toISOString() : null, upgradeDeadline: dayjs.unix(courseRun.upgradeDeadline).toISOString(), })); - const assignableCourseRunsFilter = ({ enrollBy, isActive }) => { - const enrollByDateThreshold = dayjs(enrollBy).isBefore( - Math.max( - dayjs(subsidyExpirationDatetime).subtract(MAX_ALLOWABLE_REFUND_THRESHOLD_DAYS, 'days').toDate(), - dayjs().add(DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, 'days').toDate(), - ), - ); - if (isLateRedemptionAllowed) { - return enrollByDateThreshold; + const assignableCourseRunsFilter = ({ + enrollBy, isActive, hasEnrollBy = false, isLateEnrollmentEligible = false, + }) => { + let isEligibleForEnrollment = true; + if (hasEnrollBy) { + isEligibleForEnrollment = dayjs(enrollBy).isBefore( + Math.min( + dayjs(subsidyExpirationDatetime).subtract(MAX_ALLOWABLE_REFUND_THRESHOLD_DAYS, 'days').toDate(), + dayjs().add(DAYS_UNTIL_ASSIGNMENT_ALLOCATION_EXPIRATION, 'days').toDate(), + ), + ); + } + if (hasCourseStarted(enrollBy) && isLateRedemptionAllowed) { + const lateEnrollmentCutoff = dayjs().subtract(LATE_ENROLLMENTS_BUFFER_DAYS, 'days'); + isEligibleForEnrollment = dayjs(enrollBy).isAfter(lateEnrollmentCutoff); + return isLateEnrollmentEligible && isEligibleForEnrollment; } - return isActive && enrollByDateThreshold; + return isActive && isEligibleForEnrollment; }; const assignableCourseRuns = clonedCourseRuns.filter(assignableCourseRunsFilter); const sortedAssignableCourseRuns = assignableCourseRuns.sort((a, b) => a.enrollBy - b.enrollBy);