Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jest.mock('lodash-es', () => ({

jest.mock('../../../../../data/services/EnterpriseAccessApiService', () => ({
fetchBnrSubsidyRequests: jest.fn(),
fetchBnrSubsidyRequestsOverviw: jest.fn(),
fetchBnrSubsidyRequestsOverview: jest.fn(),
}));

jest.mock('../../utils', () => ({
Expand Down Expand Up @@ -125,7 +125,7 @@ describe('useBnrSubsidyRequests', () => {
beforeEach(() => {
jest.clearAllMocks();
EnterpriseAccessApiService.fetchBnrSubsidyRequests.mockResolvedValue(mockApiResponse);
EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverviw.mockResolvedValue(mockOverviewResponse);
EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverview.mockResolvedValue(mockOverviewResponse);
camelCaseObject.mockImplementation((data) => data);
debounce.mockImplementation((fn) => fn);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ const useBnrSubsidyRequests = ({
applyOverviewFiltersToOptions(filters, options);
}

const response = await EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverviw(
const response = await EnterpriseAccessApiService.fetchBnrSubsidyRequestsOverview(
enterpriseId,
subsidyAccessPolicyId,
options,
Expand Down
2 changes: 1 addition & 1 deletion src/components/subscriptions/MultipleSubscriptionsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const MultipleSubscriptionsPage = ({
leadText,
createActions,
}) => {
const { loading, data } = useContext(SubscriptionContext);
const { data, loading } = useContext(SubscriptionContext);
const subscriptions = data.results;

if (loading) {
Expand Down
65 changes: 50 additions & 15 deletions src/components/subscriptions/SubscriptionCard.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import dayjs from 'dayjs';
import { Link } from 'react-router-dom';
import {
Card, Badge, Button, Stack, Row, Col,
} from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import classNames from 'classnames';
import {
ACTIVE, FREE_TRIAL_BADGE, TRIAL, SCHEDULED, SUBSCRIPTION_STATUS_BADGE_MAP,
} from './data/constants';
import { useUpcomingInvoiceAmount } from './data/hooks';
import { getSubscriptionStatus } from './data/utils';
import { ACTIVE, SCHEDULED, SUBSCRIPTION_STATUS_BADGE_MAP } from './data/constants';
import { SubscriptionContext } from './SubscriptionData';
import { ADMINISTER_SUBSCRIPTIONS_TARGETS } from '../ProductTours/AdminOnboardingTours/constants';
import { makePlural } from '../../utils';

const SubscriptionCard = ({
subscription,
createActions,
}) => {
console.log('sub ', subscription);

Check warning on line 24 in src/components/subscriptions/SubscriptionCard.jsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
const {
title,
startDate,
expirationDate,
licenses = {},
planType,
startDate,
title,
uuid,
} = subscription;
const { setErrors } = useContext(SubscriptionContext);
console.log('kira info in subCard ', { uuid, planType });

Check warning on line 34 in src/components/subscriptions/SubscriptionCard.jsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
const { invoiceAmount } = useUpcomingInvoiceAmount({ uuid, planType, setErrors });
console.log('kira loading? ', invoiceAmount);

Check warning on line 36 in src/components/subscriptions/SubscriptionCard.jsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

const formattedStartDate = dayjs(startDate).format('MMMM D, YYYY');
const formattedExpirationDate = dayjs(expirationDate).format('MMMM D, YYYY');
const subscriptionStatus = getSubscriptionStatus(subscription);
const subCost = '$2,000 USD';

const renderDaysUntilPlanStartText = (className) => {
if (!(subscriptionStatus === SCHEDULED)) {
Expand All @@ -39,8 +53,8 @@
return (
<span className={classNames('d-block small', className)}>
Plan begins in {
daysUntilPlanStart > 0 ? `${daysUntilPlanStart} day${daysUntilPlanStart > 1 ? 's' : ''}`
: `${hoursUntilPlanStart} hour${hoursUntilPlanStart > 1 ? 's' : ''}`
daysUntilPlanStart > 0 ? `${makePlural(daysUntilPlanStart, 'day')}`
: `${makePlural(hoursUntilPlanStart, 'hour')}`
}
</span>
);
Expand Down Expand Up @@ -69,14 +83,32 @@
const renderCardHeader = () => {
const subtitle = (
<div className="d-flex flex-wrap align-items-center">
<Stack direction="horizontal" gap={3}>
<Badge variant={SUBSCRIPTION_STATUS_BADGE_MAP[subscriptionStatus].variant}>
{subscriptionStatus}
<Badge className="mr-2" variant={SUBSCRIPTION_STATUS_BADGE_MAP[subscriptionStatus].variant}>
{subscriptionStatus}
</Badge>
{planType === TRIAL && (
<>
<Badge className="mr-2" variant="info">
{FREE_TRIAL_BADGE}
</Badge>
<span>
{formattedStartDate} - {formattedExpirationDate}
</span>
</Stack>
{true && (
<FormattedMessage
id="subscriptions.subscriptionCard.freeTrialDescription"
defaultMessage="Your 14 day free trial will expire on {boldDate} and the card on file will be charged {subCost}."
description="Description text explaining the subscription details"
values={{
boldDate: <span className="mx-1 font-weight-bold">{formattedExpirationDate} </span>,
subCost: <span className="ml-1">{ subCost }</span>,
}}
/>
)}
</>
)}
{planType !== TRIAL && (
<span>
{formattedStartDate} - {formattedExpirationDate}
</span>
)}
</div>
);

Expand All @@ -90,6 +122,7 @@
actions={(
<div>
{renderActions() || renderDaysUntilPlanStartText('mt-4')}
{renderDaysUntilPlanStartText('mt-4')}
</div>
)}
/>
Expand Down Expand Up @@ -141,16 +174,18 @@

SubscriptionCard.propTypes = {
subscription: PropTypes.shape({
startDate: PropTypes.string.isRequired,
expirationDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
licenses: PropTypes.shape({
assigned: PropTypes.number.isRequired,
activated: PropTypes.number.isRequired,
allocated: PropTypes.number.isRequired,
unassigned: PropTypes.number.isRequired,
total: PropTypes.number.isRequired,
}),
planType: PropTypes.string.isRequired,
startDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
uuid: PropTypes.string.isRequired,
}).isRequired,
createActions: PropTypes.func,
};
Expand Down
5 changes: 5 additions & 0 deletions src/components/subscriptions/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const REVOKED = 'revoked';
export const REVOCABLE_STATUSES = [ACTIVATED, ASSIGNED];
export const ENROLLABLE_STATUSES = [ACTIVATED, ASSIGNED];

export const STRIPE_EVENT_SUMMARY = 'Stripe Event Summary';
export const SUBSCRIPTIONS = 'Subscriptions';
export const SUBSCRIPTION_USERS = 'Subscription Users';
export const SUBSCRIPTION_USERS_OVERVIEW = 'Subscription Users Overview';
Expand Down Expand Up @@ -48,13 +49,17 @@ export const USER_STATUS_BADGE_MAP = {
export const ACTIVE = 'Active';
export const ENDED = 'Ended';
export const SCHEDULED = 'Scheduled';
export const TRIAL = 'Trial';

export const SUBSCRIPTION_STATUS_BADGE_MAP = {
[ACTIVE]: { variant: 'primary' },
[SCHEDULED]: { variant: 'secondary' },
[ENDED]: { variant: 'light' },
[TRIAL]: { variant: 'info' },
};

export const FREE_TRIAL_BADGE = 'Free Trial';

// Browse and request constants `BrowseAndRequestAlert`
export const BROWSE_AND_REQUEST_ALERT_COOKIE_PREFIX = 'dismissed-browse-and-request-alert';

Expand Down
48 changes: 47 additions & 1 deletion src/components/subscriptions/data/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import LicenseManagerApiService from '../../../data/services/LicenseManagerAPIService';
import {
NETWORK_ERROR_MESSAGE,
STRIPE_EVENT_SUMMARY,
SUBSCRIPTION_USERS,
SUBSCRIPTION_USERS_OVERVIEW,
SUBSCRIPTIONS,
TRIAL,
} from './constants';
import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService';

const subscriptionInitState = {
results: [],
Expand Down Expand Up @@ -41,7 +44,7 @@
.forEach(customerAgreement => {
// Push information about whether a particular subscription
// should have expiration notices displayed for it down into
// that subcription.
// that subscription.
const flattenedSubscriptionResults = customerAgreement.subscriptions.map(subscription => ({
...subscription,
showExpirationNotifications: !(customerAgreement.disableExpirationNotifications || false),
Expand Down Expand Up @@ -233,3 +236,46 @@
loading,
};
};

/**
* This hook fetches a StripeEventSummary for pricing information about a trial SubscriptionPlan
* @param {string} uuid - The UUID of the SubscriptionPlan.
*/
export const useUpcomingInvoiceAmount = ({ uuid, planType, setErrors }) => {
const [loadingStripeSummary, setLoadingStripeSummary] = useState(true);
const [invoiceAmount, setInvoiceAmount] = useState(null);
useEffect(() => {
const fetchStripeEvent = async () => {
try {
const response = await EnterpriseAccessApiService.fetchStripeEvent(uuid);
const { results } = camelCaseObject(response.data);
for (const index in results) {

Check failure on line 252 in src/components/subscriptions/data/hooks.js

View workflow job for this annotation

GitHub Actions / lint

The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype
const summary = results[index];
if (summary.eventType === 'customer.subscription.created') {
setInvoiceAmount(summary.upcomingInvoiceAmountDue);
}
}
} catch (error) {
logError(error);
setErrors(s => ({
...s,
[STRIPE_EVENT_SUMMARY]: NETWORK_ERROR_MESSAGE,
}));
} finally {
setLoadingStripeSummary(false);
}
};
// only trial plans will have associated StripeEventSummaries
if (planType === TRIAL) {
fetchStripeEvent();
} else {
// return early prevent unnecessary calls to enterprise-access for non-trial plans

}
}, [setErrors, planType, uuid]);

return {
invoiceAmount,
loadingStripeSummary,
};
};
44 changes: 44 additions & 0 deletions src/components/subscriptions/tests/SubscriptionCard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
} from '@openedx/paragon';
import { renderWithRouter } from '../../test/testUtils';
import SubscriptionCard from '../SubscriptionCard';
import { FREE_TRIAL_BADGE, TRIAL } from '../data/constants';
import { useUpcomingInvoiceAmount } from '../data/hooks';

Check failure on line 13 in src/components/subscriptions/tests/SubscriptionCard.test.jsx

View workflow job for this annotation

GitHub Actions / lint

'useUpcomingInvoiceAmount' is defined but never used

const defaultSubscription = {
uuid: 'ided',
Expand All @@ -26,6 +28,24 @@
total: 20,
},
};

const trialSubscription = {

Check failure on line 32 in src/components/subscriptions/tests/SubscriptionCard.test.jsx

View workflow job for this annotation

GitHub Actions / lint

'trialSubscription' is assigned a value but never used
uuid: 'trial-uuid',
title: 'Trial Plan',
startDate: '2021-04-13',
expirationDate: '2024-04-13',
planType: TRIAL,
};
const trialProps = {
subscription: defaultSubscription,
licenses: {
assigned: 5,
unassigned: 2,
activated: 3,
allocated: 10,
total: 20,
},
};
const responsiveContextValue = { width: breakpoints.extraSmall.maxWidth };

jest.mock('dayjs', () => (date) => {
Expand All @@ -35,6 +55,24 @@
return jest.requireActual('dayjs')('2020-01-01T00:00:00.000Z');
});

jest.mock('../data/hooks', () => ({
...jest.requireActual('../data/hooks'),
useUpcomingInvoiceAmount: jest.fn().mockReturnValue({
stripeEventSummary: [
{
id: 1,
event_type: 'customer.subscription.created',
upcoming_invoice_amount_due: '150000',
}, {
id: 2,
event_type: 'invoice.paid',
upcoming_invoice_amount_due: null,
},
],
loadingStripeEvent: false,
}),
}));

describe('SubscriptionCard', () => {
it('displays subscription information', () => {
renderWithRouter(<SubscriptionCard {...defaultProps} />);
Expand Down Expand Up @@ -78,4 +116,10 @@
expect(mockCreateActions).toHaveBeenCalledWith(defaultSubscription);
expect(screen.getByText('action 1'));
});

it('displays trial subscription with additional subtitle', () => {
renderWithRouter(<SubscriptionCard {...trialProps} />);
expect(screen.getByText(FREE_TRIAL_BADGE));
expect(screen.getByText('Your 14 day free trial will expire on April 13th and the card on file will be charged {subCost}.'));
});
});
16 changes: 15 additions & 1 deletion src/data/services/EnterpriseAccessApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ class EnterpriseAccessApiService {
* @param {string} enterpriseUUID - The UUID of the enterprise customer.
* @returns {Promise<AxiosResponse>} - A promise that resolves to the API response.
*/
static fetchBnrSubsidyRequestsOverviw(enterpriseId, policyId, options = {}) {
static fetchBnrSubsidyRequestsOverview(enterpriseId, policyId, options = {}) {
const params = new URLSearchParams({
enterprise_customer_uuid: enterpriseId,
policy_uuid: policyId,
Expand All @@ -435,6 +435,20 @@ class EnterpriseAccessApiService {
const url = `${EnterpriseAccessApiService.baseUrl}/learner-credit-requests/overview/?${params.toString()}`;
return EnterpriseAccessApiService.apiClient().get(url);
}

/**
* Fetches StripeEventSummary for trial SubscriptionPlan
* @param {string} uuid - The UUID of the enterprise customer.
*
* @returns A promise that resolves to an AxiosResponse containing the stripe event information
*/
static fetchStripeEvent(uuid: string) {
const params = new URLSearchParams({
subscription_plan_uuid: uuid,
});
const url = `${EnterpriseAccessApiService.baseUrl}/stripe-event-summary/?${params.toString()}`;
return EnterpriseAccessApiService.apiClient().get(url);
}
}

export default EnterpriseAccessApiService;
Loading
Loading