Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: late enrollment should be less sensitive to courserun state #1130

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions src/components/app/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { logError } from '@edx/frontend-platform/logging';

import { ASSIGNMENT_TYPES, POLICY_TYPES } from '../../enterprise-user-subsidy/enterprise-offers/data/constants';
import { LICENSE_STATUS } from '../../enterprise-user-subsidy/data/constants';
import { getBrandColorsFromCSSVariables, isTodayWithinDateThreshold } from '../../../utils/common';
import { getBrandColorsFromCSSVariables, isDefinedAndNotNull, isTodayWithinDateThreshold } from '../../../utils/common';
import { COURSE_STATUSES, SUBSIDY_TYPE } from '../../../constants';
import { LATE_ENROLLMENTS_BUFFER_DAYS } from '../../../config/constants';
import {
Expand Down Expand Up @@ -461,37 +461,56 @@ export function isArchived(courseRun) {
}

/**
* Returns list of available that are marketable, enrollable, and not archived.
* Returns list of available runs that are marketable, enrollable, and not archived.
*
* @param {object} course
* This function is used by logic that determines which runs should be visible on the course about page.
*
* @param {object} course - The course containing runs which will be a superset of the returned runs.
* @param {number} isEnrollableBufferDays - number of days to buffer the enrollment end date, or undefined.
* @returns List of course runs.
*/
export function getAvailableCourseRuns({ course, isEnrollableBufferDays }) {
if (!course?.courseRuns) {
return [];
}
const availableCourseRunsFilter = (courseRun) => {
if (!courseRun.isMarketable || isArchived(courseRun)) {
return false;
}

if (isEnrollableBufferDays === undefined) {
return courseRun.isEnrollable;
}
// These are the standard rules used for determining whether a run is "available".
const standardAvailableCourseRunsFilter = (courseRun) => (
courseRun.isMarketable && !isArchived(courseRun) && courseRun.isEnrollable
);

const today = dayjs();
if (courseRun.enrollmentStart && today.isBefore(dayjs(courseRun.enrollmentStart))) {
// In cases where we don't expect the buffer to change behavior, fallback to the backend-provided value.
return courseRun.isEnrollable;
// These are more relaxed availability rules when late enrollment is applicable. We still never show archived courses,
// but the rules around the following fields are relaxed:
//
// * courseRun.isEnrollable: This field represents the enrollment window actually stored in the database. However,
// during late enrollment we expand the end date of the enrollment window by isEnrollableBufferDays.
// * courseRun.isMarketable: This field is True when the run is published, has seats, and has a marketing URL. Since
// late enrollment potentially means enrolling into an unpublished run, we must ignore the run state.
const lateEnrollmentAvailableCourseRunsFilter = (courseRun) => {
if (
isArchived(courseRun)
// The next two checks are in lieu of isMarketable which is otherwise overly sensitive to courserun state.
|| !courseRun.seats?.length
|| !courseRun.marketingUrl
) {
return false;
}
if (!courseRun.enrollmentEnd) {
// Finally, check against an expanded enrollment window.
const today = dayjs();
if (!courseRun.enrollmentEnd || (courseRun.enrollmentStart && today.isBefore(dayjs(courseRun.enrollmentStart)))) {
// In cases where we don't expect the buffer to change behavior, fallback to the backend-provided value.
return courseRun.isEnrollable;
return standardAvailableCourseRunsFilter(courseRun);
}
const bufferedEnrollDeadline = dayjs(courseRun.enrollmentEnd).add(isEnrollableBufferDays, 'day');
return today.isBefore(bufferedEnrollDeadline);
};
return course.courseRuns.filter(availableCourseRunsFilter);

// isEnrollableBufferDays is used as a heuristic to determine if the late enrollment feature is enabled.
return course.courseRuns.filter(
isDefinedAndNotNull(isEnrollableBufferDays)
? lateEnrollmentAvailableCourseRunsFilter
: standardAvailableCourseRunsFilter,
);
}

export function getCatalogsForSubsidyRequests({
Expand Down
51 changes: 48 additions & 3 deletions src/components/app/data/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@ describe('getAvailableCourseRuns', () => {
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.STARTING_SOON,
isMarketable: true,
seats: [{ sku: '835BEA7' }],
marketingUrl: 'https://foo.bar/',
isEnrollable: true,
enrollmentStart: '2023-07-01T00:00:00Z',
enrollmentEnd: '2023-08-01T00:00:00Z',
Expand All @@ -543,30 +545,73 @@ describe('getAvailableCourseRuns', () => {
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.STARTING_SOON,
isMarketable: true,
seats: [{ sku: '835BEA7' }],
marketingUrl: 'https://foo.bar/',
isEnrollable: false,
enrollmentStart: '2023-06-01T00:00:00Z',
enrollmentEnd: '2023-07-01T00:00:00Z',
},
// Run with long-ago closed enrollment, but somehow still "Starting Soon". This is very edge-casey.
// Run with recently closed enrollment, but is not marketable because the course became unpublished. This should
// still be redeemable under late enrollment.
{
key: 'course-v1:edX+DemoX+Demo_Course3',
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.STARTING_SOON,
isMarketable: false,
seats: [{ sku: '835BEA7' }],
marketingUrl: 'https://foo.bar/',
isEnrollable: false,
enrollmentStart: '2023-06-01T00:00:00Z',
enrollmentEnd: '2023-07-01T00:00:00Z',
},
// Run with recently closed enrollment, but is not really not marketable.
{
key: 'course-v1:edX+DemoX+Demo_Course4',
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.STARTING_SOON,
isMarketable: false,
seats: [],
marketingUrl: undefined,
isEnrollable: false,
enrollmentStart: '2023-06-01T00:00:00Z',
enrollmentEnd: '2023-07-01T00:00:00Z',
},
// Run with long-ago closed enrollment, but somehow still "Starting Soon". This is very edge-casey.
{
key: 'course-v1:edX+DemoX+Demo_Course5',
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.STARTING_SOON,
isMarketable: true,
seats: [{ sku: '835BEA7' }],
marketingUrl: 'https://foo.bar/',
isEnrollable: false,
enrollmentStart: '2023-01-01T00:00:00Z',
enrollmentEnd: '2023-02-01T00:00:00Z',
},
// Run with long-ago closed enrollment, and now running.
{
key: 'course-v1:edX+DemoX+Demo_Course4',
key: 'course-v1:edX+DemoX+Demo_Course6',
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.CURRENT,
isMarketable: true,
seats: [{ sku: '835BEA7' }],
marketingUrl: 'https://foo.bar/',
isEnrollable: false,
enrollmentStart: '2023-01-01T00:00:00Z',
enrollmentEnd: '2023-02-01T00:00:00Z',
},
// Run with the enrollment window still in the future.
{
key: 'course-v1:edX+DemoX+Demo_Course7',
title: 'Demo Course',
availability: COURSE_AVAILABILITY_MAP.STARTING_SOON,
isMarketable: true,
seats: [{ sku: '835BEA7' }],
marketingUrl: 'https://foo.bar/',
isEnrollable: false, // enrollment hasn't officially opened yet.
enrollmentStart: '2023-07-10T00:00:00Z', // enrollment hasn't officially opened yet.
enrollmentEnd: '2023-08-01T00:00:00Z',
},
],
},
};
Expand All @@ -575,7 +620,7 @@ describe('getAvailableCourseRuns', () => {
expect(getAvailableCourseRuns({ course: sampleCourseRunDataWithRecentRuns.courseData }))
.toEqual(sampleCourseRunDataWithRecentRuns.courseData.courseRuns.slice(0, 1));
expect(getAvailableCourseRuns({ course: sampleCourseRunDataWithRecentRuns.courseData, isEnrollableBufferDays: 60 }))
.toEqual(sampleCourseRunDataWithRecentRuns.courseData.courseRuns.slice(0, 2));
.toEqual(sampleCourseRunDataWithRecentRuns.courseData.courseRuns.slice(0, 3));
});
it('returns empty array if course runs are not available', () => {
sampleCourseRunData.courseData.courseRuns = [];
Expand Down
Loading