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 : (