diff --git a/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.js b/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.js index e35559c971..5e765cf351 100644 --- a/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.js +++ b/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.js @@ -20,7 +20,7 @@ export default function useIsAssignmentsOnlyLearner() { }, }, } = useBrowseAndRequest(); - const { data: { couponCodeAssignments } } = useCouponCodes(); + const { data: { couponCodeRedemptionCount } } = useCouponCodes(); const { data: { hasCurrentEnterpriseOffers } } = useEnterpriseOffers(); const { data: redeemableLearnerCreditPolicies } = useRedeemablePolicies(); @@ -29,7 +29,7 @@ export default function useIsAssignmentsOnlyLearner() { subscriptionLicense, licenseRequests, couponCodeRequests, - couponCodesCount: couponCodeAssignments.length, + couponCodesCount: couponCodeRedemptionCount, redeemableLearnerCreditPolicies, hasCurrentEnterpriseOffers, }); diff --git a/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.test.jsx b/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.test.jsx index 7bab04b023..f9a496c61d 100644 --- a/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.test.jsx +++ b/src/components/app/data/hooks/useIsAssignmentsOnlyLearner.test.jsx @@ -385,9 +385,7 @@ describe('useIsAssignmentsOnlyLearner', () => { }, }, hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, + subscriptionPlan: undefined, subscriptionLicense: undefined, licenseRequests: [], couponCodesCount: 1, @@ -475,7 +473,10 @@ describe('useIsAssignmentsOnlyLearner', () => { }); useCouponCodes.mockReturnValue({ data: { - couponCodeAssignments: new Array(couponCodesCount).fill('test-coupon-code-assignments'), + couponCodeAssignments: new Array(couponCodesCount).fill({ + redemptionsRemaining: 1, + }), + couponCodeRedemptionCount: couponCodesCount, }, }); useEnterpriseOffers.mockReturnValue({ diff --git a/src/components/app/data/services/subsidies/couponCodes.js b/src/components/app/data/services/subsidies/couponCodes.js index c66864dd5c..3a4ef855fc 100644 --- a/src/components/app/data/services/subsidies/couponCodes.js +++ b/src/components/app/data/services/subsidies/couponCodes.js @@ -3,6 +3,7 @@ import { logError } from '@edx/frontend-platform/logging'; import { fetchPaginatedData } from '../utils'; import { hasValidStartExpirationDates } from '../../../../../utils/common'; +import { findCouponCodeRedemptionCount } from '../../../../enterprise-user-subsidy/coupons'; // Coupon Codes @@ -68,8 +69,10 @@ export async function fetchCouponCodes(enterpriseUuid) { fetchCouponsOverview(enterpriseUuid), fetchCouponCodeAssignments(enterpriseUuid), ]); + const couponCodeRedemptionCount = findCouponCodeRedemptionCount(results[1]); return { couponsOverview: results[0], couponCodeAssignments: results[1], + couponCodeRedemptionCount, }; } diff --git a/src/components/app/data/services/subsidies/couponCodes.test.js b/src/components/app/data/services/subsidies/couponCodes.test.js index 4db6921591..5ca0865124 100644 --- a/src/components/app/data/services/subsidies/couponCodes.test.js +++ b/src/components/app/data/services/subsidies/couponCodes.test.js @@ -81,7 +81,10 @@ describe('fetchCouponCodes', () => { const COUPON_CODE_ASSIGNMENTS_URL = getCouponCodeAssignmentsUrl(enterpriseId); const COUPONS_OVERVIEW_URL = getCouponsOverviewUrl(enterpriseId); const couponCodeAssignmentsResponse = { - results: [{ id: 123 }], + results: [ + { code: 123, redemptionsRemaining: 1 }, + { code: 456, redemptionsRemaining: 2 }, + ], }; const couponsOverviewResponse = { results: [{ id: 123 }], @@ -90,10 +93,14 @@ describe('fetchCouponCodes', () => { axiosMock.onGet(COUPONS_OVERVIEW_URL).reply(200, couponsOverviewResponse); const result = await fetchCouponCodes(enterpriseId); - const expectedCouponsCodeAssignmentsResponse = [{ ...couponCodeAssignmentsResponse.results[0], available: false }]; + const expectedCouponsCodeAssignmentsResponse = couponCodeAssignmentsResponse.results.map((assignment) => ({ + ...assignment, + available: false, + })); expect(result).toEqual({ couponCodeAssignments: expectedCouponsCodeAssignmentsResponse, couponsOverview: couponsOverviewResponse.results, + couponCodeRedemptionCount: 3, }); }); }); diff --git a/src/components/course/data/courseLoader.js b/src/components/course/data/courseLoader.js index 28a032e0b1..40a2a120fe 100644 --- a/src/components/course/data/courseLoader.js +++ b/src/components/course/data/courseLoader.js @@ -90,7 +90,11 @@ export default function makeCourseLoader(queryClient) { const redeemableLearnerCreditPolicies = subsidyResponses[0]; const { customerAgreement, subscriptionPlan, subscriptionLicense } = subsidyResponses[1]; const { hasCurrentEnterpriseOffers, currentEnterpriseOffers } = subsidyResponses[2]; - const { couponCodeAssignments, couponsOverview } = subsidyResponses[3]; + const { + couponCodeAssignments, + couponCodeRedemptionCount, + couponsOverview, + } = subsidyResponses[3]; const licenseRequests = subsidyResponses[4]; const couponCodeRequests = subsidyResponses[5]; const browseAndRequestConfiguration = subsidyResponses[6]; @@ -99,7 +103,7 @@ export default function makeCourseLoader(queryClient) { subscriptionLicense, licenseRequests, couponCodeRequests, - couponCodesCount: couponCodeAssignments.length, + couponCodesCount: couponCodeRedemptionCount, redeemableLearnerCreditPolicies, hasCurrentEnterpriseOffers, }); diff --git a/src/components/course/enrollment/components/ToEcomBasketPage.jsx b/src/components/course/enrollment/components/ToEcomBasketPage.jsx index b9b2ecd8e0..2759634bf4 100644 --- a/src/components/course/enrollment/components/ToEcomBasketPage.jsx +++ b/src/components/course/enrollment/components/ToEcomBasketPage.jsx @@ -19,7 +19,7 @@ import { useCouponCodes, useCourseMetadata, useEnterpriseCourseEnrollments } fro */ const ToEcomBasketPage = ({ enrollLabel, enrollmentUrl, courseRunPrice }) => { const { userSubsidyApplicableToCourse } = useUserSubsidyApplicableToCourse(); - const { data: { couponCodeAssignments } } = useCouponCodes(); + const { data: { couponCodeRedemptionCount } } = useCouponCodes(); const [isModalOpen, setIsModalOpen] = useState(false); const { data: { @@ -57,7 +57,7 @@ const ToEcomBasketPage = ({ enrollLabel, enrollmentUrl, courseRunPrice }) => { enrollmentUrl={enrollmentUrl} courseRunPrice={courseRunPrice} userSubsidyApplicableToCourse={userSubsidyApplicableToCourse} - couponCodesCount={couponCodeAssignments.length} + couponCodesCount={couponCodeRedemptionCount} onEnroll={handleEnroll} /> diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx index 0b82dd5de6..e40fff4b5d 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx @@ -18,7 +18,7 @@ const UpgradeCourseButton = ({ const [isModalOpen, setIsModalOpen] = useState(false); const { data: enterpriseCustomer } = useEnterpriseCustomer(); - const { data: { couponCodeAssignments } } = useCouponCodes(); + const { data: { couponCodeRedemptionCount } } = useCouponCodes(); const { subsidyForCourse, couponUpgradeUrl, @@ -57,7 +57,7 @@ const UpgradeCourseButton = ({ enrollmentUrl={couponUpgradeUrl} courseRunPrice={courseRunPrice} userSubsidyApplicableToCourse={subsidyForCourse} - couponCodesCount={couponCodeAssignments.length} + couponCodesCount={couponCodeRedemptionCount} onEnroll={handleEnroll} /> diff --git a/src/components/enterprise-subsidy-requests/data/hooks.js b/src/components/enterprise-subsidy-requests/data/hooks.js deleted file mode 100644 index f6d14f152d..0000000000 --- a/src/components/enterprise-subsidy-requests/data/hooks.js +++ /dev/null @@ -1,155 +0,0 @@ -import { - useState, useEffect, useCallback, useContext, -} from 'react'; -import { logError } from '@edx/frontend-platform/logging'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; -import { AppContext } from '@edx/frontend-platform/react'; - -import { - fetchSubsidyRequestConfiguration, - fetchLicenseRequests, - fetchCouponCodeRequests, -} from './service'; -import { getErrorResponseStatusCode } from '../../../utils/common'; -import { SUBSIDY_REQUEST_STATE, SUBSIDY_TYPE } from '../../../constants'; - -export const useSubsidyRequestConfiguration = (enterpriseUUID) => { - const [subsidyRequestConfiguration, setSubsidyRequestConfiguration] = useState(); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchCustomerConfiguration = async () => { - try { - const response = await fetchSubsidyRequestConfiguration(enterpriseUUID); - const config = camelCaseObject(response.data); - setSubsidyRequestConfiguration(config); - } catch (error) { - const httpErrorStatus = getErrorResponseStatusCode(error); - if (httpErrorStatus === 404) { - // Customer configuration does not exist, subsidy requests are turned off. - setSubsidyRequestConfiguration(null); - } else { - logError(error); - } - } finally { - setIsLoading(false); - } - }; - - fetchCustomerConfiguration(enterpriseUUID); - }, [enterpriseUUID]); - - return { subsidyRequestConfiguration, isLoading }; -}; - -/** - * @param {{ - * enterpriseCustomerUuid: string, - * subsidyRequestsEnabled: boolean, - * subsidyType: string - * }} subsidyRequestConfiguration The subsidy request configuration for the customer - * @returns {Object} { couponCodeRequests, licenseRequests, isLoading } - */ -export const useSubsidyRequests = (subsidyRequestConfiguration) => { - const [licenseRequests, setLicenseRequests] = useState([]); - const [couponCodeRequests, setCouponCodeRequests] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const { authenticatedUser: { email: userEmail } } = useContext(AppContext); - - const fetchSubsidyRequests = useCallback(async (subsidyType) => { - setIsLoading(true); - try { - const { enterpriseCustomerUuid: enterpriseUUID } = subsidyRequestConfiguration; - - const options = { - enterpriseUUID, - userEmail, - state: SUBSIDY_REQUEST_STATE.REQUESTED, - }; - - if (subsidyType === SUBSIDY_TYPE.COUPON) { - const { data: { results } } = await fetchCouponCodeRequests(options); - const requests = camelCaseObject(results); - setCouponCodeRequests(requests); - } if (subsidyType === SUBSIDY_TYPE.LICENSE) { - const { data: { results } } = await fetchLicenseRequests(options); - const requests = camelCaseObject(results); - setLicenseRequests(requests); - } - } catch (error) { - logError(error); - } finally { - setIsLoading(false); - } - }, [subsidyRequestConfiguration, userEmail]); - - const loadSubsidyRequests = useCallback(() => { - if (subsidyRequestConfiguration?.subsidyRequestsEnabled) { - const { subsidyType } = subsidyRequestConfiguration; - if (subsidyType) { - fetchSubsidyRequests(subsidyType); - } - } - }, [fetchSubsidyRequests, subsidyRequestConfiguration]); - - useEffect(() => { - loadSubsidyRequests(); - }, [loadSubsidyRequests]); - - return { - couponCodeRequests, - licenseRequests, - isLoading, - refreshSubsidyRequests: loadSubsidyRequests, - }; -}; - -export const useCatalogsForSubsidyRequests = ({ - subsidyRequestConfiguration, - isLoadingSubsidyRequestConfiguration, - customerAgreementConfig, - couponsOverview, -}) => { - const [catalogs, setCatalogs] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const getCatalogs = async () => { - if (subsidyRequestConfiguration.subsidyType === SUBSIDY_TYPE.COUPON) { - const catalogsFromCoupons = couponsOverview - .filter(coupon => !!coupon.available) - .map(coupon => coupon.enterpriseCatalogUuid); - - setCatalogs([...new Set(catalogsFromCoupons)]); - } - - if (subsidyRequestConfiguration.subsidyType === SUBSIDY_TYPE.LICENSE) { - const catalogsFromSubscriptions = customerAgreementConfig.subscriptions - .filter(subscription => subscription.daysUntilExpirationIncludingRenewals > 0) - .map(subscription => subscription.enterpriseCatalogUuid); - - setCatalogs([...new Set(catalogsFromSubscriptions)]); - } - - setIsLoading(false); - }; - - if (!isLoadingSubsidyRequestConfiguration) { - if (subsidyRequestConfiguration?.subsidyRequestsEnabled) { - getCatalogs(); - return; - } - setIsLoading(false); - } - }, [ - customerAgreementConfig, - isLoadingSubsidyRequestConfiguration, - subsidyRequestConfiguration, - couponsOverview, - ]); - - return { - catalogs, - isLoading, - }; -}; diff --git a/src/components/enterprise-subsidy-requests/data/index.js b/src/components/enterprise-subsidy-requests/data/index.js index db0bacf0dd..f78beabc33 100644 --- a/src/components/enterprise-subsidy-requests/data/index.js +++ b/src/components/enterprise-subsidy-requests/data/index.js @@ -1,2 +1 @@ -export * from './hooks'; export * from './service'; diff --git a/src/components/enterprise-subsidy-requests/data/service.js b/src/components/enterprise-subsidy-requests/data/service.js index d8114e11bd..41698b176c 100644 --- a/src/components/enterprise-subsidy-requests/data/service.js +++ b/src/components/enterprise-subsidy-requests/data/service.js @@ -1,43 +1,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getConfig } from '@edx/frontend-platform/config'; -import { SUBSIDY_REQUEST_STATE } from '../../../constants'; - -export function fetchSubsidyRequestConfiguration(enterpriseUUID) { - const url = `${getConfig().ENTERPRISE_ACCESS_BASE_URL}/api/v1/customer-configurations/${enterpriseUUID}/`; - return getAuthenticatedHttpClient().get(url); -} - -export function fetchLicenseRequests({ - enterpriseUUID, - userEmail, - state = SUBSIDY_REQUEST_STATE.REQUESTED, -}) { - const queryParams = new URLSearchParams({ - enterprise_customer_uuid: enterpriseUUID, - user__email: userEmail, - state, - }); - const config = getConfig(); - const url = `${config.ENTERPRISE_ACCESS_BASE_URL}/api/v1/license-requests/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -} - -export function fetchCouponCodeRequests({ - enterpriseUUID, - userEmail, - state = SUBSIDY_REQUEST_STATE.REQUESTED, -}) { - const queryParams = new URLSearchParams({ - enterprise_customer_uuid: enterpriseUUID, - user__email: userEmail, - state, - }); - const config = getConfig(); - const url = `${config.ENTERPRISE_ACCESS_BASE_URL}/api/v1/coupon-code-requests/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -} - export function postCouponCodeRequest(enterpriseUUID, courseID) { const options = { enterprise_customer_uuid: enterpriseUUID, diff --git a/src/components/enterprise-subsidy-requests/data/tests/hooks.test.jsx b/src/components/enterprise-subsidy-requests/data/tests/hooks.test.jsx deleted file mode 100644 index 99328955d4..0000000000 --- a/src/components/enterprise-subsidy-requests/data/tests/hooks.test.jsx +++ /dev/null @@ -1,263 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import * as logger from '@edx/frontend-platform/logging'; -import { AppContext } from '@edx/frontend-platform/react'; -import { - useCatalogsForSubsidyRequests, - useSubsidyRequestConfiguration, - useSubsidyRequests, -} from '../hooks'; -import * as service from '../service'; -import { SUBSIDY_REQUEST_STATE, SUBSIDY_TYPE } from '../../../../constants'; - -const mockEmail = 'edx@example.com'; -const mockEnterpriseUUID = 'enterprise-uuid'; - -jest.mock('../service'); -jest.mock('../../../enterprise-user-subsidy/coupons/data/service'); - -const wrapper = ({ children }) => ( - - {children} - -); - -describe('useSubsidyRequestConfiguration', () => { - afterEach(() => jest.clearAllMocks()); - - it('should fetch subsidy request configuration for the given enterprise', async () => { - service.fetchSubsidyRequestConfiguration.mockResolvedValue({ - data: { - subsidy_requests_enabled: true, - subsidy_type: SUBSIDY_TYPE.COUPON, - }, - }); - const { result, waitForNextUpdate } = renderHook(() => useSubsidyRequestConfiguration(mockEnterpriseUUID)); - await waitForNextUpdate(); - expect(result.current.subsidyRequestConfiguration).toEqual({ - subsidyRequestsEnabled: true, - subsidyType: SUBSIDY_TYPE.COUPON, - }); - }); - - it('sets subsidyRequestConfiguration to null if customer configuration does not exist', async () => { - const error = new Error('Something went wrong.'); - error.customAttributes = { - httpErrorStatus: 404, - }; - service.fetchSubsidyRequestConfiguration.mockRejectedValue(error); - const { result, waitForNextUpdate } = renderHook(() => useSubsidyRequestConfiguration(mockEnterpriseUUID)); - await waitForNextUpdate(); - expect(result.current.subsidyRequestConfiguration).toEqual(null); - }); - - it('handles any errors', async () => { - const error = new Error('Something went wrong.'); - service.fetchSubsidyRequestConfiguration.mockRejectedValue(error); - const { result, waitForNextUpdate } = renderHook(() => useSubsidyRequestConfiguration(mockEnterpriseUUID)); - await waitForNextUpdate(); - expect(result.current.subsidyRequestConfiguration).toEqual(undefined); - expect(logger.logError).toHaveBeenCalledWith(error); - }); -}); - -describe('useSubsidyRequests', () => { - afterEach(() => jest.clearAllMocks()); - - it.each([null, { - subsidyRequestsEnabled: false, - }, { - subsidyRequestsEnabled: true, - subsidyType: undefined, - }])('should not do anything if subsidy requests are disabled', async (subsidyRequestsConfiguration) => { - renderHook(() => useSubsidyRequests(subsidyRequestsConfiguration), { wrapper }); - expect(service.fetchCouponCodeRequests).not.toHaveBeenCalled(); - expect(service.fetchLicenseRequests).not.toHaveBeenCalled(); - }); - - it('should fetch coupon code requests', async () => { - service.fetchCouponCodeRequests.mockResolvedValue({ - data: { - results: [ - { - lms_user_id: 1, - enterprise_customer_uuid: mockEnterpriseUUID, - }, - ], - }, - }); - - const args = { - subsidyRequestsEnabled: true, - subsidyType: SUBSIDY_TYPE.COUPON, - enterpriseCustomerUuid: mockEnterpriseUUID, - }; - const { result, waitForNextUpdate } = renderHook(() => useSubsidyRequests(args), { wrapper }); - await waitForNextUpdate(); - expect(service.fetchCouponCodeRequests).toHaveBeenCalledWith({ - enterpriseUUID: mockEnterpriseUUID, - userEmail: mockEmail, - state: SUBSIDY_REQUEST_STATE.REQUESTED, - }); - expect(service.fetchLicenseRequests).not.toHaveBeenCalled(); - - expect(result.current.couponCodeRequests).toEqual( - [ - { - lmsUserId: 1, - enterpriseCustomerUuid: mockEnterpriseUUID, - }, - ], - ); - }); - - it('should fetch coupon code requests', async () => { - service.fetchCouponCodeRequests.mockResolvedValue({ - data: { - results: [ - { - lms_user_id: 1, - enterprise_customer_uuid: mockEnterpriseUUID, - }, - ], - }, - }); - - const args = { - subsidyRequestsEnabled: true, - subsidyType: SUBSIDY_TYPE.COUPON, - enterpriseCustomerUuid: mockEnterpriseUUID, - }; - const { result, waitForNextUpdate } = renderHook(() => useSubsidyRequests(args), { wrapper }); - await waitForNextUpdate(); - - expect(service.fetchCouponCodeRequests).toHaveBeenCalledWith({ - enterpriseUUID: mockEnterpriseUUID, - userEmail: mockEmail, - state: SUBSIDY_REQUEST_STATE.REQUESTED, - }); - expect(service.fetchLicenseRequests).not.toHaveBeenCalled(); - - expect(result.current.couponCodeRequests).toEqual( - [ - { - lmsUserId: 1, - enterpriseCustomerUuid: mockEnterpriseUUID, - }, - ], - ); - - expect(result.current.licenseRequests).toEqual([]); - }); - - it('should fetch license requests', async () => { - service.fetchLicenseRequests.mockResolvedValue({ - data: { - results: [ - { - lms_user_id: 1, - enterprise_customer_uuid: mockEnterpriseUUID, - }, - ], - }, - }); - - const args = { - subsidyRequestsEnabled: true, - subsidyType: SUBSIDY_TYPE.LICENSE, - enterpriseCustomerUuid: mockEnterpriseUUID, - }; - const { result, waitForNextUpdate } = renderHook(() => useSubsidyRequests(args), { wrapper }); - await waitForNextUpdate(); - - expect(service.fetchCouponCodeRequests).not.toHaveBeenCalled(); - expect(service.fetchLicenseRequests).toHaveBeenCalledWith({ - enterpriseUUID: mockEnterpriseUUID, - userEmail: mockEmail, - state: SUBSIDY_REQUEST_STATE.REQUESTED, - }); - - expect(result.current.licenseRequests).toEqual( - [ - { - lmsUserId: 1, - enterpriseCustomerUuid: mockEnterpriseUUID, - }, - ], - ); - - expect(result.current.couponCodeRequests).toEqual([]); - }); -}); - -describe('useCatalogsForSubsidyRequests', () => { - afterEach(() => jest.clearAllMocks()); - - it('sets isLoading to false if there is no subsidy request configuration', () => { - const args = { - subsidyRequestConfiguration: null, - isLoadingSubsidyRequestConfiguration: false, - customerAgreementConfig: null, - }; - const { result } = renderHook(() => useCatalogsForSubsidyRequests(args)); - - expect(result.current.isLoading).toBe(false); - }); - - it('fetches coupons overview and sets catalogs correctly if configured subsidy type is coupons', () => { - const mockCatalogUUIDs = ['uuid1', 'uuid2']; - const subsidyRequestConfiguration = { - subsidyType: SUBSIDY_TYPE.COUPON, - subsidyRequestsEnabled: true, - }; - const args = { - subsidyRequestConfiguration, - isLoadingSubsidyRequestConfiguration: false, - customerAgreementConfig: null, - couponsOverview: mockCatalogUUIDs.map(uuid => ({ - enterpriseCatalogUuid: uuid, - available: true, - })), - }; - const { result } = renderHook(() => useCatalogsForSubsidyRequests(args)); - - expect(result.current.isLoading).toBe(false); - expect([...result.current.catalogs]).toEqual(mockCatalogUUIDs); - }); - - it('does nothing if subsidy requests are not enabled', async () => { - const args = { - subsidyRequestConfiguration: { - subsidyType: SUBSIDY_TYPE.COUPON, - subsidyRequestsEnabled: false, - }, - isLoadingSubsidyRequestConfiguration: false, - customerAgreementConfig: null, - }; - const { result } = renderHook(() => useCatalogsForSubsidyRequests(args)); - - expect(result.current.isLoading).toBe(false); - expect([...result.current.catalogs]).toEqual([]); - }); - - it('sets catalogs from subscription plans correctly if configured subsidy type is licenses', async () => { - const mockCatalogUUIDs = ['uuid1', 'uuid2']; - const subsidyRequestConfiguration = { - subsidyType: SUBSIDY_TYPE.LICENSE, - subsidyRequestsEnabled: true, - }; - const args = { - subsidyRequestConfiguration, - isLoadingSubsidyRequestConfiguration: false, - customerAgreementConfig: { - subscriptions: mockCatalogUUIDs.map(uuid => ({ - enterpriseCatalogUuid: uuid, - daysUntilExpirationIncludingRenewals: 123, - })), - }, - }; - const { result } = renderHook(() => useCatalogsForSubsidyRequests(args)); - - expect(result.current.isLoading).toBe(false); - expect([...result.current.catalogs]).toEqual(mockCatalogUUIDs); - }); -}); diff --git a/src/components/enterprise-subsidy-requests/data/tests/service.test.js b/src/components/enterprise-subsidy-requests/data/tests/service.test.js index b8352001fd..9c31e44bcb 100644 --- a/src/components/enterprise-subsidy-requests/data/tests/service.test.js +++ b/src/components/enterprise-subsidy-requests/data/tests/service.test.js @@ -4,68 +4,55 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { mergeConfig } from '@edx/frontend-platform/config'; import { - fetchSubsidyRequestConfiguration, - fetchCouponCodeRequests, - fetchLicenseRequests, + postLicenseRequest, + postCouponCodeRequest, } from '../service'; -import { SUBSIDY_REQUEST_STATE } from '../../../../constants'; - jest.mock('@edx/frontend-platform/auth'); const axiosMock = new MockAdapter(axios); getAuthenticatedHttpClient.mockReturnValue(axios); axiosMock.onAny().reply(200); axios.get = jest.fn(); +axios.post = jest.fn(); const enterpriseAccessBaseUrl = `${process.env.ENTERPRISE_ACCESS_BASE_URL}`; const mockEnterpriseUUID = 'test-enterprise-id'; -const mockEmail = 'edx@example.com'; +const mockCourseId = 'test-course-id'; -describe('fetchSubsidyRequestConfiguration', () => { +describe('postCouponCodeRequest', () => { beforeEach(() => { + jest.clearAllMocks(); mergeConfig({ ENTERPRISE_ACCESS_BASE_URL: enterpriseAccessBaseUrl, }); }); - it('fetches subsidy request configuration for the given enterprise', () => { - fetchSubsidyRequestConfiguration(mockEnterpriseUUID); - expect(axios.get).toBeCalledWith(`${enterpriseAccessBaseUrl}/api/v1/customer-configurations/${mockEnterpriseUUID}/`); + it('posts coupon code request for the given enterprise and course', () => { + postCouponCodeRequest(mockEnterpriseUUID, mockCourseId); + const options = { + enterprise_customer_uuid: mockEnterpriseUUID, + course_id: mockCourseId, + }; + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith(`${enterpriseAccessBaseUrl}/api/v1/coupon-code-requests/`, options); }); }); -describe('fetchLicenseRequests', () => { - it('fetches license requests', () => { - fetchLicenseRequests({ - enterpriseUUID: mockEnterpriseUUID, - userEmail: mockEmail, - state: SUBSIDY_REQUEST_STATE.DECLINED, - }); - const queryParams = new URLSearchParams({ - enterprise_customer_uuid: mockEnterpriseUUID, - user__email: mockEmail, - state: SUBSIDY_REQUEST_STATE.DECLINED, +describe('postLicenseRequest', () => { + beforeEach(() => { + jest.clearAllMocks(); + mergeConfig({ + ENTERPRISE_ACCESS_BASE_URL: enterpriseAccessBaseUrl, }); - expect(axios.get).toBeCalledWith( - `${enterpriseAccessBaseUrl}/api/v1/license-requests/?${queryParams.toString()}`, - ); }); -}); -describe('fetchCouponCodeRequests', () => { - it('fetches coupon code requests', () => { - fetchCouponCodeRequests({ - enterpriseUUID: mockEnterpriseUUID, - userEmail: mockEmail, - state: SUBSIDY_REQUEST_STATE.REQUESTED, - }); - const queryParams = new URLSearchParams({ + it('posts license request for the given enterprise and course', () => { + postLicenseRequest(mockEnterpriseUUID, mockCourseId); + const options = { enterprise_customer_uuid: mockEnterpriseUUID, - user__email: mockEmail, - state: SUBSIDY_REQUEST_STATE.REQUESTED, - }); - expect(axios.get).toBeCalledWith( - `${enterpriseAccessBaseUrl}/api/v1/coupon-code-requests/?${queryParams.toString()}`, - ); + course_id: mockCourseId, + }; + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith(`${enterpriseAccessBaseUrl}/api/v1/license-requests/`, options); }); }); diff --git a/src/components/enterprise-user-subsidy/UserSubsidy.jsx b/src/components/enterprise-user-subsidy/UserSubsidy.jsx deleted file mode 100644 index 401d240f94..0000000000 --- a/src/components/enterprise-user-subsidy/UserSubsidy.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { - createContext, useContext, useMemo, -} from 'react'; -import PropTypes from 'prop-types'; -import { AppContext } from '@edx/frontend-platform/react'; -import { Container } from '@openedx/paragon'; - -import { LoadingSpinner } from '../loading-spinner'; -import { - useCouponCodes, - useSubscriptions, - useRedeemableLearnerCreditPolicies, -} from './data/hooks'; -import { useEnterpriseOffers } from './enterprise-offers/data/hooks'; -import { LOADING_SCREEN_READER_TEXT } from './data/constants'; -import { useEnterpriseCustomer } from '../app/data'; - -export const UserSubsidyContext = createContext(); - -const UserSubsidy = ({ children }) => { - const { authenticatedUser } = useContext(AppContext); - const { userId } = authenticatedUser; - const { data: enterpriseCustomer } = useEnterpriseCustomer(); - - // Subscriptions - const { - customerAgreementConfig, - subscriptionPlan, - subscriptionLicense, - isLoading: isLoadingSubscriptions, - showExpirationNotifications, - } = useSubscriptions({ enterpriseCustomer, authenticatedUser }); - - // Subsidy Access Policies - const { - data: redeemableLearnerCreditPolicies, - isLoading: isLoadingRedeemablePolicies, - } = useRedeemableLearnerCreditPolicies(enterpriseCustomer.uuid, userId); - - // Coupon Codes - const [couponCodes, isLoadingCouponCodes] = useCouponCodes(enterpriseCustomer.uuid); - - // Enterprise Offers - const { - enterpriseOffers, - currentEnterpriseOffers, - hasCurrentEnterpriseOffers, - canEnrollWithEnterpriseOffers, - hasLowEnterpriseOffersBalance, - hasNoEnterpriseOffersBalance, - isLoading: isLoadingEnterpriseOffers, - } = useEnterpriseOffers({ - enterpriseId: enterpriseCustomer.uuid, - enableLearnerPortalOffers: enterpriseCustomer.enableLearnerPortalOffers, - customerAgreementConfig, - }); - - const isLoadingSubsidies = useMemo( - () => { - const loadingStates = [ - isLoadingSubscriptions, - isLoadingCouponCodes, - isLoadingEnterpriseOffers, - isLoadingRedeemablePolicies, - ]; - return loadingStates.includes(true); - }, - [isLoadingSubscriptions, isLoadingCouponCodes, isLoadingEnterpriseOffers, isLoadingRedeemablePolicies], - ); - - const contextValue = useMemo( - () => { - if (isLoadingSubsidies) { - return {}; - } - return { - subscriptionLicense, - subscriptionPlan, - couponCodes, - enterpriseOffers, - currentEnterpriseOffers, - hasCurrentEnterpriseOffers, - canEnrollWithEnterpriseOffers, - hasLowEnterpriseOffersBalance, - hasNoEnterpriseOffersBalance, - showExpirationNotifications, - customerAgreementConfig, - redeemableLearnerCreditPolicies, - }; - }, - [ - isLoadingSubsidies, - subscriptionLicense, - subscriptionPlan, - couponCodes, - enterpriseOffers, - currentEnterpriseOffers, - hasCurrentEnterpriseOffers, - canEnrollWithEnterpriseOffers, - hasLowEnterpriseOffersBalance, - hasNoEnterpriseOffersBalance, - showExpirationNotifications, - customerAgreementConfig, - redeemableLearnerCreditPolicies, - ], - ); - - if (isLoadingSubsidies) { - return ( - - - - ); - } - return ( - <> - {/* Render the children so the rest of the page shows */} - - {children} - - - ); -}; - -UserSubsidy.propTypes = { - children: PropTypes.node.isRequired, -}; - -export default UserSubsidy; diff --git a/src/components/enterprise-user-subsidy/coupons/data/__mocks__/OfferAssignments.json b/src/components/enterprise-user-subsidy/coupons/data/__mocks__/OfferAssignments.json deleted file mode 100644 index 5a7cf2843c..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/__mocks__/OfferAssignments.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "count": 4, - "num_pages": 1, - "current_page": 1, - "results": [ - { - "usage_type": "Percentage", - "benefit_value": 100, - "redemptions_remaining": 4, - "code": "EPKAUH7MZ7WALXZV", - "catalog": "9014df44-e8eb-41c0-ab39-fb9a508ac716", - "coupon_start_date": "2019-07-14T00:00:00Z", - "coupon_end_date": "2019-07-31T00:00:00Z" - }, - { - "usage_type": "Percentage", - "benefit_value": 100, - "redemptions_remaining": 1, - "code": "XUFEXU3MXSWBYMLY", - "catalog": "97207126-7f9b-43c7-ab6d-c77260069126", - "coupon_start_date": "2019-04-30T00:00:00Z", - "coupon_end_date": "2020-04-30T00:00:00Z" - }, - { - "usage_type": "Percentage", - "benefit_value": 50, - "redemptions_remaining": 1, - "code": "FUT4VBONG3BA7R76", - "catalog": "88fce376-946e-419e-967b-ea6e68a44d23", - "coupon_start_date": "2019-07-15T00:00:00Z", - "coupon_end_date": "2019-07-30T00:00:00Z" - }, - { - "usage_type": "Absolute", - "benefit_value": 10, - "redemptions_remaining": 1, - "code": "2YL4L3QW32SZGVCY", - "catalog": "88fce376-946e-419e-967b-ea6e68a44d23", - "coupon_start_date": "2018-11-01T00:00:00Z", - "coupon_end_date": "2019-11-30T00:00:00Z" - } - ], - "next": null, - "start": 0, - "previous": null -} diff --git a/src/components/enterprise-user-subsidy/coupons/data/actions.js b/src/components/enterprise-user-subsidy/coupons/data/actions.js deleted file mode 100644 index 02906c9a5a..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/actions.js +++ /dev/null @@ -1,53 +0,0 @@ -import { camelCaseObject } from '@edx/frontend-platform'; - -import { - FETCH_COUPON_CODES_REQUEST, - FETCH_COUPON_CODES_SUCCESS, - FETCH_COUPON_CODES_FAILURE, -} from './constants'; - -import findCouponCodeRedemptionCount from './utils'; -import * as service from './service'; -import { hasValidStartExpirationDates } from '../../../../utils/common'; - -const fetchCouponCodesRequest = () => ({ - type: FETCH_COUPON_CODES_REQUEST, -}); - -const fetchCouponCodesSuccess = data => ({ - type: FETCH_COUPON_CODES_SUCCESS, - payload: { - couponCodes: data.results, - couponCodesCount: findCouponCodeRedemptionCount(data.results), - }, -}); - -const fetchCouponCodesFailure = error => ({ - type: FETCH_COUPON_CODES_FAILURE, - payload: { - error, - }, -}); - -export const fetchCouponCodeAssignments = (queryOptions, dispatch) => { - dispatch(fetchCouponCodesRequest()); - - return service.fetchCouponCodeAssignments(queryOptions) - .then((response) => { - const formattedResponse = camelCaseObject(response.data); - const transformedResults = formattedResponse.results.map((couponCode) => ({ - ...couponCode, - available: hasValidStartExpirationDates({ - startDate: couponCode.couponStartDate, - endDate: couponCode.couponEndDate, - }), - })); - dispatch(fetchCouponCodesSuccess(camelCaseObject({ - ...formattedResponse, - results: transformedResults, - }))); - }) - .catch((error) => { - dispatch(fetchCouponCodesFailure(error)); - }); -}; diff --git a/src/components/enterprise-user-subsidy/coupons/data/constants.js b/src/components/enterprise-user-subsidy/coupons/data/constants.js deleted file mode 100644 index 368a02a64c..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const FETCH_COUPON_CODES_REQUEST = 'FETCH_COUPON_CODES_REQUEST'; -export const FETCH_COUPON_CODES_SUCCESS = 'FETCH_COUPON_CODES_SUCCESS'; -export const FETCH_COUPON_CODES_FAILURE = 'FETCH_COUPON_CODES_FAILURE'; diff --git a/src/components/enterprise-user-subsidy/coupons/data/index.js b/src/components/enterprise-user-subsidy/coupons/data/index.js new file mode 100644 index 0000000000..04bca77e0d --- /dev/null +++ b/src/components/enterprise-user-subsidy/coupons/data/index.js @@ -0,0 +1 @@ +export * from './utils'; diff --git a/src/components/enterprise-user-subsidy/coupons/data/reducer.js b/src/components/enterprise-user-subsidy/coupons/data/reducer.js deleted file mode 100644 index 7517ab4d2d..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/reducer.js +++ /dev/null @@ -1,42 +0,0 @@ -import { - FETCH_COUPON_CODES_REQUEST, - FETCH_COUPON_CODES_SUCCESS, - FETCH_COUPON_CODES_FAILURE, -} from './constants'; - -export const initialCouponCodesState = { - loading: false, - couponCodes: [], - couponCodesCount: 0, - couponsOverview: [], - error: null, -}; - -const couponCodesReducer = (state, action) => { - switch (action.type) { - case FETCH_COUPON_CODES_REQUEST: - return { - ...state, - loading: true, - error: null, - }; - case FETCH_COUPON_CODES_SUCCESS: - return { - ...state, - loading: false, - error: null, - couponCodes: action.payload.couponCodes, - couponCodesCount: action.payload.couponCodesCount, - }; - case FETCH_COUPON_CODES_FAILURE: - return { - ...state, - loading: false, - error: action.payload.error, - }; - default: - return state; - } -}; - -export default couponCodesReducer; diff --git a/src/components/enterprise-user-subsidy/coupons/data/service.js b/src/components/enterprise-user-subsidy/coupons/data/service.js deleted file mode 100644 index e419cfb89b..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/service.js +++ /dev/null @@ -1,22 +0,0 @@ -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { getConfig } from '@edx/frontend-platform/config'; - -const fetchCouponCodeAssignments = (options) => { - const queryParams = new URLSearchParams(options); - const config = getConfig(); - const url = `${config.ECOMMERCE_BASE_URL}/api/v2/enterprise/offer_assignment_summary/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -}; - -const fetchCouponsOverview = ({ enterpriseId, options = {} }) => { - const queryParams = new URLSearchParams({ - page: 1, - page_size: 100, - ...options, - }); - const config = getConfig(); - const url = `${config.ECOMMERCE_BASE_URL}/api/v2/enterprise/coupons/${enterpriseId}/overview/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -}; - -export { fetchCouponCodeAssignments, fetchCouponsOverview }; diff --git a/src/components/enterprise-user-subsidy/coupons/data/tests/actions.test.js b/src/components/enterprise-user-subsidy/coupons/data/tests/actions.test.js deleted file mode 100644 index 59b43a679a..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/tests/actions.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { - FETCH_COUPON_CODES_REQUEST, - FETCH_COUPON_CODES_SUCCESS, - FETCH_COUPON_CODES_FAILURE, -} from '../constants'; -import { - fetchCouponCodeAssignments, -} from '../actions'; -import * as service from '../service'; -import { hasValidStartExpirationDates } from '../../../../../utils/common'; - -jest.mock('../service'); -jest.mock('../../../../../utils/common', () => ({ - ...jest.requireActual('../../../../../utils/common'), - hasValidStartExpirationDates: jest.fn(), -})); - -describe('fetchCouponCodeAssignments action', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it.each([ - { couponStartDate: '2020-10-20', couponEndDate: '2021-10-20', expectedAvailability: true }, - { couponStartDate: '2022-06-12', couponEndDate: '2023-06-12', expectedAvailability: false }, - ])('fetch coupon codes success (%s)', ({ - couponStartDate, - couponEndDate, - expectedAvailability, - }) => { - hasValidStartExpirationDates.mockReturnValue(expectedAvailability); - const expectedAction = [ - { type: FETCH_COUPON_CODES_REQUEST }, - { - type: FETCH_COUPON_CODES_SUCCESS, - payload: { - couponCodes: [{ - fooBar: 'foo', - redemptionsRemaining: 2, - available: expectedAvailability, - couponStartDate, - couponEndDate, - }], - couponCodesCount: 2, - }, - }, - ]; - - service.fetchCouponCodeAssignments.mockImplementation(( - () => Promise.resolve({ - data: { - results: [{ - foo_bar: 'foo', - redemptions_remaining: 2, - coupon_start_date: couponStartDate, - coupon_end_date: couponEndDate, - }], - count: 2, - }, - }) - )); - const dispatchSpy = jest.fn(); - return fetchCouponCodeAssignments(null, dispatchSpy) - .then(() => { - expect(dispatchSpy).toHaveBeenNthCalledWith(1, expectedAction[0]); - expect(dispatchSpy).toHaveBeenNthCalledWith(2, expectedAction[1]); - }); - }); - - it('fetch coupon codes failure', () => { - const expectedAction = [ - { type: FETCH_COUPON_CODES_REQUEST }, - { - type: FETCH_COUPON_CODES_FAILURE, - payload: { - error: Error, - }, - }, - ]; - - service.fetchCouponCodeAssignments.mockImplementation(( - () => Promise.reject(Error) - )); - const dispatchSpy = jest.fn(); - return fetchCouponCodeAssignments(null, dispatchSpy) - .then(() => { - expect(dispatchSpy).toHaveBeenNthCalledWith(1, expectedAction[0]); - expect(dispatchSpy).toHaveBeenNthCalledWith(2, expectedAction[1]); - }); - }); -}); diff --git a/src/components/enterprise-user-subsidy/coupons/data/tests/reducer.test.js b/src/components/enterprise-user-subsidy/coupons/data/tests/reducer.test.js deleted file mode 100644 index 114fbc9429..0000000000 --- a/src/components/enterprise-user-subsidy/coupons/data/tests/reducer.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import couponCodesReducer from '../reducer'; -import { - FETCH_COUPON_CODES_REQUEST, - FETCH_COUPON_CODES_SUCCESS, - FETCH_COUPON_CODES_FAILURE, -} from '../constants'; - -const initialState = { - loading: false, - couponCodes: [], - couponCodesCount: 0, - error: null, -}; - -describe('couponCodesReducer', () => { - it('should return the initial state', () => { - expect(couponCodesReducer(initialState, {})).toEqual(initialState); - }); - - it('should handle FETCH_COUPON_CODES_REQUEST', () => { - const expected = { - ...initialState, - loading: true, - error: null, - }; - expect(couponCodesReducer(initialState, { - type: FETCH_COUPON_CODES_REQUEST, - })).toEqual(expected); - }); - - it('should handle FETCH_COUPON_CODES_SUCCESS', () => { - const expected = { - loading: false, - couponCodes: ['some data'], - couponCodesCount: 4, - error: null, - }; - expect(couponCodesReducer(initialState, { - type: FETCH_COUPON_CODES_SUCCESS, - payload: { - couponCodes: ['some data'], - couponCodesCount: 4, - }, - })).toEqual(expected); - }); - - it('should handle FETCH_COUPON_CODES_FAILURE', () => { - const expected = { - ...initialState, - loading: false, - error: Error, - }; - expect(couponCodesReducer(initialState, { - type: FETCH_COUPON_CODES_FAILURE, - payload: { - error: Error, - }, - })).toEqual(expected); - }); -}); diff --git a/src/components/enterprise-user-subsidy/coupons/data/tests/utils.test.js b/src/components/enterprise-user-subsidy/coupons/data/tests/utils.test.js index 400f50dab7..3e490fe39f 100644 --- a/src/components/enterprise-user-subsidy/coupons/data/tests/utils.test.js +++ b/src/components/enterprise-user-subsidy/coupons/data/tests/utils.test.js @@ -1,4 +1,4 @@ -import findCouponCodeRedemptionCount from '../utils'; +import { findCouponCodeRedemptionCount } from '../utils'; describe('find coupon code redemption count function', () => { it('should not fail and return 0 if there are no remaining redemptions', () => { diff --git a/src/components/enterprise-user-subsidy/coupons/data/utils.jsx b/src/components/enterprise-user-subsidy/coupons/data/utils.jsx index 8cf2ad00ec..c3c6a320fa 100644 --- a/src/components/enterprise-user-subsidy/coupons/data/utils.jsx +++ b/src/components/enterprise-user-subsidy/coupons/data/utils.jsx @@ -1,4 +1,4 @@ -export default function findCouponCodeRedemptionCount(couponCodes) { +export function findCouponCodeRedemptionCount(couponCodes) { let totalRedemptionsRemaining = 0; couponCodes.forEach((couponCode) => { totalRedemptionsRemaining += couponCode.redemptionsRemaining; diff --git a/src/components/enterprise-user-subsidy/coupons/index.js b/src/components/enterprise-user-subsidy/coupons/index.js index de13cd12cf..3707679222 100644 --- a/src/components/enterprise-user-subsidy/coupons/index.js +++ b/src/components/enterprise-user-subsidy/coupons/index.js @@ -1,2 +1 @@ -export { fetchCouponCodeAssignments } from './data/actions'; -export { default as reducer } from './data/reducer'; +export * from './data'; diff --git a/src/components/enterprise-user-subsidy/data/constants.js b/src/components/enterprise-user-subsidy/data/constants.js index b03a3b2f99..f3541c6efb 100644 --- a/src/components/enterprise-user-subsidy/data/constants.js +++ b/src/components/enterprise-user-subsidy/data/constants.js @@ -4,35 +4,3 @@ export const LICENSE_STATUS = { REVOKED: 'revoked', UNASSIGNED: 'unassigned', }; - -export const LOADING_SCREEN_READER_TEXT = 'loading your edX benefits from your organization'; - -export const enterpriseUserSubsidyQueryKeys = { - // Namespace for all user subsidy query keys - all: ['user-subsidy'], - policy: () => [ - ...enterpriseUserSubsidyQueryKeys.all, - 'policy', - ], - // Used with query against `can-redeem` API endpoint - coursePolicyRedeemability: ({ - enterpriseId, lmsUserId, courseRunKeys, activeCourseRunKey, - }) => [ - ...enterpriseUserSubsidyQueryKeys.policy(), - enterpriseId, - 'can-redeem', - { lmsUserId, courseRunKeys, activeCourseRunKey }, - ], - // Used with query to fetch user's redeemable subsidy access policies - redeemablePolicies: (enterpriseId, userId) => [ - ...enterpriseUserSubsidyQueryKeys.policy(), - 'redeemable-policies', - enterpriseId, - userId], - // Used with query for polling pending policy transactions after initial policy redemption - pollPendingPolicyTransaction: (transaction) => [ - ...enterpriseUserSubsidyQueryKeys.policy(), - 'transactions', - transaction, - ], -}; diff --git a/src/components/enterprise-user-subsidy/data/hooks/hooks.js b/src/components/enterprise-user-subsidy/data/hooks/hooks.js deleted file mode 100644 index f71d13c44d..0000000000 --- a/src/components/enterprise-user-subsidy/data/hooks/hooks.js +++ /dev/null @@ -1,204 +0,0 @@ -import { - useEffect, useMemo, useReducer, useState, -} from 'react'; -import { logError } from '@edx/frontend-platform/logging'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; -import { useQuery } from '@tanstack/react-query'; - -import { fetchCouponCodeAssignments } from '../../coupons'; -import couponCodesReducer, { initialCouponCodesState } from '../../coupons/data/reducer'; - -import { enterpriseUserSubsidyQueryKeys, LICENSE_STATUS } from '../constants'; -import { - fetchCustomerAgreementData, - fetchRedeemableLearnerCreditPolicies, - fetchSubscriptionLicensesForUser, -} from '../service'; -import { features } from '../../../../config'; -import { fetchCouponsOverview } from '../../coupons/data/service'; -import { getAssignmentsByState, transformRedeemablePoliciesData } from '../utils'; - -/** - * Attempts to fetch any existing licenses associated with the authenticated user and the - * specified enterprise customer. Priority is given to activated licenses, then assigned - * licenses. - * - * @param {string} enterpriseId The UUID of the enterprise customer - * @returns An object representing a user's subscription license or null if no license was found. - */ -const fetchExistingUserLicense = async (enterpriseId) => { - try { - const response = await fetchSubscriptionLicensesForUser(enterpriseId); - const { results } = camelCaseObject(response.data); - /** - * Ordering of these status keys (i.e., activated, assigned, revoked) is important as the first - * license found when iterating through each status key in this order will be selected as the - * applicable license for use by the rest of the application. - * - * Example: an activated license will be chosen as the applicable license because activated licenses - * come first in ``licensesByStatus`` even if the user also has a revoked license. - */ - const licensesByStatus = { - [LICENSE_STATUS.ACTIVATED]: [], - [LICENSE_STATUS.ASSIGNED]: [], - [LICENSE_STATUS.REVOKED]: [], - }; - results.forEach((item) => { - licensesByStatus[item.status].push(item); - }); - const applicableLicense = Object.values(licensesByStatus).flat()[0]; - return applicableLicense; - } catch (error) { - logError(error); - return null; - } -}; - -/** - * Retrieves a license for the authenticated user, if applicable. First attempts to find any existing licenses - * for the user. If a license is found, the app uses it. - * - * @param {object} args - * @param {object} args.enterpriseCustomer The enterprise customer config - * @param {object} args.customerAgreementConfig The customer agreement config associated with the enterprise - * @param {boolean} args.isLoadingCustomerAgreementConfig Whether the customer agreement is still resolving - * @param {object} args.user The authenticated user - * @returns Object containing a user license, if applicable, whether the license data is still resolving, and a callback - * to activate the user license. - */ -export function useSubscriptionLicense({ - enterpriseCustomer, - customerAgreementConfig, - isLoadingCustomerAgreementConfig, - user, -}) { - const [license, setLicense] = useState(); - const [isLoading, setIsLoading] = useState(true); - - const { - enterpriseId, - enterpriseIdentityProvider, - } = useMemo(() => ({ - enterpriseId: enterpriseCustomer.uuid, - enterpriseIdentityProvider: enterpriseCustomer.identityProvider, - }), [enterpriseCustomer]); - - useEffect(() => { - async function retrieveUserLicense() { - const result = await fetchExistingUserLicense(enterpriseId); - return result; - } - - if (!isLoadingCustomerAgreementConfig) { - setIsLoading(true); - retrieveUserLicense().then((userLicense) => { - const subscriptionPlan = customerAgreementConfig?.subscriptions?.find( - subscription => subscription.uuid === userLicense?.subscriptionPlanUuid, - ); - - if (userLicense) { - setLicense({ - ...userLicense, - subscriptionPlan, - }); - } else { - setLicense(null); - } - - setIsLoading(false); - }); - } - }, [customerAgreementConfig, enterpriseId, enterpriseIdentityProvider, isLoadingCustomerAgreementConfig, user]); - - return { license, isLoading }; -} - -/** - * Given an enterprise UUID, returns overview of coupons associated with the enterprise - * and a list of coupon codes assigned to the authenticated user. - */ -export function useCouponCodes(enterpriseId) { - const [state, dispatch] = useReducer(couponCodesReducer, initialCouponCodesState); - - const couponsOverviewQueryData = useQuery({ - queryKey: ['coupons', 'overview', enterpriseId], - queryFn: async () => { - const response = await fetchCouponsOverview({ enterpriseId }); - return camelCaseObject(response.data); - }, - }); - useEffect( - () => { - if (features.ENROLL_WITH_CODES) { - fetchCouponCodeAssignments( - { - enterprise_uuid: enterpriseId, - full_discount_only: 'True', // Must be a string because the API does a string compare not a true JSON boolean compare. - is_active: 'True', - }, - dispatch, - ); - } - }, - [enterpriseId], - ); - - const result = useMemo(() => { - const updatedState = { - ...state, - couponsOverview: couponsOverviewQueryData, - loading: state.loading || couponsOverviewQueryData.isLoading, - }; - return [updatedState, updatedState.loading]; - }, [state, couponsOverviewQueryData]); - - return result; -} - -export function useCustomerAgreementData(enterpriseId) { - const [customerAgreement, setCustomerAgreement] = useState(); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - fetchCustomerAgreementData(enterpriseId) - .then((response) => { - const { results } = camelCaseObject(response.data); - // Note: customer agreements are unique, only 1 can exist per customer - setCustomerAgreement(results[0] || null); - }) - .catch((error) => { - logError(new Error(error)); - setCustomerAgreement(null); - }) - .finally(() => { - setIsLoading(false); - }); - }, [enterpriseId]); - - return [customerAgreement, isLoading]; -} - -const getRedeemablePoliciesData = async ({ queryKey }) => { - const enterpriseId = queryKey[3]; - const userID = queryKey[4]; - const response = await fetchRedeemableLearnerCreditPolicies(enterpriseId, userID); - const responseData = camelCaseObject(response.data); - const redeemablePolicies = transformRedeemablePoliciesData(responseData); - const learnerContentAssignments = getAssignmentsByState( - redeemablePolicies?.flatMap(item => item.learnerContentAssignments || []), - ); - return { - redeemablePolicies, - learnerContentAssignments, - }; -}; - -export function useRedeemableLearnerCreditPolicies(enterpriseId, userID) { - return useQuery({ - queryKey: enterpriseUserSubsidyQueryKeys.redeemablePolicies(enterpriseId, userID), - queryFn: getRedeemablePoliciesData, - onError: (error) => { - logError(error); - }, - }); -} diff --git a/src/components/enterprise-user-subsidy/data/hooks/index.js b/src/components/enterprise-user-subsidy/data/hooks/index.js deleted file mode 100644 index 6a59410e84..0000000000 --- a/src/components/enterprise-user-subsidy/data/hooks/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './hooks'; -export { default as useSubscriptions } from './useSubscriptions'; diff --git a/src/components/enterprise-user-subsidy/data/hooks/useSubscriptions.js b/src/components/enterprise-user-subsidy/data/hooks/useSubscriptions.js deleted file mode 100644 index c713168f2c..0000000000 --- a/src/components/enterprise-user-subsidy/data/hooks/useSubscriptions.js +++ /dev/null @@ -1,53 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { useSubscriptionLicense, useCustomerAgreementData } from './hooks'; -import { hasValidStartExpirationDates } from '../../../../utils/common'; - -/** - * Given an authenticated user and an enterprise customer config, returns the user's subscription license (if any) - * along with metadata about the customer agreement and subscription plan(s). Includes a function to allow consumers - * to activate the user's license. - */ -function useSubscriptions({ - authenticatedUser, - enterpriseCustomer, -}) { - const [subscriptionPlan, setSubscriptionPlan] = useState(); - const [showExpirationNotifications, setShowExpirationNotifications] = useState(); - const [customerAgreementConfig, isLoadingCustomerAgreementConfig] = useCustomerAgreementData(enterpriseCustomer.uuid); - const { - license: subscriptionLicense, - isLoading: isLoadingLicense, - } = useSubscriptionLicense({ - enterpriseCustomer, - customerAgreementConfig, - isLoadingCustomerAgreementConfig, - user: authenticatedUser, - }); - - useEffect( - () => { - if (subscriptionLicense?.subscriptionPlan) { - setSubscriptionPlan({ - ...subscriptionLicense.subscriptionPlan, - isCurrent: hasValidStartExpirationDates({ - startDate: subscriptionLicense.subscriptionPlan.startDate, - expirationDate: subscriptionLicense.subscriptionPlan.expirationDate, - }), - }); - } - setShowExpirationNotifications(!(customerAgreementConfig?.disableExpirationNotifications)); - }, - [subscriptionLicense, customerAgreementConfig], - ); - - return { - customerAgreementConfig, - subscriptionPlan, - subscriptionLicense, - isLoading: isLoadingCustomerAgreementConfig || isLoadingLicense, - showExpirationNotifications, - }; -} - -export default useSubscriptions; diff --git a/src/components/enterprise-user-subsidy/data/hooks/useSubscriptions.test.js b/src/components/enterprise-user-subsidy/data/hooks/useSubscriptions.test.js deleted file mode 100644 index e32988626d..0000000000 --- a/src/components/enterprise-user-subsidy/data/hooks/useSubscriptions.test.js +++ /dev/null @@ -1,122 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; - -import useSubscriptions from './useSubscriptions'; -import { useCustomerAgreementData, useSubscriptionLicense } from './hooks'; -import { hasValidStartExpirationDates } from '../../../../utils/common'; - -jest.mock('./hooks', () => ({ - ...jest.requireActual('./hooks'), - useCustomerAgreementData: jest.fn(), - useSubscriptionLicense: jest.fn(), -})); - -jest.mock('../../../../utils/common', () => ({ - ...jest.requireActual('../../../../utils/common'), - hasValidStartExpirationDates: jest.fn(), -})); - -const mockCustomerAgreement = { - uuid: 'test-customer-agreement-uuid', - disableExpirationNotifications: false, -}; -const mockSubscriptionPlan = { uuid: 'test-subscription-plan-uuid', isCurrent: true }; - -describe('useSubscriptions', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it.each([ - { - isLoadingCustomerAgreement: false, - isLoadingLicense: false, - expectedLoadingState: false, - }, - { - isLoadingCustomerAgreement: false, - isLoadingLicense: true, - expectedLoadingState: true, - }, - { - isLoadingCustomerAgreement: true, - isLoadingLicense: false, - expectedLoadingState: true, - }, - { - isLoadingCustomerAgreement: true, - isLoadingLicense: true, - expectedLoadingState: true, - }, - ])('handles loading states (%s)', ({ - isLoadingCustomerAgreement, - isLoadingLicense, - expectedLoadingState, - }) => { - useCustomerAgreementData.mockReturnValue([undefined, isLoadingCustomerAgreement]); - useSubscriptionLicense.mockReturnValue({ - license: undefined, - isLoading: isLoadingLicense, - }); - - const args = { - authenticatedUser: {}, - enterpriseCustomer: {}, - }; - const { result } = renderHook(() => useSubscriptions(args)); - expect(result.current).toEqual( - expect.objectContaining({ - isLoading: expectedLoadingState, - }), - ); - }); - - it.each([ - { - hasDisabledExpirationNotifications: false, - expectedShowExpirationNotifications: true, - isSubscriptionPlanCurrent: true, - }, - { - hasDisabledExpirationNotifications: true, - expectedShowExpirationNotifications: false, - isSubscriptionPlanCurrent: true, - }, - ])('does stuff (%s)', async ({ - hasDisabledExpirationNotifications, - expectedShowExpirationNotifications, - isSubscriptionPlanCurrent, - }) => { - const anotherMockCustomerAgreement = { - ...mockCustomerAgreement, - disableExpirationNotifications: hasDisabledExpirationNotifications, - }; - const mockSubscriptionPlanWithCurrentStatus = { - ...mockSubscriptionPlan, - isCurrent: isSubscriptionPlanCurrent, - }; - const mockSubscriptionLicense = { - uuid: 'test-license-uuid', - subscriptionPlan: mockSubscriptionPlanWithCurrentStatus, - }; - useCustomerAgreementData.mockReturnValue([anotherMockCustomerAgreement, false]); - useSubscriptionLicense.mockReturnValue({ - license: mockSubscriptionLicense, - isLoading: false, - }); - hasValidStartExpirationDates.mockReturnValue(isSubscriptionPlanCurrent); - const args = { - authenticatedUser: {}, - enterpriseCustomer: {}, - }; - const { result } = renderHook(() => useSubscriptions(args)); - expect(result.current).toEqual( - expect.objectContaining({ - customerAgreementConfig: anotherMockCustomerAgreement, - isLoading: false, - subscriptionLicense: mockSubscriptionLicense, - subscriptionPlan: mockSubscriptionPlanWithCurrentStatus, - showExpirationNotifications: expectedShowExpirationNotifications, - }), - ); - }); -}); diff --git a/src/components/enterprise-user-subsidy/data/service.js b/src/components/enterprise-user-subsidy/data/service.js deleted file mode 100644 index c3fd9f26ee..0000000000 --- a/src/components/enterprise-user-subsidy/data/service.js +++ /dev/null @@ -1,46 +0,0 @@ -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { getConfig } from '@edx/frontend-platform/config'; -import { loginRefresh } from '../../../utils/common'; - -export function fetchSubscriptionLicensesForUser(enterpriseUUID) { - const queryParams = new URLSearchParams({ - enterprise_customer_uuid: enterpriseUUID, - include_revoked: true, - }); - const config = getConfig(); - const url = `${config.LICENSE_MANAGER_URL}/api/v1/learner-licenses/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -} - -export async function activateLicense(activationKey) { - const config = getConfig(); - - // If the user has not refreshed their JWT since they created their account, - // we should refresh it so that they'll have appropriate roles (if available), - // and thus, have any appropriate permissions when making downstream requests. - loginRefresh(); - - const queryParams = new URLSearchParams({ activation_key: activationKey }); - const url = `${config.LICENSE_MANAGER_URL}/api/v1/license-activation/?${queryParams.toString()}`; - - return getAuthenticatedHttpClient().post(url); -} - -export function fetchCustomerAgreementData(enterpriseUUID) { - const queryParams = new URLSearchParams({ - enterprise_customer_uuid: enterpriseUUID, - }); - const config = getConfig(); - const url = `${config.LICENSE_MANAGER_URL}/api/v1/customer-agreement/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -} - -export function fetchRedeemableLearnerCreditPolicies(enterpriseUUID, userID) { - const queryParams = new URLSearchParams({ - enterprise_customer_uuid: enterpriseUUID, - lms_user_id: userID, - }); - const config = getConfig(); - const url = `${config.ENTERPRISE_ACCESS_BASE_URL}/api/v1/policy-redemption/credits_available/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -} diff --git a/src/components/enterprise-user-subsidy/data/utils.js b/src/components/enterprise-user-subsidy/data/utils.js deleted file mode 100644 index 9a71102527..0000000000 --- a/src/components/enterprise-user-subsidy/data/utils.js +++ /dev/null @@ -1,150 +0,0 @@ -import { logError } from '@edx/frontend-platform/logging'; - -import { ASSIGNMENT_TYPES, POLICY_TYPES } from '../enterprise-offers/data/constants'; -import { LICENSE_STATUS } from './constants'; - -/** - * Transforms the redeemable policies data by attaching the subsidy expiration date - * to each assignment within the policies, if available. - * @param {object[]} [policies] - Array of policy objects containing learner assignments. - * @returns {object} - Returns modified policies data with subsidy expiration dates attached to assignments. - */ -export const transformRedeemablePoliciesData = (policies = []) => policies.map((policy) => { - const assignmentsWithSubsidyExpiration = policy.learnerContentAssignments?.map(assignment => ({ - ...assignment, - subsidyExpirationDate: policy.subsidyExpirationDate, - })); - return { - ...policy, - learnerContentAssignments: assignmentsWithSubsidyExpiration, - }; -}); - -/** - * Determine whether learner has only content assignments available to them, based on the presence of: - * - content assignments for display (allocated or canceled) - * - no auto-applied budgets - * - no current enterprise offers - * - no active license or license requests - * - no assigned codes or code requests - * - * @param {Object} params - The parameters object. - * @param {Object} params.subscriptionPlan - The subscription plan of the learner. - * @param {Object} params.subscriptionLicense - The subscription license of the learner. - * @param {Array} params.licenseRequests - The license requests of the learner. - * @param {number} params.couponCodesCount - The count of assigned coupon codes of the learner. - * @param {Array} params.couponCodeRequests - The coupon code requests of the learner. - * @param {Object} params.redeemableLearnerCreditPolicies - The redeemable learner credit policies. - * @param {boolean} params.hasCurrentEnterpriseOffers - Whether the learner has current enterprise offers. - * @returns {boolean} - Returns true if the learner has only content assignments available to them, false otherwise. - */ -export const determineLearnerHasContentAssignmentsOnly = ({ - subscriptionPlan, - subscriptionLicense, - licenseRequests, - couponCodesCount, - couponCodeRequests, - redeemableLearnerCreditPolicies, - hasCurrentEnterpriseOffers, -}) => { - const hasActiveLicense = !!(subscriptionPlan?.isActive && subscriptionLicense?.status === LICENSE_STATUS.ACTIVATED); - const hasActiveLicenseOrLicenseRequest = hasActiveLicense || licenseRequests.length > 0; - const hasAssignedCodesOrCodeRequests = couponCodesCount > 0 || couponCodeRequests.length > 0; - const autoAppliedPolicyTypes = [ - POLICY_TYPES.PER_LEARNER_CREDIT, - POLICY_TYPES.PER_ENROLLMENT_CREDIT, - ]; - const hasAutoAppliedLearnerCreditPolicies = !!redeemableLearnerCreditPolicies?.redeemablePolicies?.filter( - policy => autoAppliedPolicyTypes.includes(policy.policyType), - ).length > 0; - const hasAllocatedOrAcceptedAssignments = !!( - redeemableLearnerCreditPolicies?.learnerContentAssignments?.hasAllocatedAssignments - || redeemableLearnerCreditPolicies?.learnerContentAssignments?.hasAcceptedAssignments - ); - - return ( - hasAllocatedOrAcceptedAssignments - && !hasCurrentEnterpriseOffers - && !hasActiveLicenseOrLicenseRequest - && !hasAssignedCodesOrCodeRequests - && !hasAutoAppliedLearnerCreditPolicies - ); -}; - -/** - * Takes a flattened array of assignments and returns an object containing - * lists of assignments for each assignment state. - * - * @param {Array} assignments - List of content assignments. - * @returns {{ -* assignments: Array, -* hasAssignments: Boolean, -* allocatedAssignments: Array, -* hasAllocatedAssignments: Boolean, -* canceledAssignments: Array, -* hasCanceledAssignments: Boolean, -* acceptedAssignments: Array, -* hasAcceptedAssignments: Boolean, -* }} -*/ -export function getAssignmentsByState(assignments = []) { - const allocatedAssignments = []; - const acceptedAssignments = []; - const canceledAssignments = []; - const expiredAssignments = []; - const erroredAssignments = []; - const assignmentsForDisplay = []; - - assignments.forEach((assignment) => { - switch (assignment.state) { - case ASSIGNMENT_TYPES.ALLOCATED: - allocatedAssignments.push(assignment); - break; - case ASSIGNMENT_TYPES.ACCEPTED: - acceptedAssignments.push(assignment); - break; - case ASSIGNMENT_TYPES.CANCELED: - canceledAssignments.push(assignment); - break; - case ASSIGNMENT_TYPES.EXPIRED: - expiredAssignments.push(assignment); - break; - case ASSIGNMENT_TYPES.ERRORED: - erroredAssignments.push(assignment); - break; - default: - logError(`[getAssignmentsByState] Unsupported state ${assignment.state} for assignment ${assignment.uuid}`); - break; - } - }); - - const hasAssignments = assignments.length > 0; - const hasAllocatedAssignments = allocatedAssignments.length > 0; - const hasAcceptedAssignments = acceptedAssignments.length > 0; - const hasCanceledAssignments = canceledAssignments.length > 0; - const hasExpiredAssignments = expiredAssignments.length > 0; - const hasErroredAssignments = erroredAssignments.length > 0; - - // Concatenate all assignments for display (includes allocated and canceled assignments) - assignmentsForDisplay.push(...allocatedAssignments); - assignmentsForDisplay.push(...canceledAssignments); - assignmentsForDisplay.push(...expiredAssignments); - const hasAssignmentsForDisplay = assignmentsForDisplay.length > 0; - - return { - assignments, - hasAssignments, - allocatedAssignments, - hasAllocatedAssignments, - acceptedAssignments, - hasAcceptedAssignments, - canceledAssignments, - hasCanceledAssignments, - expiredAssignments, - hasExpiredAssignments, - erroredAssignments, - hasErroredAssignments, - assignmentsForDisplay, - hasAssignmentsForDisplay, - }; -} diff --git a/src/components/enterprise-user-subsidy/data/utils.test.js b/src/components/enterprise-user-subsidy/data/utils.test.js deleted file mode 100644 index 8fbedee082..0000000000 --- a/src/components/enterprise-user-subsidy/data/utils.test.js +++ /dev/null @@ -1,496 +0,0 @@ -import { ASSIGNMENT_TYPES, POLICY_TYPES } from '../enterprise-offers/data/constants'; -import { LICENSE_STATUS } from './constants'; -import { determineLearnerHasContentAssignmentsOnly, transformRedeemablePoliciesData } from './utils'; - -import { emptyRedeemableLearnerCreditPolicies } from '../../app/data'; - -describe('transformRedeemablePoliciesData', () => { - test('transforms policies data by attaching subsidy expiration date to assignments', () => { - const mockPolicies = [ - { - subsidyExpirationDate: '2024-03-15T18:48:26Z', - learnerContentAssignments: [ - { assignmentId: 1 }, - { assignmentId: 2 }, - ], - }, - { - subsidyExpirationDate: '2023-12-31T23:59:59Z', - learnerContentAssignments: [ - { assignmentId: 3 }, - ], - }, - ]; - - const expectedTransformedData = [ - { - subsidyExpirationDate: '2024-03-15T18:48:26Z', - learnerContentAssignments: [ - { assignmentId: 1, subsidyExpirationDate: '2024-03-15T18:48:26Z' }, - { assignmentId: 2, subsidyExpirationDate: '2024-03-15T18:48:26Z' }, - ], - }, - { - subsidyExpirationDate: '2023-12-31T23:59:59Z', - learnerContentAssignments: [ - { assignmentId: 3, subsidyExpirationDate: '2023-12-31T23:59:59Z' }, - ], - }, - ]; - - const transformedData = transformRedeemablePoliciesData(mockPolicies); - expect(transformedData).toEqual(expectedTransformedData); - }); -}); - -describe('determineLearnerHasContentAssignmentsOnly', () => { - test.each([ - /** - * - `isAssignmentLearnerOnly`: true - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: true, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: true - * - Has assignable redeemable policy with accepted assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: true, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ACCEPTED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ACCEPTED }], - hasAssignments: true, - allocatedAssignments: [], - hasAllocatedAssignments: false, - acceptedAssignments: [{ state: ASSIGNMENT_TYPES.ACCEPTED }], - hasAcceptedAssignments: true, - assignmentsForDisplay: [], - hasAssignmentsForDisplay: false, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has assignable redeemable policy with allocated assignment - * - Has another auto-applied redeemable policy - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - { - policyType: POLICY_TYPES.PER_LEARNER_CREDIT, - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has current enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: true, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: true - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has active subscription plan (without activated license) - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: true, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: true, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: true - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has inactive subscription plan (with activated license) - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: true, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: { status: LICENSE_STATUS.ACTIVATED }, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has active subscription plan (with activated license) - * - Has no subscription license requests - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: true, - }, - subscriptionLicense: { status: LICENSE_STATUS.ACTIVATED }, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has subscription license request(s) - * - Has no coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [{ id: 1 }], - couponCodesCount: 0, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license request(s) - * - Has available coupon codes - * - Has no coupon code requests - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 1, - couponCodeRequests: [], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has assignable redeemable policy with allocated assignment - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license request(s) - * - Has no coupon codes - * - Has coupon code request(s) - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: { - redeemablePolicies: [ - { - policyType: POLICY_TYPES.ASSIGNED_CREDIT, - learnerContentAssignments: [ - { state: ASSIGNMENT_TYPES.ALLOCATED }, - ], - }, - ], - learnerContentAssignments: { - ...emptyRedeemableLearnerCreditPolicies.learnerContentAssignments, - assignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignments: true, - allocatedAssignments: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAllocatedAssignments: true, - assignmentsForDisplay: [{ state: ASSIGNMENT_TYPES.ALLOCATED }], - hasAssignmentsForDisplay: true, - }, - }, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [{ id: 1 }], - }, - /** - * - `isAssignmentLearnerOnly`: false - * - Has no assignable redeemable policy - * - Has no other redeemable policies (auto-applied) - * - Has no enterprise offer - * - Has no active subscription plan and/or activated license - * - Has no subscription license request(s) - * - Has no coupon codes - * - Has no coupon code request(s) - */ - { - isAssignmentLearnerOnly: false, - redeemableLearnerCreditPolicies: emptyRedeemableLearnerCreditPolicies, - hasCurrentEnterpriseOffers: false, - subscriptionPlan: { - isActive: false, - }, - subscriptionLicense: undefined, - licenseRequests: [], - couponCodesCount: 0, - couponCodeRequests: [], - }, - ])('determines whether learner only has assignments available, i.e. no other subsidies (%s)', ({ - isAssignmentLearnerOnly, - redeemableLearnerCreditPolicies, - hasCurrentEnterpriseOffers, - subscriptionPlan, - subscriptionLicense, - licenseRequests, - couponCodesCount, - couponCodeRequests, - }) => { - const actualResult = determineLearnerHasContentAssignmentsOnly({ - subscriptionPlan, - subscriptionLicense, - licenseRequests, - couponCodesCount, - couponCodeRequests, - redeemableLearnerCreditPolicies, - hasCurrentEnterpriseOffers, - }); - expect(actualResult).toEqual(isAssignmentLearnerOnly); - }); -}); diff --git a/src/components/enterprise-user-subsidy/enterprise-offers/data/constants.js b/src/components/enterprise-user-subsidy/enterprise-offers/data/constants.js index c0df59bb38..fa7f56b3cc 100644 --- a/src/components/enterprise-user-subsidy/enterprise-offers/data/constants.js +++ b/src/components/enterprise-user-subsidy/enterprise-offers/data/constants.js @@ -30,6 +30,7 @@ export const NO_BALANCE_CONTACT_ADMIN_TEXT = 'Contact administrator'; export const OFFER_BALANCE_CLICK_EVENT = 'edx.ui.enterprise.learner_portal.offer_balance_alert.clicked'; +// TODO: why are these Learner Credit policies and content assignments related constants in this file? export const ASSIGNMENT_TYPES = { ACCEPTED: 'accepted', ALLOCATED: 'allocated', @@ -37,7 +38,6 @@ export const ASSIGNMENT_TYPES = { EXPIRED: 'expired', ERRORED: 'errored', }; - export const POLICY_TYPES = { ASSIGNED_CREDIT: 'AssignedLearnerCreditAccessPolicy', PER_LEARNER_CREDIT: 'PerLearnerSpendCreditAccessPolicy', diff --git a/src/components/enterprise-user-subsidy/enterprise-offers/data/hooks.js b/src/components/enterprise-user-subsidy/enterprise-offers/data/hooks.js deleted file mode 100644 index c56204f144..0000000000 --- a/src/components/enterprise-user-subsidy/enterprise-offers/data/hooks.js +++ /dev/null @@ -1,79 +0,0 @@ -import { - useState, - useEffect, -} from 'react'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; -import { logError } from '@edx/frontend-platform/logging'; -import { features } from '../../../../config'; -import * as enterpriseOffersService from './service'; -import { transformEnterpriseOffer } from './utils'; - -export const useEnterpriseOffers = ({ - enterpriseId, - enableLearnerPortalOffers, -}) => { - const [enterpriseOffers, setEnterpriseOffers] = useState([]); - const [currentEnterpriseOffers, setCurrentEnterpriseOffers] = useState([]); - const [isLoadingOffers, setIsLoadingOffers] = useState(true); - const [canEnrollWithEnterpriseOffers, setCanEnrollWithEnterpriseOffers] = useState(false); - const [hasCurrentEnterpriseOffers, setHasCurrentEnterpriseOffers] = useState(false); - const [hasLowEnterpriseOffersBalance, setHasLowEnterpriseOffersBalance] = useState(false); - const [hasNoEnterpriseOffersBalance, setHasNoEnterpriseOffersBalance] = useState(false); - - const enableOffers = features.FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS && enableLearnerPortalOffers; - - useEffect(() => { - // Fetch enterprise offers here if features.FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS is true - const fetchEnterpriseOffers = async () => { - try { - const response = await enterpriseOffersService.fetchEnterpriseOffers(enterpriseId); - const { results } = camelCaseObject(response.data); - setEnterpriseOffers(results.map(offer => transformEnterpriseOffer(offer))); - } catch (error) { - logError(error); - } finally { - setIsLoadingOffers(false); - } - }; - - if (enableOffers) { - fetchEnterpriseOffers(); - } else { - setIsLoadingOffers(false); - } - }, [enterpriseId, enableOffers]); - - useEffect(() => { - if (!enableOffers || isLoadingOffers) { - return; - } - - if (enterpriseOffers.length > 0) { - setCanEnrollWithEnterpriseOffers(true); - } - - const currentEntOffers = enterpriseOffers.filter(offer => offer.isCurrent); - if (currentEntOffers.length > 0) { - setCurrentEnterpriseOffers(currentEntOffers); - setHasCurrentEnterpriseOffers(true); - const hasLowBalance = currentEntOffers.some(offer => offer.isLowOnBalance); - const hasNoBalance = currentEntOffers.every(offer => offer.isOutOfBalance); - setHasLowEnterpriseOffersBalance(hasLowBalance); - setHasNoEnterpriseOffersBalance(hasNoBalance); - } - }, [ - isLoadingOffers, - enableOffers, - enterpriseOffers, - ]); - - return { - enterpriseOffers, - currentEnterpriseOffers, - hasCurrentEnterpriseOffers, - canEnrollWithEnterpriseOffers, - hasLowEnterpriseOffersBalance, - hasNoEnterpriseOffersBalance, - isLoading: isLoadingOffers, - }; -}; diff --git a/src/components/enterprise-user-subsidy/enterprise-offers/data/service.js b/src/components/enterprise-user-subsidy/enterprise-offers/data/service.js deleted file mode 100644 index 777910d957..0000000000 --- a/src/components/enterprise-user-subsidy/enterprise-offers/data/service.js +++ /dev/null @@ -1,17 +0,0 @@ -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { getConfig } from '@edx/frontend-platform/config'; -import { ENTERPRISE_OFFER_STATUS, ENTERPRISE_OFFER_USAGE_TYPE } from './constants'; - -export function fetchEnterpriseOffers(enterpriseId, options = { - usage_type: ENTERPRISE_OFFER_USAGE_TYPE.PERCENTAGE, - discount_value: 100, - status: ENTERPRISE_OFFER_STATUS.OPEN, - page_size: 100, -}) { - const queryParams = new URLSearchParams({ - ...options, - }); - const config = getConfig(); - const url = `${config.ECOMMERCE_BASE_URL}/api/v2/enterprise/${enterpriseId}/enterprise-learner-offers/?${queryParams.toString()}`; - return getAuthenticatedHttpClient().get(url); -} diff --git a/src/components/enterprise-user-subsidy/enterprise-offers/data/tests/hooks.test.jsx b/src/components/enterprise-user-subsidy/enterprise-offers/data/tests/hooks.test.jsx deleted file mode 100644 index a3480dee0e..0000000000 --- a/src/components/enterprise-user-subsidy/enterprise-offers/data/tests/hooks.test.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; - -import { useEnterpriseOffers } from '../hooks'; -import * as enterpriseOffersService from '../service'; -import * as config from '../../../../../config'; -import { transformEnterpriseOffer } from '../utils'; - -jest.mock('../../../coupons/data/service'); -jest.mock('../service'); -jest.mock('../../../data/service'); - -const mockEnterpriseUUID = 'enterprise-uuid'; -const defaultProps = { - enterpriseId: mockEnterpriseUUID, - enableLearnerPortalOffers: true, - customerAgreementConfig: undefined, - isLoadingCustomerAgreementConfig: false, -}; -const mockEnterpriseOffers = [{ - discount_value: 100, - end_datetime: '2023-01-06T00:00:00Z', - enterprise_catalog_uuid: 'uuid', - id: 1, - max_discount: 200, - remaining_balance: 200, - start_datetime: '2022-06-09T00:00:00Z', - usage_type: 'Percentage', - is_current: true, -}]; - -describe('useEnterpriseOffers', () => { - beforeEach(() => { - config.features.FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS = true; - }); - afterEach(() => jest.resetAllMocks()); - it('fetches and sets enterprise offers', async () => { - enterpriseOffersService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: mockEnterpriseOffers, - }, - }); - - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers(defaultProps)); - - await waitForNextUpdate(); - - expect(enterpriseOffersService.fetchEnterpriseOffers).toHaveBeenCalled(); - expect(result.current.enterpriseOffers).toEqual( - camelCaseObject(mockEnterpriseOffers).map(offer => transformEnterpriseOffer(offer)), - ); - }); - - it.each([{ - featureFlagToggled: true, - featureEnabledForEnterprise: false, - }, { - featureFlagToggled: false, - featureEnabledForEnterprise: true, - }])( - 'does not fetch enterprise offers if FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS = false or enableLearnerPortalOffers = false', - async ( - { - featureFlagToggled, - featureEnabledForEnterprise, - }, - ) => { - config.features.FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS = featureFlagToggled; - - renderHook(() => useEnterpriseOffers({ - ...defaultProps, - enableLearnerPortalOffers: featureEnabledForEnterprise, - })); - - expect(enterpriseOffersService.fetchEnterpriseOffers).not.toHaveBeenCalled(); - }, - ); - - it.each([ - { - offers: [], - expectedResult: false, - }, - { - offers: [ - { ...mockEnterpriseOffers[0], is_current: false }, - { ...mockEnterpriseOffers[0], id: 2, is_current: false }, - ], - expectedResult: true, - }, - { - offers: [mockEnterpriseOffers[0], { ...mockEnterpriseOffers[0], id: 2, is_current: false }], - expectedResult: true, - }, - ])('returns canEnrollWithEnterpriseOffers = true if the enterprise has >= 1 current (non-expired) enterprise offer', async ({ - offers, - expectedResult, - }) => { - enterpriseOffersService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { results: offers }, - }); - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers(defaultProps)); - await waitForNextUpdate(); - expect(result.current.canEnrollWithEnterpriseOffers).toEqual(expectedResult); - }); - - it.each([ - { - offerResults: [{ - ...mockEnterpriseOffers[0], - remaining_balance: 50, - max_discount: 1000, - }, { - ...mockEnterpriseOffers[0], - id: 2, - remaining_balance: 500, - max_discount: 1000, - }], - expectedHasLowEnterpriseOffersBalance: true, - }, - { - offerResults: [{ - ...mockEnterpriseOffers[0], - remaining_balance: 150, - max_discount: 1000, - }, { - ...mockEnterpriseOffers[0], - id: 2, - remaining_balance: 500, - max_discount: 1000, - }], - expectedHasLowEnterpriseOffersBalance: false, - }, - ])( - 'determines low balance when any enterprise offer has remainingBalance <= 10%', - async ({ offerResults, expectedHasLowEnterpriseOffersBalance }) => { - enterpriseOffersService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: offerResults, - }, - }); - - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers(defaultProps)); - - await waitForNextUpdate(); - - expect(result.current.hasLowEnterpriseOffersBalance).toEqual(expectedHasLowEnterpriseOffersBalance); - }, - ); - - it.each([ - { - offerResults: [{ - ...mockEnterpriseOffers[0], - remaining_balance: 50, - max_discount: 1000, - }, { - ...mockEnterpriseOffers[0], - remaining_balance: 80, - max_discount: 1000, - }], - expectedHasNoEnterpriseOffersBalance: true, - }, - { - offerResults: [{ - ...mockEnterpriseOffers[0], - remaining_balance: 100, - max_discount: 1000, - }, { - ...mockEnterpriseOffers[0], - remaining_balance: 25, - max_discount: 1000, - }], - expectedHasNoEnterpriseOffersBalance: false, - }, - ])( - 'determines no balance when all enterprise offers have remainingBalance <= 99', - async ({ - offerResults, expectedHasNoEnterpriseOffersBalance, - }) => { - enterpriseOffersService.fetchEnterpriseOffers.mockResolvedValueOnce({ - data: { - results: offerResults, - }, - }); - - const { result, waitForNextUpdate } = renderHook(() => useEnterpriseOffers(defaultProps)); - - await waitForNextUpdate(); - - expect(result.current.hasNoEnterpriseOffersBalance).toEqual(expectedHasNoEnterpriseOffersBalance); - }, - ); -}); diff --git a/src/components/enterprise-user-subsidy/enterprise-offers/data/tests/service.test.js b/src/components/enterprise-user-subsidy/enterprise-offers/data/tests/service.test.js deleted file mode 100644 index 70bcf52fb7..0000000000 --- a/src/components/enterprise-user-subsidy/enterprise-offers/data/tests/service.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { getConfig } from '@edx/frontend-platform/config'; - -import { fetchEnterpriseOffers } from '../service'; -import { ENTERPRISE_OFFER_STATUS, ENTERPRISE_OFFER_USAGE_TYPE } from '../constants'; - -jest.mock('@edx/frontend-platform/auth'); -const axiosMock = new MockAdapter(axios); -getAuthenticatedHttpClient.mockReturnValue(axios); -axiosMock.onAny().reply(200); -axios.get = jest.fn(); - -const TEST_ENTERPRISE_UUID = 'enterprise-uuid'; - -describe('fetchEnterpriseOffers', () => { - const config = getConfig(); - - it('fetches enterprise offers with the correct query params', () => { - const queryParams = new URLSearchParams({ - usage_type: ENTERPRISE_OFFER_USAGE_TYPE.PERCENTAGE, - discount_value: 100, - status: ENTERPRISE_OFFER_STATUS.OPEN, - page_size: 100, - }); - const url = `${config.ECOMMERCE_BASE_URL}/api/v2/enterprise/${TEST_ENTERPRISE_UUID}/enterprise-learner-offers/?${queryParams.toString()}`; - fetchEnterpriseOffers(TEST_ENTERPRISE_UUID); - expect(axios.get).toBeCalledWith(url); - }); -}); diff --git a/src/components/enterprise-user-subsidy/index.js b/src/components/enterprise-user-subsidy/index.js index 5b71e947de..dc09ebe534 100644 --- a/src/components/enterprise-user-subsidy/index.js +++ b/src/components/enterprise-user-subsidy/index.js @@ -1,2 +1 @@ -export { default as UserSubsidy, UserSubsidyContext } from './UserSubsidy'; export { default as EnterpriseOffersBalanceAlert } from './enterprise-offers/EnterpriseOffersBalanceAlert'; diff --git a/src/components/enterprise-user-subsidy/tests/UserSubsidy.test.jsx b/src/components/enterprise-user-subsidy/tests/UserSubsidy.test.jsx deleted file mode 100644 index 70b7df9dce..0000000000 --- a/src/components/enterprise-user-subsidy/tests/UserSubsidy.test.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { AppContext } from '@edx/frontend-platform/react'; -import '@testing-library/jest-dom/extend-expect'; - -import UserSubsidy from '../UserSubsidy'; -import { LOADING_SCREEN_READER_TEXT } from '../data/constants'; -import { useCouponCodes, useSubscriptions, useRedeemableLearnerCreditPolicies } from '../data/hooks'; -import { useEnterpriseOffers } from '../enterprise-offers/data/hooks'; -import { useEnterpriseCustomer } from '../../app/data'; -import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../app/data/services/data/__factories__'; - -jest.mock('../data/hooks', () => ({ - ...jest.requireActual('../data/hooks'), - useSubscriptions: jest.fn().mockReturnValue({}), - useCouponCodes: jest.fn().mockReturnValue([]), - useRedeemableLearnerCreditPolicies: jest.fn().mockReturnValue({ data: undefined }), -})); - -jest.mock('../enterprise-offers/data/hooks', () => ({ - ...jest.requireActual('../enterprise-offers/data/hooks'), - useEnterpriseOffers: jest.fn().mockReturnValue({}), -})); - -jest.mock('../../app/data', () => ({ - ...jest.requireActual('../../app/data'), - useEnterpriseCustomer: jest.fn(), -})); - -const mockEnterpriseCustomer = enterpriseCustomerFactory(); -const mockAuthenticatedUser = authenticatedUserFactory(); - -const UserSubsidyWithAppContext = ({ - contextValue = {}, - authenticatedUser = mockAuthenticatedUser, - children, -}) => ( - - - {children} - - -); - -describe('UserSubsidy', () => { - beforeEach(() => { - jest.clearAllMocks(); - useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer }); - }); - - test.each([ - { - isSubscriptionsLoading: false, - isCouponCodesLoading: false, - isEnterpriseOffersLoading: false, - isLoadingRedeemablePolicies: false, - isLoadingExpected: false, - }, - { - isSubscriptionsLoading: true, - isCouponCodesLoading: false, - isEnterpriseOffersLoading: false, - isLoadingRedeemablePolicies: false, - isLoadingExpected: true, - }, - { - isSubscriptionsLoading: false, - isCouponCodesLoading: true, - isEnterpriseOffersLoading: false, - isLoadingRedeemablePolicies: false, - isLoadingExpected: true, - }, - { - isSubscriptionsLoading: false, - isCouponCodesLoading: false, - isEnterpriseOffersLoading: true, - isLoadingRedeemablePolicies: false, - isLoadingExpected: true, - }, - { - isSubscriptionsLoading: false, - isCouponCodesLoading: false, - isEnterpriseOffersLoading: false, - isLoadingRedeemablePolicies: true, - isLoadingExpected: true, - }, - { - isSubscriptionsLoading: true, - isCouponCodesLoading: true, - isEnterpriseOffersLoading: true, - isLoadingRedeemablePolicies: true, - isLoadingExpected: true, - }, - { - isSubscriptionsLoading: true, - isCouponCodesLoading: true, - isEnterpriseOffersLoading: false, - isLoadingRedeemablePolicies: true, - isLoadingExpected: true, - }, - ])('shows loading spinner when expected (%s)', ({ - isSubscriptionsLoading, - isCouponCodesLoading, - isEnterpriseOffersLoading, - isLoadingRedeemablePolicies, - isLoadingExpected, - }) => { - useSubscriptions.mockReturnValue({ isLoading: isSubscriptionsLoading }); - useCouponCodes.mockReturnValue([[], isCouponCodesLoading]); - useEnterpriseOffers.mockReturnValue({ isLoading: isEnterpriseOffersLoading }); - useRedeemableLearnerCreditPolicies.mockReturnValue({ - data: { redeemablePolicies: [], learnerContentAssignments: [] }, - isLoading: isLoadingRedeemablePolicies, - }); - - const Component = ( - -
hello world
-
- ); - render(Component); - - if (isLoadingExpected) { - // assert component is loading - expect(screen.getByText(LOADING_SCREEN_READER_TEXT)).toBeInTheDocument(); - expect(screen.queryByText('hello world')).not.toBeInTheDocument(); - } else { - // assert component is not loading - expect(screen.queryByText(LOADING_SCREEN_READER_TEXT)).not.toBeInTheDocument(); - expect(screen.getByText('hello world')).toBeInTheDocument(); - } - }); -}); diff --git a/src/constants/subsidies.js b/src/constants/subsidies.js index d2f588f3c0..852e9d2d8b 100644 --- a/src/constants/subsidies.js +++ b/src/constants/subsidies.js @@ -9,4 +9,3 @@ export const SUBSIDY_REQUEST_STATE = { DECLINED: 'declined', ERROR: 'error', }; -export const LOADING_SCREEN_READER_TEXT = 'loading your subsidy requests from your organization';