Skip to content

Commit

Permalink
feat: incrementally adopt Dashboard BFF API
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed Oct 11, 2024
1 parent 5a1e7a6 commit 52115b8
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 137 deletions.
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) {
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

0 comments on commit 52115b8

Please sign in to comment.