Skip to content
Merged
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
22 changes: 22 additions & 0 deletions static/gsApp/utils/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
42 changes: 42 additions & 0 deletions static/gsApp/views/amCheckout/cart.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Cart
activePlan={businessPlan}
formData={defaultFormData}
formDataForPreview={getFormDataForPreview(defaultFormData)}
organization={organization}
subscription={subscription}
onSuccess={jest.fn()}
/>
);

const ondemandItem = await screen.findByTestId('summary-item-ondemand-total');
expect(ondemandItem).toHaveTextContent('Pay-as-you-go usage');
expect(ondemandItem).toHaveTextContent('$50');
});
});
15 changes: 15 additions & 0 deletions static/gsApp/views/amCheckout/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
getCreditApplied,
getCredits,
getFees,
getOnDemandItems,
getPlanIcon,
getProductIcon,
getReservedBudgetCategoryForAddOn,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -557,6 +559,19 @@ function TotalSummary({
</Item>
);
})}
{onDemandItems.length > 0 && (
<Item data-test-id="summary-item-ondemand-total">
<ItemWithPrice
item={tct('[budgetTerm] usage', {
budgetTerm: displayBudgetName(activePlan, {title: true}),
})}
price={utils.displayPrice({
cents: onDemandItems.reduce((sum, item) => sum + item.amount, 0),
})}
shouldBoldItem={false}
/>
</Item>
)}
{!!creditApplied && (
<Item data-test-id="summary-item-credit_applied">
<ItemWithPrice
Expand Down
46 changes: 46 additions & 0 deletions static/gsApp/views/amCheckout/checkoutSuccess.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';
import {resetMockDate, setMockDate} from 'sentry-test/utils';

import {PreviewDataFixture} from 'getsentry/__fixtures__/previewData';
import {InvoiceItemType} from 'getsentry/types';
import CheckoutSuccess from 'getsentry/views/amCheckout/checkoutSuccess';

describe('CheckoutSuccess', () => {
Expand Down Expand Up @@ -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(
<CheckoutSuccess
invoice={invoiceWithOnDemand}
basePlan={bizPlan}
nextQueryParams={[]}
/>
);

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();
});
});
36 changes: 36 additions & 0 deletions static/gsApp/views/amCheckout/checkoutSuccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ import {
type PreviewInvoiceItem,
} from 'getsentry/types';
import {
displayBudgetName,
formatOnDemandDescription,
formatReservedWithUnits,
getCreditApplied,
getCredits,
getFees,
getOnDemandItems,
getPlanIcon,
getProductIcon,
} from 'getsentry/utils/billing';
Expand Down Expand Up @@ -66,6 +69,7 @@ interface ScheduledChangesProps extends ChangesProps {
interface ReceiptProps extends ChangesProps {
charges: Charge[];
dateCreated: string;
onDemandItems: Array<InvoiceItem | PreviewInvoiceItem>;
planItem: InvoiceItem;
}

Expand Down Expand Up @@ -299,6 +303,7 @@ function Receipt({
creditApplied,
credits,
fees,
onDemandItems,
products,
reservedVolume,
total,
Expand Down Expand Up @@ -403,6 +408,35 @@ function Receipt({
})}
</ReceiptSection>
)}
{onDemandItems.length > 0 && (
<ReceiptSection>
<ReceiptItem
rowItems={[
tct('[budgetTerm] usage', {
budgetTerm: displayBudgetName(plan, {title: true}),
}),
null,
]}
/>
{onDemandItems.map(item => {
const cleanDescription = formatOnDemandDescription(
item.description,
plan
);

return (
<ReceiptItem
key={item.type}
isSubItem={false}
rowItems={[
cleanDescription,
utils.displayPrice({cents: item.amount}),
]}
/>
);
})}
</ReceiptSection>
)}
{(creditApplied > 0 || credits.length + fees.length > 0) && (
<ReceiptSection>
{fees.map(item => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -602,6 +637,7 @@ function CheckoutSuccess({
{...commonChangesProps}
charges={invoice.charges}
planItem={planItem as InvoiceItem}
onDemandItems={onDemandItems}
dateCreated={invoice.dateCreated}
/>
) : effectiveToday ? null : (
Expand Down
Loading