Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[REFERENCE] feat: incrementally adopt Dashboard BFF API #1205

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions src/components/app/data/hooks/useEnterpriseCourseEnrollments.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useLocation } from 'react-router-dom';

import { queryEnterpriseCourseEnrollments } from '../queries';
import useEnterpriseCustomer from './useEnterpriseCustomer';
Expand All @@ -11,6 +12,7 @@ import {
transformLearnerContentAssignment,
transformSubsidyRequest,
} from '../utils';
import { resolveBFFQuery } from '../../routes/data/utils';
import { COURSE_STATUSES } from '../../../../constants';

export const transformAllEnrollmentsByStatus = ({
Expand All @@ -27,6 +29,34 @@ export const transformAllEnrollmentsByStatus = ({
return enrollmentsByStatus;
};

export function useBaseEnterpriseCourseEnrollments(queryOptions = {}) {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const location = useLocation();

// Determine the BFF query to use based on the current location
const matchedBFFQuery = resolveBFFQuery(location.pathname);

// Determine the query configuration: use the matched BFF query or fallback to default
let queryConfig;
if (matchedBFFQuery) {
queryConfig = {
...matchedBFFQuery(enterpriseCustomer.uuid),
// TODO: move transforms into the BFF response
select: (data) => data.enterpriseCourseEnrollments.map(transformCourseEnrollment),
};
} else {
queryConfig = {
...queryEnterpriseCourseEnrollments(enterpriseCustomer.uuid),
select: (data) => data.map(transformCourseEnrollment),
};
}

return useQuery({
...queryConfig,
enabled: queryOptions.enabled,
});
}

/**
* Retrieves the relevant enterprise course enrollments, subsidy requests (e.g., license
* requests), and content assignments for the active enterprise customer user.
Expand All @@ -35,11 +65,7 @@ export const transformAllEnrollmentsByStatus = ({
export default function useEnterpriseCourseEnrollments(queryOptions = {}) {
const isEnabled = queryOptions.enabled;
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { data: enterpriseCourseEnrollments } = useQuery({
...queryEnterpriseCourseEnrollments(enterpriseCustomer.uuid),
select: (data) => data.map(transformCourseEnrollment),
enabled: isEnabled,
});
const { data: enterpriseCourseEnrollments } = useBaseEnterpriseCourseEnrollments(queryOptions);
const { data: { requests } } = useBrowseAndRequest({
subscriptionLicensesQueryOptions: {
select: (data) => data.map((subsidyRequest) => transformSubsidyRequest({
Expand Down
28 changes: 27 additions & 1 deletion src/components/app/data/hooks/useSubscriptions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { useLocation } from 'react-router-dom';

import { querySubscriptions } from '../queries';
import useEnterpriseCustomer from './useEnterpriseCustomer';
import { transformSubscriptionsData } from '../services';
import { resolveBFFQuery } from '../../routes/data';

/**
* Custom hook to get subscriptions data for the enterprise.
Expand All @@ -9,8 +13,30 @@ import useEnterpriseCustomer from './useEnterpriseCustomer';
*/
export default function useSubscriptions(queryOptions = {}) {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const location = useLocation();

const matchedBFFQuery = resolveBFFQuery(location.pathname);

// Determine the query configuration: use the matched BFF query or fallback to default
let queryConfig;
if (matchedBFFQuery) {
queryConfig = {
...matchedBFFQuery(enterpriseCustomer.uuid),
select: (data) => {
const { customerAgreement, subscriptionLicenses } = data?.enterpriseCustomerUserSubsidies?.subscriptions || {};
if (!customerAgreement || !subscriptionLicenses) {
return {};
}
// TODO: move transforms into the BFF response
const transformedSubscriptionsData = transformSubscriptionsData(customerAgreement, subscriptionLicenses);
return transformedSubscriptionsData;
},
};
} else {
queryConfig = querySubscriptions(enterpriseCustomer.uuid);
}
return useQuery({
...querySubscriptions(enterpriseCustomer.uuid),
...queryConfig,
...queryOptions,
});
}
9 changes: 9 additions & 0 deletions src/components/app/data/queries/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,12 @@ export function queryVideoDetail(videoUUID: string, enterpriseUUID: string) {
._ctx.video
._ctx.detail(videoUUID);
}

// BFF queries

export function queryEnterpriseLearnerDashboardBFF(enterpriseUuid: string) {
return queries
.enterprise
.enterpriseCustomer(enterpriseUuid)
._ctx.bffs._ctx.dashboard;
}
10 changes: 10 additions & 0 deletions src/components/app/data/queries/queryKeyFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
fetchEnterpriseCourseEnrollments,
fetchEnterpriseCuration,
fetchEnterpriseCustomerContainsContent,
fetchEnterpriseLearnerDashboard,
fetchEnterpriseLearnerData,
fetchEnterpriseOffers,
fetchInProgressPathways,
Expand Down Expand Up @@ -47,6 +48,15 @@ const enterprise = createQueryKeys('enterprise', {
enterpriseCustomer: (enterpriseUuid) => ({
queryKey: [enterpriseUuid],
contextQueries: {
bffs: {
queryKey: null,
contextQueries: {
dashboard: ({
queryKey: null,
queryFn: ({ queryKey }) => fetchEnterpriseLearnerDashboard(queryKey[2]),
}),
},
},
academies: {
queryKey: null,
contextQueries: {
Expand Down
21 changes: 21 additions & 0 deletions src/components/app/data/services/bffs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getConfig } from '@edx/frontend-platform/config';
import { logError } from '@edx/frontend-platform/logging';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCaseObject } from '@edx/frontend-platform/utils';

export async function fetchEnterpriseLearnerDashboard(enterpriseId, lmsUserId) {
const { ENTERPRISE_ACCESS_BASE_URL } = getConfig();
const params = {
enterprise_customer_uuid: enterpriseId,
lms_user_id: lmsUserId,
};
const url = `${ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`;
try {
const result = await getAuthenticatedHttpClient().post(url, params);
return camelCaseObject(result.data);
} catch (error) {
logError(error);
// TODO: consider returning a sane default API response structure here to mitigate complete failure.
return {};
}
}
1 change: 1 addition & 0 deletions src/components/app/data/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './subsidies';
export * from './user';
export * from './utils';
export * from './videos';
export * from './bffs';
117 changes: 68 additions & 49 deletions src/components/app/data/services/subsidies/subscriptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,59 @@ export async function activateOrAutoApplySubscriptionLicense({
return activatedOrAutoAppliedLicense;
}

export function transformSubscriptionsData(customerAgreement, subscriptionLicenses) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[inform] Abstracted from fetchSubscriptions below. Now re-used along with the BFF response, but these types of transforms should ultimately live in the BFF.

const licensesByStatus = {
[LICENSE_STATUS.ACTIVATED]: [],
[LICENSE_STATUS.ASSIGNED]: [],
[LICENSE_STATUS.REVOKED]: [],
};
const subscriptionsData = {
subscriptionLicenses,
customerAgreement,
subscriptionLicense: null,
subscriptionPlan: null,
licensesByStatus,
showExpirationNotifications: false,
shouldShowActivationSuccessMessage: false,
};

subscriptionsData.customerAgreement = customerAgreement;
subscriptionsData.showExpirationNotifications = !(customerAgreement?.disableExpirationNotifications);

// Sort licenses within each license status by whether the associated subscription plans
// are current; current plans should be prioritized over non-current plans.
const sortedSubscriptionLicenses = [...subscriptionLicenses].sort((a, b) => {
const aIsCurrent = a.subscriptionPlan.isCurrent;
const bIsCurrent = b.subscriptionPlan.isCurrent;
if (aIsCurrent && bIsCurrent) { return 0; }
return aIsCurrent ? -1 : 1;
});
subscriptionsData.subscriptionLicenses = sortedSubscriptionLicenses;

// Group licenses by status.
subscriptionLicenses.forEach((license) => {
const { subscriptionPlan, status } = license;
const isUnassignedLicense = status === LICENSE_STATUS.UNASSIGNED;
if (isUnassignedLicense || !subscriptionPlan.isActive) {
return;
}
licensesByStatus[license.status].push(license);
});

// Extracts a single subscription license for the user, from the ordered licenses by status.
const applicableSubscriptionLicense = Object.values(licensesByStatus).flat()[0];
if (applicableSubscriptionLicense) {
subscriptionsData.subscriptionLicense = applicableSubscriptionLicense;
subscriptionsData.subscriptionPlan = applicableSubscriptionLicense.subscriptionPlan;
}
subscriptionsData.licensesByStatus = licensesByStatus;

return subscriptionsData;
}

/**
* TODO
* @returns
* Fetches the subscription licenses for the specified enterprise customer.
* @returns {Promise<Object>} The subscription licenses and related data.
* @param enterpriseUUID
*/
export async function fetchSubscriptions(enterpriseUUID) {
Expand All @@ -213,61 +263,30 @@ export async function fetchSubscriptions(enterpriseUUID) {
* 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]: [],
};
const subscriptionsData = {
subscriptionLicenses: [],
customerAgreement: null,
subscriptionLicense: null,
subscriptionPlan: null,
licensesByStatus,
showExpirationNotifications: false,
shouldShowActivationSuccessMessage: false,
};
try {
const {
results: subscriptionLicenses,
response,
} = await fetchPaginatedData(url);
const { customerAgreement } = response;
if (customerAgreement) {
subscriptionsData.customerAgreement = customerAgreement;
}
subscriptionsData.showExpirationNotifications = !(customerAgreement?.disableExpirationNotifications);

// Sort licenses within each license status by whether the associated subscription plans
// are current; current plans should be prioritized over non-current plans.
subscriptionLicenses.sort((a, b) => {
const aIsCurrent = a.subscriptionPlan.isCurrent;
const bIsCurrent = b.subscriptionPlan.isCurrent;
if (aIsCurrent && bIsCurrent) { return 0; }
return aIsCurrent ? -1 : 1;
});
subscriptionsData.subscriptionLicenses = subscriptionLicenses;

// Group licenses by status.
subscriptionLicenses.forEach((license) => {
const { subscriptionPlan, status } = license;
const isUnassignedLicense = status === LICENSE_STATUS.UNASSIGNED;
if (isUnassignedLicense || !subscriptionPlan.isActive) {
return;
}
licensesByStatus[license.status].push(license);
});

// Extracts a single subscription license for the user, from the ordered licenses by status.
const applicableSubscriptionLicense = Object.values(licensesByStatus).flat()[0];
if (applicableSubscriptionLicense) {
subscriptionsData.subscriptionLicense = applicableSubscriptionLicense;
subscriptionsData.subscriptionPlan = applicableSubscriptionLicense.subscriptionPlan;
}
subscriptionsData.licensesByStatus = licensesByStatus;
return subscriptionsData;
const transformedSubscriptionsData = transformSubscriptionsData(customerAgreement, subscriptionLicenses);
return transformedSubscriptionsData;
} catch (error) {
logError(error);
return subscriptionsData;
const emptySubscriptionsData = {
subscriptionLicenses: [],
customerAgreement: null,
subscriptionLicense: null,
subscriptionPlan: null,
licensesByStatus: {
[LICENSE_STATUS.ACTIVATED]: [],
[LICENSE_STATUS.ASSIGNED]: [],
[LICENSE_STATUS.REVOKED]: [],
},
showExpirationNotifications: false,
shouldShowActivationSuccessMessage: false,
};
return emptySubscriptionsData;
}
}
Loading
Loading