diff --git a/static/gsApp/utils/billing.tsx b/static/gsApp/utils/billing.tsx index a3f405765b5fe9..13c3f2ebc4ca38 100644 --- a/static/gsApp/utils/billing.tsx +++ b/static/gsApp/utils/billing.tsx @@ -833,3 +833,25 @@ export function getFees({ (item.type === InvoiceItemType.BALANCE_CHANGE && item.amount > 0) ); } + +/** + * Returns ondemand invoice items from the invoice or preview data. + */ +export function getOnDemandItems({ + invoiceItems, +}: { + invoiceItems: InvoiceItem[] | PreviewInvoiceItem[]; +}) { + return invoiceItems.filter(item => item.type.startsWith('ondemand')); +} + +/** + * Removes the budget term (pay-as-you-go/on-demand) from an ondemand item description. + */ +export function formatOnDemandDescription( + description: string, + plan?: Plan | null +): string { + const budgetTerm = displayBudgetName(plan, {title: false}).toLowerCase(); + return description.replace(new RegExp(`\\s*${budgetTerm}\\s*`, 'gi'), ' ').trim(); +} diff --git a/static/gsApp/views/amCheckout/cart.spec.tsx b/static/gsApp/views/amCheckout/cart.spec.tsx index 000003123f0bf1..557ef7aeb43b96 100644 --- a/static/gsApp/views/amCheckout/cart.spec.tsx +++ b/static/gsApp/views/amCheckout/cart.spec.tsx @@ -596,4 +596,46 @@ describe('Cart', () => { await userEvent.click(planSummaryButton); expect(within(planSummary).queryByText('Business Plan')).not.toBeInTheDocument(); }); + + it('renders ondemand usage as a single summed line item', async () => { + MockApiClient.addMockResponse({ + url: `/customers/${organization.slug}/subscription/preview/`, + method: 'GET', + body: { + effectiveAt: new Date(MOCK_TODAY).toISOString(), + billedAmount: 150_00, + proratedAmount: 150_00, + creditApplied: 0, + invoiceItems: [ + { + amount: 25_00, + description: '500 pay-as-you-go replays', + data: {quantity: 500}, + type: InvoiceItemType.ONDEMAND_REPLAYS, + }, + { + amount: 25_00, + description: '50 GB pay-as-you-go attachments', + data: {quantity: 53687091200}, + type: InvoiceItemType.ONDEMAND_ATTACHMENTS, + }, + ], + }, + }); + + render( + + ); + + const ondemandItem = await screen.findByTestId('summary-item-ondemand-total'); + expect(ondemandItem).toHaveTextContent('Pay-as-you-go usage'); + expect(ondemandItem).toHaveTextContent('$50'); + }); }); diff --git a/static/gsApp/views/amCheckout/cart.tsx b/static/gsApp/views/amCheckout/cart.tsx index 304151098903a6..4d1e31c700ee20 100644 --- a/static/gsApp/views/amCheckout/cart.tsx +++ b/static/gsApp/views/amCheckout/cart.tsx @@ -34,6 +34,7 @@ import { getCreditApplied, getCredits, getFees, + getOnDemandItems, getPlanIcon, getProductIcon, getReservedBudgetCategoryForAddOn, @@ -526,6 +527,7 @@ function TotalSummary({ }; const fees = getFees({invoiceItems: previewData?.invoiceItems ?? []}); + const onDemandItems = getOnDemandItems({invoiceItems: previewData?.invoiceItems ?? []}); const credits = getCredits({invoiceItems: previewData?.invoiceItems ?? []}); const creditApplied = getCreditApplied({ creditApplied: previewData?.creditApplied ?? 0, @@ -557,6 +559,19 @@ function TotalSummary({ ); })} + {onDemandItems.length > 0 && ( + + sum + item.amount, 0), + })} + shouldBoldItem={false} + /> + + )} {!!creditApplied && ( { @@ -67,4 +68,49 @@ describe('CheckoutSuccess', () => { expect(screen.queryByTestId('scheduled-changes')).not.toBeInTheDocument(); expect(screen.queryByTestId('receipt')).not.toBeInTheDocument(); }); + + it('renders ondemand items in receipt', async () => { + const invoiceWithOnDemand = InvoiceFixture({ + items: [ + { + type: InvoiceItemType.SUBSCRIPTION, + description: 'Subscription to Team Plan', + amount: 31200, + data: {quantity: null}, + periodStart: '2025-01-01T00:00:00Z', + periodEnd: '2026-01-01T00:00:00Z', + }, + { + type: InvoiceItemType.ONDEMAND_ERRORS, + description: '4,901,066 pay-as-you-go errors', + amount: 94022, + data: {quantity: 4901066}, + periodStart: '2025-01-01T00:00:00Z', + periodEnd: '2026-01-01T00:00:00Z', + }, + { + type: InvoiceItemType.ONDEMAND_MONITOR_SEATS, + description: '2 pay-as-you-go cron monitors', + amount: 156, + data: {quantity: 2}, + periodStart: '2025-01-01T00:00:00Z', + periodEnd: '2026-01-01T00:00:00Z', + }, + ], + }); + + render( + + ); + + expect(await screen.findByText('Pay-as-you-go usage')).toBeInTheDocument(); + expect(screen.getByText('4,901,066 errors')).toBeInTheDocument(); + expect(screen.getByText('2 cron monitors')).toBeInTheDocument(); + expect(screen.getByText('$940.22')).toBeInTheDocument(); + expect(screen.getByText('$1.56')).toBeInTheDocument(); + }); }); diff --git a/static/gsApp/views/amCheckout/checkoutSuccess.tsx b/static/gsApp/views/amCheckout/checkoutSuccess.tsx index 2ed52c540b528c..a3ae4d71211d73 100644 --- a/static/gsApp/views/amCheckout/checkoutSuccess.tsx +++ b/static/gsApp/views/amCheckout/checkoutSuccess.tsx @@ -27,10 +27,13 @@ import { type PreviewInvoiceItem, } from 'getsentry/types'; import { + displayBudgetName, + formatOnDemandDescription, formatReservedWithUnits, getCreditApplied, getCredits, getFees, + getOnDemandItems, getPlanIcon, getProductIcon, } from 'getsentry/utils/billing'; @@ -66,6 +69,7 @@ interface ScheduledChangesProps extends ChangesProps { interface ReceiptProps extends ChangesProps { charges: Charge[]; dateCreated: string; + onDemandItems: Array; planItem: InvoiceItem; } @@ -299,6 +303,7 @@ function Receipt({ creditApplied, credits, fees, + onDemandItems, products, reservedVolume, total, @@ -403,6 +408,35 @@ function Receipt({ })} )} + {onDemandItems.length > 0 && ( + + + {onDemandItems.map(item => { + const cleanDescription = formatOnDemandDescription( + item.description, + plan + ); + + return ( + + ); + })} + + )} {(creditApplied > 0 || credits.length + fees.length > 0) && ( {fees.map(item => { @@ -486,6 +520,7 @@ function CheckoutSuccess({ const products = invoiceItems.filter( item => item.type === InvoiceItemType.RESERVED_SEER_BUDGET ); + const onDemandItems = getOnDemandItems({invoiceItems}); const fees = getFees({invoiceItems}); const credits = getCredits({invoiceItems}); // TODO(isabella): PreviewData never has the InvoiceItemType.BALANCE_CHANGE type @@ -602,6 +637,7 @@ function CheckoutSuccess({ {...commonChangesProps} charges={invoice.charges} planItem={planItem as InvoiceItem} + onDemandItems={onDemandItems} dateCreated={invoice.dateCreated} /> ) : effectiveToday ? null : (