Skip to content

Commit

Permalink
fix: late enrollment should be less sensitive to courserun state (#1130)
Browse files Browse the repository at this point in the history
ENT-9259
  • Loading branch information
pwnage101 authored Jul 24, 2024
1 parent 2e013ab commit e354666
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 20 deletions.
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

0 comments on commit e354666

Please sign in to comment.