Skip to content

Commit

Permalink
feat: implementing gated top down allocation table subsidy service ap…
Browse files Browse the repository at this point in the history
…i usage
  • Loading branch information
alex-sheehan-edx committed Nov 8, 2023
1 parent d518a4d commit 70e50ae
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import { connect } from 'react-redux';
import LearnerCreditAllocationTable from './LearnerCreditAllocationTable';
import { useBudgetId, useOfferRedemptions } from './data';

const BudgetDetailRedemptions = ({ enterpriseUUID }) => {
const BudgetDetailRedemptions = ({ enterpriseFeatures, enterpriseUUID }) => {
const { enterpriseOfferId, subsidyAccessPolicyId } = useBudgetId();
const {
isLoading,
offerRedemptions,
fetchOfferRedemptions,
} = useOfferRedemptions(enterpriseUUID, enterpriseOfferId, subsidyAccessPolicyId);

} = useOfferRedemptions(
enterpriseUUID,
enterpriseOfferId,
subsidyAccessPolicyId,
enterpriseFeatures.topDownAssignmentRealTimeLcm,
);
return (
<section>
<h3 className="mb-3">Spent</h3>
Expand All @@ -30,11 +34,15 @@ const BudgetDetailRedemptions = ({ enterpriseUUID }) => {
};

const mapStateToProps = state => ({
enterpriseFeatures: state.portalConfiguration.enterpriseFeatures,
enterpriseUUID: state.portalConfiguration.enterpriseId,
});

BudgetDetailRedemptions.propTypes = {
enterpriseUUID: PropTypes.string.isRequired,
enterpriseFeatures: PropTypes.shape({
topDownAssignmentRealTimeLcm: PropTypes.bool,
}).isRequired,
};

export default connect(mapStateToProps)(BudgetDetailRedemptions);
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import debounce from 'lodash.debounce';

import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService';
import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService';
import { API_FIELDS_BY_TABLE_COLUMN_ACCESSOR } from '../constants';
import { transformUtilizationTableResults } from '../utils';
import { transformUtilizationTableResults, transformUtilizationTableSubsidyTransactionResults } from '../utils';
import useSubsidyAccessPolicy from './useSubsidyAccessPolicy';

const applySortByToOptions = (sortBy, options) => {
const orderingStrings = sortBy.map(({ id, desc }) => {
Expand All @@ -29,26 +31,34 @@ const applySortByToOptions = (sortBy, options) => {
});
};

const applyFiltersToOptions = (filters, options) => {
const applyFiltersToOptions = (filters, options, shouldFetchSubsidyTransactions = false) => {
const courseProductLineSearchQuery = filters?.find(filter => filter.id === 'courseProductLine')?.value;
const searchQuery = filters?.find(filter => filter.id.toLowerCase() === 'enrollment details')?.value;
const searchQuery = filters?.find(filter => filter.id === 'enrollmentDetails')?.value;

if (courseProductLineSearchQuery) {
Object.assign(options, { courseProductLine: courseProductLineSearchQuery });
}
if (searchQuery) {
Object.assign(options, { searchAll: searchQuery });
const searchParams = {};
searchParams[shouldFetchSubsidyTransactions ? 'search' : 'searchAll'] = searchQuery;
Object.assign(options, searchParams);
}
};

const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) => {
const useOfferRedemptions = (
enterpriseUUID,
offerId = null,
budgetId = null,
shouldFetchSubsidyTransactions = false,

Check warning on line 52 in src/components/learner-credit-management/data/hooks/useOfferRedemptions.js

View check run for this annotation

Codecov / codecov/patch

src/components/learner-credit-management/data/hooks/useOfferRedemptions.js#L52

Added line #L52 was not covered by tests
) => {
const shouldTrackFetchEvents = useRef(false);
const [isLoading, setIsLoading] = useState(true);
const [offerRedemptions, setOfferRedemptions] = useState({
itemCount: 0,
pageCount: 0,
results: [],
});
const { data: subsidyAccessPolicy } = useSubsidyAccessPolicy(budgetId);

const fetchOfferRedemptions = useCallback((args) => {
const fetch = async () => {
Expand All @@ -69,14 +79,26 @@ const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) =>
applySortByToOptions(args.sortBy, options);
}
if (args.filters?.length > 0) {
applyFiltersToOptions(args.filters, options);
applyFiltersToOptions(args.filters, options, shouldFetchSubsidyTransactions);
}
const response = await EnterpriseDataApiService.fetchCourseEnrollments(
enterpriseUUID,
options,
);
const data = camelCaseObject(response.data);
const transformedTableResults = transformUtilizationTableResults(data.results);
let data;
let transformedTableResults;
if (budgetId && shouldFetchSubsidyTransactions) {
const response = await SubsidyApiService.fetchCustomerTransactions(
subsidyAccessPolicy?.subsidyUuid,
options,
);
data = camelCaseObject(response.data);
transformedTableResults = transformUtilizationTableSubsidyTransactionResults(data.results);
} else {
const response = await EnterpriseDataApiService.fetchCourseEnrollments(
enterpriseUUID,
options,
);
data = camelCaseObject(response.data);
transformedTableResults = transformUtilizationTableResults(data.results);
}

setOfferRedemptions({
itemCount: data.count,
pageCount: data.numPages,
Expand Down Expand Up @@ -104,7 +126,14 @@ const useOfferRedemptions = (enterpriseUUID, offerId = null, budgetId = null) =>
if (offerId || budgetId) {
fetch();
}
}, [enterpriseUUID, offerId, budgetId, shouldTrackFetchEvents]);
}, [
enterpriseUUID,
offerId,
budgetId,
shouldTrackFetchEvents,
shouldFetchSubsidyTransactions,
subsidyAccessPolicy?.subsidyUuid,
]);

const debouncedFetchOfferRedemptions = useMemo(() => debounce(fetchOfferRedemptions, 300), [fetchOfferRedemptions]);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { act, renderHook } from '@testing-library/react-hooks/dom';
import { camelCaseObject } from '@edx/frontend-platform/utils';

import useOfferRedemptions from './useOfferRedemptions';
import useSubsidyAccessPolicy from './useSubsidyAccessPolicy';
import EnterpriseDataApiService from '../../../../data/services/EnterpriseDataApiService';
import SubsidyApiService from '../../../../data/services/EnterpriseSubsidyApiService';
import { queryClient } from '../../../test/testUtils';

const TEST_ENTERPRISE_UUID = 'test-enterprise-uuid';
const TEST_ENTERPRISE_OFFER_ID = 1;
const subsidyUuid = 'test-subsidy-uuid';
const courseTitle = 'Test Course Title';
const userEmail = '[email protected]';

const mockOfferEnrollments = [{
user_email: userEmail,
course_title: courseTitle,
course_list_price: '100.00',
enrollment_date: '2022-01-01',
}];

const mockOfferEnrollmentsResponse = {
count: 100,
current_page: 1,
num_pages: 5,
results: mockOfferEnrollments,
};

const mockSubsidyTransactionResponse = {
count: 100,
current_page: 1,
num_pages: 5,
results: [{
uuid: subsidyUuid,
state: 'committed',
idempotency_key: '5d00d319-fe46-41f7-b14e-966534da9f72',
lms_user_id: 999,
lms_user_email: userEmail,
content_key: 'course-v1:edX+test+course.1',
content_title: courseTitle,
quantity: -1000,
unit: 'usd_cents',
}],
};

const mockEnterpriseOffer = {
id: TEST_ENTERPRISE_OFFER_ID,
};

jest.mock('./useSubsidyAccessPolicy');
jest.mock('../../../../data/services/EnterpriseDataApiService');
jest.mock('../../../../data/services/EnterpriseSubsidyApiService');

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient()}>{children}</QueryClientProvider>
);

describe('useOfferRedemptions', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it.each([
{
budgetId: 'test-budget-id',
offerId: undefined,
shouldFetchSubsidyTransactions: true,
},
{
budgetId: 'test-budget-id',
offerId: undefined,
shouldFetchSubsidyTransactions: false,
},
{
budgetId: undefined,
offerId: mockEnterpriseOffer.id,
shouldFetchSubsidyTransactions: false,
},
])('should fetch enrollment/redemptions metadata for enterprise offer', async ({
budgetId,
offerId,
shouldFetchSubsidyTransactions,
}) => {
EnterpriseDataApiService.fetchCourseEnrollments.mockResolvedValueOnce({ data: mockOfferEnrollmentsResponse });
SubsidyApiService.fetchCustomerTransactions.mockResolvedValueOnce({ data: mockSubsidyTransactionResponse });
useSubsidyAccessPolicy.mockReturnValue({ data: { subsidyUuid } });

const { result, waitForNextUpdate } = renderHook(
() => useOfferRedemptions(TEST_ENTERPRISE_UUID, offerId, budgetId, shouldFetchSubsidyTransactions),
{ wrapper },
);

expect(result.current).toMatchObject({
offerRedemptions: {
itemCount: 0,
pageCount: 0,
results: [],
},
isLoading: true,
fetchOfferRedemptions: expect.any(Function),
});
act(() => {
result.current.fetchOfferRedemptions({
pageIndex: 0, // `DataTable` uses zero-based indexing
pageSize: 20,
sortBy: [
{ id: 'enrollmentDate', desc: true },
],
filters: [
{ id: 'enrollmentDetails', value: mockOfferEnrollments[0].user_email },
],
});
});

await waitForNextUpdate();

if (budgetId && shouldFetchSubsidyTransactions) {
const expectedApiOptions = {
page: 1,
pageSize: 20,
offerId,
ordering: '-enrollment_date', // default sort order
search: mockOfferEnrollments[0].user_email,
ignoreNullCourseListPrice: true,
budgetId,
};
expect(SubsidyApiService.fetchCustomerTransactions).toHaveBeenCalledWith(
subsidyUuid,
expectedApiOptions,
);
} else {
const expectedApiOptions = {
page: 1,
pageSize: 20,
offerId,
ordering: '-enrollment_date', // default sort order
searchAll: mockOfferEnrollments[0].user_email,
ignoreNullCourseListPrice: true,
budgetId,
};
expect(EnterpriseDataApiService.fetchCourseEnrollments).toHaveBeenCalledWith(
TEST_ENTERPRISE_UUID,
expectedApiOptions,
);
}

const mockExpectedResultsObj = shouldFetchSubsidyTransactions ? [{
courseListPrice: 10,
courseTitle,
userEmail,
}] : camelCaseObject(mockOfferEnrollments);

expect(result.current).toMatchObject({
offerRedemptions: {
itemCount: 100,
pageCount: 5,
results: mockExpectedResultsObj,
},
isLoading: false,
fetchOfferRedemptions: expect.any(Function),
});
});
});
Loading

0 comments on commit 70e50ae

Please sign in to comment.