Skip to content

Commit

Permalink
feat: fetch restricted runs and display if unrestricted for redeemabl…
Browse files Browse the repository at this point in the history
…e catalog

ENT-9360
  • Loading branch information
pwnage101 committed Aug 16, 2024
1 parent f0d0c7e commit 0c2eb59
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 10 deletions.
28 changes: 22 additions & 6 deletions src/components/app/data/hooks/useCourseRedemptionEligibility.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,48 @@
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';

import { isRunUnrestricted } from '../utils';
import useCourseMetadata from './useCourseMetadata';
import { queryCanRedeem } from '../queries';
import useEnterpriseCustomer from './useEnterpriseCustomer';
import useLateEnrollmentBufferDays from './useLateEnrollmentBufferDays';
import useEnterpriseCustomerContainsContent from './useEnterpriseCustomerContainsContent';

export function transformCourseRedemptionEligibility({
courseMetadata,
canRedeemData,
courseRunKey,
restrictedRunsAllowed,
}) {
const redeemabilityForActiveCourseRun = canRedeemData.find(r => r.contentKey === courseMetadata.activeCourseRun?.key);
// Begin by excluding restricted runs that should not be visible to the requester.
// This filtering does not control visibility of individual course runs, but
// it does serve as input to the determination of redemption eligiblity.
const unrestrictedCanRedeemData = canRedeemData.filter(r => isRunUnrestricted({
restrictedRunsAllowed,
courseMetadata,
courseRunKey: r.contentKey,
catalogUuid: r.redeemableSubsidyAccessPolicy?.catalogUuid,
}));
const redeemabilityForActiveCourseRun = unrestrictedCanRedeemData.find(
r => r.contentKey === courseMetadata.activeCourseRun?.key,
);
const missingSubsidyAccessPolicyReason = redeemabilityForActiveCourseRun?.reasons[0];
const preferredSubsidyAccessPolicy = redeemabilityForActiveCourseRun?.redeemableSubsidyAccessPolicy;
const otherSubsidyAccessPolicy = canRedeemData.find(
const anyRedeemableSubsidyAccessPolicy = unrestrictedCanRedeemData.find(
r => r.redeemableSubsidyAccessPolicy,
)?.redeemableSubsidyAccessPolicy;
const listPrice = redeemabilityForActiveCourseRun?.listPrice?.usd;
const hasSuccessfulRedemption = courseRunKey
? !!canRedeemData.find(r => r.contentKey === courseRunKey)?.hasSuccessfulRedemption
: canRedeemData.some(r => r.hasSuccessfulRedemption);
? !!unrestrictedCanRedeemData.find(r => r.contentKey === courseRunKey)?.hasSuccessfulRedemption
: unrestrictedCanRedeemData.some(r => r.hasSuccessfulRedemption);

// If there is a redeemable subsidy access policy for the active course run, use that. Otherwise, use any other
// redeemable subsidy access policy for any of the content keys.
const redeemableSubsidyAccessPolicy = preferredSubsidyAccessPolicy || otherSubsidyAccessPolicy;
const redeemableSubsidyAccessPolicy = preferredSubsidyAccessPolicy || anyRedeemableSubsidyAccessPolicy;
const isPolicyRedemptionEnabled = hasSuccessfulRedemption || !!redeemableSubsidyAccessPolicy;
return {
isPolicyRedemptionEnabled,
redeemabilityPerContentKey: canRedeemData,
redeemabilityPerContentKey: unrestrictedCanRedeemData,
redeemableSubsidyAccessPolicy,
missingSubsidyAccessPolicyReason,
hasSuccessfulRedemption,
Expand All @@ -45,6 +59,7 @@ export default function useCourseRedemptionEligibility(queryOptions = {}) {
const { select, ...queryOptionsRest } = queryOptions;
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { data: courseMetadata } = useCourseMetadata();
const { data: { restrictedRunsAllowed } } = useEnterpriseCustomerContainsContent([courseMetadata.key]);
const lateEnrollmentBufferDays = useLateEnrollmentBufferDays();

return useQuery({
Expand All @@ -55,6 +70,7 @@ export default function useCourseRedemptionEligibility(queryOptions = {}) {
courseMetadata,
canRedeemData: data,
courseRunKey,
restrictedRunsAllowed,
});
if (select) {
return select({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import useEnterpriseCustomer from './useEnterpriseCustomer';
import { queryClient } from '../../../../utils/tests';
import { fetchCanRedeem } from '../services';
import useCourseMetadata from './useCourseMetadata';
import { useCourseRedemptionEligibility, useLateEnrollmentBufferDays } from './index';
import {
useCourseRedemptionEligibility,
useLateEnrollmentBufferDays,
useEnterpriseCustomerContainsContent,
} from './index';
import { transformCourseRedemptionEligibility } from './useCourseRedemptionEligibility';

jest.mock('./useEnterpriseCustomer');
jest.mock('./useCourseMetadata');
jest.mock('./useLateEnrollmentBufferDays');
jest.mock('./useEnterpriseCustomerContainsContent');
jest.mock('../services', () => ({
...jest.requireActual('../services'),
fetchCanRedeem: jest.fn().mockResolvedValue(null),
Expand Down Expand Up @@ -87,6 +92,7 @@ describe('useCourseRedemptionEligibility', () => {
useParams.mockReturnValue({ courseRunKey: mockCourseRunKey });
useCourseMetadata.mockReturnValue({ data: mockCourseMetadata });
useLateEnrollmentBufferDays.mockReturnValue(undefined);
useEnterpriseCustomerContainsContent.mockReturnValue({ data: {} });
});
it('should handle resolved value correctly', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCourseRedemptionEligibility(), { wrapper: Wrapper });
Expand Down
10 changes: 9 additions & 1 deletion src/components/app/data/services/course.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { findHighestLevelEntitlementSku, getActiveCourseRun } from '../utils';
export async function fetchCourseMetadata(courseKey, courseRunKey) {
const contentMetadataUrl = `${getConfig().DISCOVERY_API_BASE_URL}/api/v1/courses/${courseKey}/`;
const queryParams = new URLSearchParams();
// Always include restricted/custom-b2b-enterprise runs in case one has been requested.
queryParams.append('include_restricted', 'custom-b2b-enterprise');
const url = `${contentMetadataUrl}?${queryParams.toString()}`;
try {
const response = await getAuthenticatedHttpClient().get(url);
Expand All @@ -28,6 +30,8 @@ export async function fetchCourseMetadata(courseKey, courseRunKey) {
transformedData.activeCourseRun = getActiveCourseRun(transformedData);
transformedData.courseEntitlementProductSku = findHighestLevelEntitlementSku(transformedData.entitlements);

// If a specific courseRunKey is requested, and that courseRunKey belongs
// to the specified course, narrow the returned runs to just the one run.
const courseRunKeys = transformedData.courseRuns.map(({ key }) => key);
if (courseRunKey && courseRunKeys.includes(courseRunKey)) {
transformedData.canonicalCourseRunKey = courseRunKey;
Expand All @@ -47,8 +51,12 @@ export async function fetchCourseMetadata(courseKey, courseRunKey) {

export async function fetchCourseRunMetadata(courseRunKey) {
const courseRunMetadataUrl = `${getConfig().DISCOVERY_API_BASE_URL}/api/v1/course_runs/${courseRunKey}/`;
const queryParams = new URLSearchParams();
// Always include restricted/custom-b2b-enterprise runs in case one has been requested.
queryParams.append('include_restricted', 'custom-b2b-enterprise');
const url = `${courseRunMetadataUrl}?${queryParams.toString()}`;
try {
const response = await getAuthenticatedHttpClient().get(courseRunMetadataUrl);
const response = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(response.data);
} catch (error) {
if (getErrorResponseStatusCode(error) !== 404) {
Expand Down
18 changes: 18 additions & 0 deletions src/components/app/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,3 +832,21 @@ export function transformCourseMetadataByAllocatedCourseRunAssignments({
}
return courseMetadata;
}

/*
* A centralized helper to tell us if a given run is unrestricted for the given catalog.
*/
export function isRunUnrestricted({
restrictedRunsAllowed,
courseMetadata,
courseRunKey,
catalogUuid,
}) {
const courseRunMetadata = courseMetadata.availableCourseRuns.find(r => r.contentKey === courseRunKey);
if (courseRunMetadata?.restrictionType === 'custom-b2b-enterprise') {
// If the run is restricted for enterprise, make sure the catalog of interest explicitly allows it.
return restrictedRunsAllowed[courseMetadata.key][courseRunKey].includes(catalogUuid);
}
// Otherwise, only allow completely unrestricted runs.
return !courseRunMetadata?.restrictionType;
}
17 changes: 15 additions & 2 deletions src/components/course/course-header/CourseRunCards.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useEnterpriseCourseEnrollments,
useEnterpriseCustomerContainsContent,
useUserEntitlements,
isRunUnrestricted,
} from '../../app/data';

/**
Expand All @@ -21,18 +22,30 @@ const CourseRunCards = () => {
const {
userSubsidyApplicableToCourse,
missingUserSubsidyReason,
applicableCatalogUuid,
} = useUserSubsidyApplicableToCourse();
const {
data: {
catalogList,
restrictedRunsAllowed,
},
} = useEnterpriseCustomerContainsContent([courseKey]);
const { data: courseMetadata } = useCourseMetadata();
const { data: { catalogList } } = useEnterpriseCustomerContainsContent([courseKey]);
const { data: { enterpriseCourseEnrollments } } = useEnterpriseCourseEnrollments();
const { data: userEntitlements } = useUserEntitlements();
const availableCourseRuns = courseMetadata.availableCourseRuns.filter(r => isRunUnrestricted({
restrictedRunsAllowed,
courseMetadata,
courseRunKey: r.key,
applicableCatalogUuid,
}));

return (
<CardGrid
columnSizes={{ xs: 12, md: 6, lg: 5 }}
hasEqualColumnHeights={false}
>
{courseMetadata.availableCourseRuns.map((courseRun) => {
{availableCourseRuns.map((courseRun) => {
const hasRedeemablePolicy = userSubsidyApplicableToCourse?.subsidyType === LEARNER_CREDIT_SUBSIDY_TYPE;

// Render the newer `CourseRunCard` component when the user's subsidy, if any, is
Expand Down

0 comments on commit 0c2eb59

Please sign in to comment.