Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type CustomerUsage,
type Subscription,
} from 'getsentry/types';
import UsageCharts from 'getsentry/views/subscriptionPage/usageOverview/charts';
import UsageCharts from 'getsentry/views/subscriptionPage/usageOverview/components/charts';

interface CategoryUsageDrawerProps {
categoryInfo: BillingMetricHistory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {OrganizationContext} from 'sentry/views/organizationContext';

import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import {PlanTier} from 'getsentry/types';
import UsageCharts from 'getsentry/views/subscriptionPage/usageOverview/charts';
import UsageCharts from 'getsentry/views/subscriptionPage/usageOverview/components/charts';
import type {BreakdownPanelProps} from 'getsentry/views/subscriptionPage/usageOverview/types';

describe('UsageCharts', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import moment from 'moment-timezone';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {CustomerUsageFixture} from 'getsentry-test/fixtures/customerUsage';
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
import {render, screen} from 'sentry-test/reactTestingLibrary';
import {resetMockDate, setMockDate} from 'sentry-test/utils';

import {DataCategory} from 'sentry/types/core';

import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import {OnDemandBudgetMode, type Subscription} from 'getsentry/types';
import ProductBreakdownPanel from 'getsentry/views/subscriptionPage/usageOverview/components/panel';

describe('ProductBreakdownPanel', () => {
const organization = OrganizationFixture();
let subscription: Subscription;
const usageData = CustomerUsageFixture();

beforeEach(() => {
setMockDate(new Date('2021-05-07'));
organization.features = ['subscriptions-v3', 'seer-billing'];
organization.access = ['org:billing'];
subscription = SubscriptionFixture({organization, plan: 'am3_business'});
SubscriptionStore.set(organization.slug, subscription);
});

afterEach(() => {
resetMockDate();
});

it('renders product trial CTA', async () => {
subscription.productTrials = [
{
category: DataCategory.PROFILE_DURATION,
isStarted: false,
reasonCode: 1001,
startDate: undefined,
endDate: moment().utc().add(20, 'years').format(),
},
];
SubscriptionStore.set(organization.slug, subscription);
render(
<ProductBreakdownPanel
subscription={subscription}
organization={organization}
usageData={usageData}
selectedProduct={DataCategory.PROFILE_DURATION}
/>
);

await screen.findByRole('heading', {name: 'Continuous Profile Hours'});
expect(screen.getByRole('button', {name: 'Activate free trial'})).toBeInTheDocument();
});

it('renders upgrade CTA', async () => {
render(
<ProductBreakdownPanel
subscription={subscription}
organization={organization}
usageData={usageData}
selectedProduct={DataCategory.PROFILE_DURATION}
/>
);

await screen.findByRole('heading', {name: 'Continuous Profile Hours'});
expect(screen.getByRole('button', {name: 'Upgrade now'})).toBeInTheDocument();
});

it('renders active product trial status', async () => {
subscription.productTrials = [
{
category: DataCategory.REPLAYS,
isStarted: true,
reasonCode: 1001,
startDate: moment().utc().subtract(10, 'days').format(),
endDate: moment().utc().add(20, 'days').format(),
},
];
SubscriptionStore.set(organization.slug, subscription);
render(
<ProductBreakdownPanel
subscription={subscription}
organization={organization}
usageData={usageData}
selectedProduct={DataCategory.REPLAYS}
/>
);

await screen.findByRole('heading', {name: 'Replays'});
expect(screen.getByText('Trial - 20 days left')).toBeInTheDocument();
});

it('renders usage exceeded status without PAYG set', async () => {
subscription.categories.errors = {
...subscription.categories.errors!,
usageExceeded: true,
};
SubscriptionStore.set(organization.slug, subscription);
render(
<ProductBreakdownPanel
subscription={subscription}
organization={organization}
usageData={usageData}
selectedProduct={DataCategory.ERRORS}
/>
);

await screen.findByRole('heading', {name: 'Errors'});
expect(screen.getByText('Usage exceeded')).toBeInTheDocument();
});

it('renders usage exceeded status with PAYG set', async () => {
subscription.onDemandBudgets = {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 100_00,
onDemandSpendUsed: 100_00,
};
subscription.categories.errors = {
...subscription.categories.errors!,
usageExceeded: true,
};
SubscriptionStore.set(organization.slug, subscription);
render(
<ProductBreakdownPanel
subscription={subscription}
organization={organization}
usageData={usageData}
selectedProduct={DataCategory.ERRORS}
/>
);

await screen.findByRole('heading', {name: 'Errors'});
expect(screen.getByText('Pay-as-you-go limit reached')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {Fragment} from 'react';

import {Tag} from '@sentry/scraps/badge';
import {Container, Flex} from '@sentry/scraps/layout';
import {Heading} from '@sentry/scraps/text';

import {IconClock, IconWarning} from 'sentry/icons';
import {t, tct, tn} from 'sentry/locale';
import getDaysSinceDate from 'sentry/utils/getDaysSinceDate';

import {useProductBillingMetadata} from 'getsentry/hooks/useProductBillingMetadata';
import {AddOnCategory, OnDemandBudgetMode} from 'getsentry/types';
import {displayBudgetName, supportsPayg} from 'getsentry/utils/billing';
import UsageCharts from 'getsentry/views/subscriptionPage/usageOverview/components/charts';
import {
ProductTrialCta,
UpgradeCta,
} from 'getsentry/views/subscriptionPage/usageOverview/components/upgradeOrTrialCta';
import type {BreakdownPanelProps} from 'getsentry/views/subscriptionPage/usageOverview/types';

function PanelHeader({
selectedProduct,
subscription,
}: Pick<BreakdownPanelProps, 'selectedProduct' | 'subscription'>) {
const {onDemandBudgets: paygBudgets} = subscription;

const {
displayName,
billedCategory,
isAddOn,
addOnInfo,
usageExceeded,
activeProductTrial,
} = useProductBillingMetadata(subscription, selectedProduct);

if (
// special case for seer add-on
selectedProduct === AddOnCategory.SEER ||
!billedCategory ||
(isAddOn && !addOnInfo)
) {
return null;
}

const trialDaysLeft = -1 * getDaysSinceDate(activeProductTrial?.endDate ?? '');

const hasPaygAvailable =
supportsPayg(subscription) &&
subscription.planDetails.onDemandCategories.includes(billedCategory) &&
((paygBudgets?.budgetMode === OnDemandBudgetMode.SHARED &&
paygBudgets.sharedMaxBudget > 0) ||
(paygBudgets?.budgetMode === OnDemandBudgetMode.PER_CATEGORY &&
(paygBudgets?.budgets[billedCategory] ?? 0) > 0));

const status = activeProductTrial ? (
<Tag type="promotion" icon={<IconClock />}>
{tn('Trial - %s day left', 'Trial - %s days left', trialDaysLeft)}
</Tag>
) : usageExceeded ? (
<Tag type="error" icon={<IconWarning />}>
{hasPaygAvailable
? tct('[budgetTerm] limit reached', {
budgetTerm: displayBudgetName(subscription.planDetails, {title: true}),
})
: t('Usage exceeded')}
</Tag>
) : null;

return (
<Flex gap="md" align="center" borderBottom="primary" padding="xl">
<Heading as="h3">{displayName}</Heading>
{status}
</Flex>
);
}

function ProductBreakdownPanel({
organization,
selectedProduct,
subscription,
usageData,
isInline,
}: BreakdownPanelProps) {
const {billedCategory, isAddOn, isEnabled, addOnInfo, potentialProductTrial} =
useProductBillingMetadata(subscription, selectedProduct);

if (!billedCategory) {
return null;
}

const breakdownInfo = null;

if ((isAddOn && !addOnInfo) || (!isAddOn && !subscription.categories[billedCategory])) {
return null;
}

const isEmpty = !potentialProductTrial && !isEnabled;

return (
<Container
background="primary"
border={isInline ? undefined : 'primary'}
borderBottom={isInline ? 'primary' : undefined}
radius={isInline ? undefined : 'md'}
style={
isInline
? {
gridColumn: '1 / -1',
}
: undefined
}
>
<PanelHeader selectedProduct={selectedProduct} subscription={subscription} />
{potentialProductTrial && (
<ProductTrialCta
organization={organization}
subscription={subscription}
selectedProduct={selectedProduct}
showBottomBorder={isEnabled}
potentialProductTrial={potentialProductTrial}
/>
)}
{isEnabled && (
<Fragment>
{breakdownInfo}
<UsageCharts
selectedProduct={selectedProduct}
usageData={usageData}
subscription={subscription}
organization={organization}
/>
</Fragment>
)}
{isEmpty && <UpgradeCta organization={organization} subscription={subscription} />}
</Container>
);
}

export default ProductBreakdownPanel;
Loading
Loading