diff --git a/static/gsApp/views/subscriptionPage/usageHistory.spec.tsx b/static/gsApp/views/subscriptionPage/usageHistory.spec.tsx index a607562cd9d43b..3a311f49aee1d6 100644 --- a/static/gsApp/views/subscriptionPage/usageHistory.spec.tsx +++ b/static/gsApp/views/subscriptionPage/usageHistory.spec.tsx @@ -8,7 +8,7 @@ import { Am3DsEnterpriseSubscriptionFixture, SubscriptionFixture, } from 'getsentry-test/fixtures/subscription'; -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {DataCategory} from 'sentry/types/core'; @@ -23,7 +23,9 @@ import { } from 'getsentry/constants'; import {SubscriptionStore} from 'getsentry/stores/subscriptionStore'; import {OnDemandBudgetMode, PlanTier} from 'getsentry/types'; -import UsageHistory from 'getsentry/views/subscriptionPage/usageHistory'; +import UsageHistory, { + usagePercentage, +} from 'getsentry/views/subscriptionPage/usageHistory'; describe('Subscription > UsageHistory', () => { const organization = OrganizationFixture({access: ['org:billing']}); @@ -166,6 +168,88 @@ describe('Subscription > UsageHistory', () => { expect(mockCall).toHaveBeenCalled(); }); + it('renders ∞ when reserved and prepaid are UNLIMITED_RESERVED', async () => { + MockApiClient.addMockResponse({ + url: `/customers/${organization.slug}/history/`, + method: 'GET', + body: [ + BillingHistoryFixture({ + categories: { + errors: MetricHistoryFixture({ + usage: 12345, + reserved: UNLIMITED_RESERVED, + prepaid: UNLIMITED_RESERVED, + }), + transactions: MetricHistoryFixture({ + category: DataCategory.TRANSACTIONS, + reserved: 1000, + prepaid: 1000, + }), + attachments: MetricHistoryFixture({ + category: DataCategory.ATTACHMENTS, + reserved: 1, + prepaid: 1, + }), + }, + }), + ], + }); + + const subscription = SubscriptionFixture({ + plan: 'am1_f', + planTier: PlanTier.AM1, + organization, + }); + SubscriptionStore.set(organization.slug, subscription); + + render(, {organization}); + + const errorsRow = await screen.findByRole('row', {name: /^Errors/}); + expect(within(errorsRow).getAllByRole('cell', {name: UNLIMITED})).toHaveLength(2); + expect(screen.queryByText('>100%')).not.toBeInTheDocument(); + }); + + it('renders ∞ when prepaid is UNLIMITED_RESERVED but reserved is positive', async () => { + MockApiClient.addMockResponse({ + url: `/customers/${organization.slug}/history/`, + method: 'GET', + body: [ + BillingHistoryFixture({ + categories: { + errors: MetricHistoryFixture({ + usage: 9999, + reserved: 1000, + prepaid: UNLIMITED_RESERVED, + }), + transactions: MetricHistoryFixture({ + category: DataCategory.TRANSACTIONS, + reserved: 1000, + prepaid: 1000, + }), + attachments: MetricHistoryFixture({ + category: DataCategory.ATTACHMENTS, + reserved: 1, + prepaid: 1, + }), + }, + }), + ], + }); + + const subscription = SubscriptionFixture({ + plan: 'am1_f', + planTier: PlanTier.AM1, + organization, + }); + SubscriptionStore.set(organization.slug, subscription); + + render(, {organization}); + + const errorsRow = await screen.findByRole('row', {name: /^Errors/}); + expect(within(errorsRow).getByRole('cell', {name: UNLIMITED})).toBeInTheDocument(); + expect(screen.queryByText('>100%')).not.toBeInTheDocument(); + }); + it('overage is shown as >100%', async () => { MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/history/`, @@ -913,3 +997,36 @@ describe('Subscription > UsageHistory', () => { expect(await screen.findByText('>100%')).toBeInTheDocument(); }); }); + +describe('usagePercentage', () => { + it('returns 0% when prepaid is null', () => { + expect(usagePercentage(0, null)).toBe('0%'); + expect(usagePercentage(100, null)).toBe('0%'); + }); + + it('returns 0% when prepaid is 0', () => { + expect(usagePercentage(0, 0)).toBe('0%'); + expect(usagePercentage(50, 0)).toBe('0%'); + }); + + it('returns UNLIMITED when prepaid equals UNLIMITED_RESERVED', () => { + expect(usagePercentage(0, UNLIMITED_RESERVED)).toBe(UNLIMITED); + expect(usagePercentage(1_000_000, UNLIMITED_RESERVED)).toBe(UNLIMITED); + }); + + it('returns >100% when usage exceeds prepaid', () => { + expect(usagePercentage(1001, 1000)).toBe('>100%'); + }); + + it('returns formatted percentage when usage is below prepaid', () => { + expect(usagePercentage(500, 1000)).toBe('50%'); + }); + + it('returns 0% when usage is 0 and prepaid is positive', () => { + expect(usagePercentage(0, 1000)).toBe('0%'); + }); + + it('returns 100% when usage equals prepaid', () => { + expect(usagePercentage(1000, 1000)).toBe('100%'); + }); +}); diff --git a/static/gsApp/views/subscriptionPage/usageHistory.tsx b/static/gsApp/views/subscriptionPage/usageHistory.tsx index 64c3b03c535b7a..3108590f26aa46 100644 --- a/static/gsApp/views/subscriptionPage/usageHistory.tsx +++ b/static/gsApp/views/subscriptionPage/usageHistory.tsx @@ -40,6 +40,7 @@ import { formatReservedWithUnits, formatUsageWithUnits, getSoftCapType, + isUnlimitedReserved, } from 'getsentry/utils/billing'; import {getPlanCategoryName, sortCategories} from 'getsentry/utils/dataCategory'; import {trackGetsentryAnalytics} from 'getsentry/utils/trackGetsentryAnalytics'; @@ -53,7 +54,10 @@ interface Props { subscription: Subscription; } -function usagePercentage(usage: number, prepaid: number | null): string { +export function usagePercentage(usage: number, prepaid: number | null): string { + if (isUnlimitedReserved(prepaid)) { + return UNLIMITED; + } if (prepaid === null || prepaid === 0) { return t('0%'); } @@ -345,13 +349,15 @@ function UsageHistoryRow({history}: RowProps) { {metricHistory.reserved === RESERVED_BUDGET_QUOTA ? 'N/A' - : usagePercentage( - convertUsageToReservedUnit( - metricHistory.usage, - metricHistory.category - ), - metricHistory.prepaid - )} + : isUnlimitedReserved(metricHistory.reserved) + ? UNLIMITED + : usagePercentage( + convertUsageToReservedUnit( + metricHistory.usage, + metricHistory.category + ), + metricHistory.prepaid + )} ))}