import {OrganizationFixture} from 'sentry-fixture/organization';
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
import {
fireEvent,
render,
renderGlobalModal,
screen,
userEvent,
} from 'sentry-test/reactTestingLibrary';
import SubscriptionStore from 'getsentry/stores/subscriptionStore';
import {OnDemandBudgetMode, PlanTier, type Subscription} from 'getsentry/types';
import {OnDemandSettings} from './onDemandSettings';
describe('edit on-demand budget', () => {
const organization = OrganizationFixture({
features: ['ondemand-budgets'],
});
const onDemandOrg = OrganizationFixture({
features: ['ondemand-budgets'],
access: ['org:billing'],
});
beforeEach(() => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/monitor-count/`,
method: 'GET',
body: {enabledMonitorCount: 0, disabledMonitorCount: 0},
});
});
it('allows VC partner accounts to edit on-demand budget without payment source', function () {
const subscription = SubscriptionFixture({
plan: 'am3_business',
planTier: PlanTier.AM3,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
paymentSource: null,
partner: {
externalId: 'x123x',
name: 'VC Org',
partnership: {
id: 'VC',
displayName: 'VC',
supportNote: '',
},
isActive: true,
},
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 4500,
onDemandSpendUsed: 0,
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
render(, {
organization: onDemandOrg,
});
// Should show Edit button even without payment source
expect(screen.getByText('Edit')).toBeInTheDocument();
expect(screen.getByText('Pay-as-you-go Budget')).toBeInTheDocument();
expect(screen.getByText('$45')).toBeInTheDocument();
});
it('requires payment source for non-VC accounts', function () {
const subscription = SubscriptionFixture({
plan: 'am3_business',
planTier: PlanTier.AM3,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
paymentSource: null,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 4500,
onDemandSpendUsed: 0,
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
render(, {
organization: onDemandOrg,
});
// Should show Add Credit Card message
expect(screen.getByText('Add Credit Card')).toBeInTheDocument();
expect(
screen.getByText(
"To set a pay-as-you-go budget, you'll need a valid credit card on file."
)
).toBeInTheDocument();
});
it('switch from shared budget to per-category budget', async function () {
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/ondemand-budgets/`,
method: 'POST',
statusCode: 200,
body: {
enabled: true,
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
errorsBudget: 1000,
transactionsBudget: 2000,
attachmentsBudget: 3000,
budgets: {errors: 1000, transactions: 2000, attachments: 3000},
},
});
const subscription = SubscriptionFixture({
plan: 'am1_business',
planTier: PlanTier.AM1,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 4500,
onDemandSpendUsed: 0,
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
MockApiClient.addMockResponse({
url: `/subscriptions/${onDemandOrg.slug}/`,
method: 'GET',
statusCode: 200,
body: {
...subscription,
onDemandMaxSpend: 1000 + 2000 + 3000,
onDemandSpendUsed: 100 + 200 + 300,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
errorsBudget: 1000,
transactionsBudget: 2000,
attachmentsBudget: 3000,
budgets: {
errors: 1000,
transactions: 2000,
attachments: 3000,
monitorSeats: 4000,
},
errorSpendUsed: 100,
transactionSpendUsed: 200,
attachmentSpendUsed: 300,
usedSpends: {
errors: 100,
transactions: 200,
attachments: 300,
monitorSeats: 400,
},
},
},
});
const {rerender} = render(
,
{
organization: onDemandOrg,
}
);
const {waitForModalToHide} = renderGlobalModal();
expect(await screen.findByTestId('shared-budget-info')).toBeInTheDocument();
expect(screen.getByText('$45')).toBeInTheDocument();
await userEvent.click(screen.getByText('Edit'));
expect(await screen.findByText('Edit On-Demand Budgets')).toBeInTheDocument();
expect(screen.getByTestId('shared-budget-radio')).toBeChecked();
expect(screen.getByRole('textbox', {name: 'Shared max budget'})).toHaveValue('45');
// Select per-category budget strategy
await userEvent.click(screen.getByTestId('per-category-budget-radio'));
expect(screen.getByTestId('per-category-budget-radio')).toBeChecked();
expect(screen.getByTestId('shared-budget-radio')).not.toBeChecked();
expect(
screen.queryByRole('textbox', {name: 'Shared max budget'})
).not.toBeInTheDocument();
// Shared budget should split 50:50 between transactions and errors (whole dollars, remainder added to errors)
expect(screen.getByRole('textbox', {name: 'Errors budget'})).toHaveValue('23');
expect(screen.getByRole('textbox', {name: 'Transactions budget'})).toHaveValue('22');
expect(screen.getByRole('textbox', {name: 'Attachments budget'})).toHaveValue('0');
expect(screen.getByRole('textbox', {name: 'Cron monitors budget'})).toHaveValue('0');
fireEvent.change(screen.getByRole('textbox', {name: 'Errors budget'}), {
target: {value: '10'},
});
fireEvent.change(screen.getByRole('textbox', {name: 'Transactions budget'}), {
target: {value: '20'},
});
fireEvent.change(screen.getByRole('textbox', {name: 'Attachments budget'}), {
target: {value: '30'},
});
fireEvent.change(screen.getByRole('textbox', {name: 'Cron monitors budget'}), {
target: {value: '40'},
});
await userEvent.click(screen.getByLabelText('Save'));
await waitForModalToHide();
const updatedSubscription = await new Promise(resolve => {
SubscriptionStore.get(organization.slug, resolve);
});
expect(updatedSubscription.onDemandMaxSpend).toBe(1000 + 2000 + 3000);
rerender(
);
expect(await screen.findByText('$10')).toBeInTheDocument();
expect(screen.getByText('$20')).toBeInTheDocument();
expect(screen.getByText('$30')).toBeInTheDocument();
expect(screen.getByText('$40')).toBeInTheDocument();
expect(screen.getByTestId('per-category-budget-info')).toBeInTheDocument();
});
it('switch from shared budget to per-category budget with sub-$1.00 budget', async function () {
MockApiClient.addMockResponse({
url: `/customers/${organization.slug}/ondemand-budgets/`,
method: 'POST',
statusCode: 200,
body: {
enabled: true,
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
errorsBudget: 100,
transactionsBudget: 0,
attachmentsBudget: 3000,
budgets: {
errors: 100,
transactions: 0,
attachments: 2400,
replays: 300,
monitorSeats: 200,
},
},
});
const subscription = SubscriptionFixture({
plan: 'am1_business',
planTier: PlanTier.AM1,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 76,
onDemandSpendUsed: 0,
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
MockApiClient.addMockResponse({
url: `/subscriptions/${onDemandOrg.slug}/`,
method: 'GET',
statusCode: 200,
body: {
...subscription,
onDemandMaxSpend: 100 + 3000,
onDemandSpendUsed: 76,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
errorsBudget: 100,
transactionsBudget: 0,
attachmentsBudget: 3000,
budgets: {
errors: 100,
transactions: 0,
attachments: 2400,
replays: 300,
monitorSeats: 200,
},
errorSpendUsed: 76,
transactionSpendUsed: 0,
attachmentSpendUsed: 0,
usedSpends: {
errors: 76,
transactions: 0,
attachments: 0,
replays: 0,
monitorSeats: 100,
},
},
},
});
const {rerender} = render(
,
{
organization: onDemandOrg,
}
);
const {waitForModalToHide} = renderGlobalModal();
expect(await screen.findByTestId('shared-budget-info')).toBeInTheDocument();
expect(screen.getByText('$0.76')).toBeInTheDocument();
await userEvent.click(screen.getByText('Edit'));
expect(await screen.findByText('Edit On-Demand Budgets')).toBeInTheDocument();
expect(screen.getByTestId('shared-budget-radio')).toBeChecked();
expect(screen.getByRole('textbox', {name: 'Shared max budget'})).toHaveValue('0.76');
// Select per-category budget strategy
await userEvent.click(screen.getByTestId('per-category-budget-radio'));
expect(screen.getByTestId('per-category-budget-radio')).toBeChecked();
expect(screen.getByTestId('shared-budget-radio')).not.toBeChecked();
expect(
screen.queryByRole('textbox', {name: 'Shared max budget'})
).not.toBeInTheDocument();
// Shared budget should split 50:50 between transactions and errors (whole dollars, remainder added to errors)
expect(screen.getByRole('textbox', {name: 'Errors budget'})).toHaveValue('1');
expect(screen.getByRole('textbox', {name: 'Transactions budget'})).toHaveValue('0');
expect(screen.getByRole('textbox', {name: 'Attachments budget'})).toHaveValue('0');
expect(screen.getByRole('textbox', {name: 'Cron monitors budget'})).toHaveValue('0');
fireEvent.change(screen.getByRole('textbox', {name: 'Attachments budget'}), {
target: {value: '30'},
});
await userEvent.click(screen.getByLabelText('Save'));
await waitForModalToHide();
const updatedSubscription = await new Promise(resolve => {
SubscriptionStore.get(onDemandOrg.slug, resolve);
});
expect(updatedSubscription.onDemandMaxSpend).toBe(3100);
rerender(
);
expect(await screen.findByText('$3')).toBeInTheDocument();
expect(screen.getByText('$24')).toBeInTheDocument();
expect(screen.getByText('$2')).toBeInTheDocument();
expect(screen.getByTestId('per-category-budget-info')).toBeInTheDocument();
});
it('switch from per-category budget to shared budget', async function () {
MockApiClient.addMockResponse({
url: `/customers/${onDemandOrg.slug}/ondemand-budgets/`,
method: 'POST',
statusCode: 200,
body: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 4200,
},
});
const subscription = SubscriptionFixture({
plan: 'am1_business',
planTier: PlanTier.AM1,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
errorsBudget: 1000,
transactionsBudget: 2000,
attachmentsBudget: 3000,
replaysBudget: 0,
budgets: {
errors: 1000,
transactions: 2000,
attachments: 3000,
replays: 0,
monitorSeats: 5000,
},
attachmentSpendUsed: 0,
errorSpendUsed: 0,
transactionSpendUsed: 0,
usedSpends: {},
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
MockApiClient.addMockResponse({
url: `/subscriptions/${onDemandOrg.slug}/`,
method: 'GET',
statusCode: 200,
body: {
...subscription,
onDemandMaxSpend: 4200,
onDemandSpendUsed: 2022,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 4200,
onDemandSpendUsed: 2022,
},
},
});
const {rerender} = render(
,
{
organization: onDemandOrg,
}
);
const {waitForModalToHide} = renderGlobalModal();
expect(await screen.findByTestId('per-category-budget-info')).toBeInTheDocument();
expect(screen.getByText('$10')).toBeInTheDocument();
expect(screen.getByText('$20')).toBeInTheDocument();
expect(screen.getByText('$30')).toBeInTheDocument();
await userEvent.click(screen.getByText('Edit'));
expect(await screen.findByText('Edit On-Demand Budgets')).toBeInTheDocument();
expect(screen.getByTestId('per-category-budget-radio')).toBeChecked();
expect(screen.getByRole('textbox', {name: 'Errors budget'})).toHaveValue('10');
expect(screen.getByRole('textbox', {name: 'Transactions budget'})).toHaveValue('20');
expect(screen.getByRole('textbox', {name: 'Attachments budget'})).toHaveValue('30');
expect(screen.getByRole('textbox', {name: 'Cron monitors budget'})).toHaveValue('50');
// Select shared budget strategy
await userEvent.click(screen.getByTestId('shared-budget-radio'));
expect(screen.getByTestId('shared-budget-radio')).toBeChecked();
expect(screen.getByTestId('per-category-budget-radio')).not.toBeChecked();
expect(
screen.queryByRole('textbox', {name: 'Errors budget'})
).not.toBeInTheDocument();
expect(
screen.queryByRole('textbox', {name: 'Transactions budget'})
).not.toBeInTheDocument();
expect(
screen.queryByRole('textbox', {name: 'Attachments budget'})
).not.toBeInTheDocument();
expect(
screen.queryByRole('textbox', {name: 'Cron Monitors budget'})
).not.toBeInTheDocument();
// Default shared budget should be total of the current per-category budget.
expect(screen.getByRole('textbox', {name: 'Shared max budget'})).toHaveValue('110');
fireEvent.change(screen.getByRole('textbox', {name: 'Shared max budget'}), {
target: {value: '42'},
});
await userEvent.click(screen.getByLabelText('Save'));
await waitForModalToHide();
const updatedSubscription = await new Promise(resolve => {
SubscriptionStore.get(onDemandOrg.slug, resolve);
});
expect(updatedSubscription.onDemandMaxSpend).toBe(4200);
rerender(
);
expect(await screen.findByText('$42')).toBeInTheDocument();
expect(screen.getByTestId('shared-budget-info')).toBeInTheDocument();
});
it('disable shared on-demand budget', async function () {
MockApiClient.addMockResponse({
url: `/customers/${onDemandOrg.slug}/ondemand-budgets/`,
method: 'POST',
statusCode: 200,
body: {
enabled: false,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 0,
},
});
const subscription = SubscriptionFixture({
plan: 'am1_business',
planTier: PlanTier.AM1,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 4200,
onDemandSpendUsed: 0,
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
MockApiClient.addMockResponse({
url: `/subscriptions/${onDemandOrg.slug}/`,
method: 'GET',
statusCode: 200,
body: {
...subscription,
onDemandMaxSpend: 0,
onDemandSpendUsed: 0,
onDemandBudgets: {
enabled: false,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 0,
onDemandSpendUsed: 0,
},
},
});
const {rerender} = render(
,
{
organization: onDemandOrg,
}
);
const {waitForModalToHide} = renderGlobalModal();
expect(await screen.findByTestId('shared-budget-info')).toBeInTheDocument();
expect(screen.getByText('$42')).toBeInTheDocument();
await userEvent.click(screen.getByText('Edit'));
expect(await screen.findByText('Edit On-Demand Budgets')).toBeInTheDocument();
expect(screen.getByTestId('shared-budget-radio')).toBeChecked();
expect(screen.getByRole('textbox', {name: 'Shared max budget'})).toHaveValue('42');
// Disable on-demand budgets
fireEvent.change(screen.getByRole('textbox', {name: 'Shared max budget'}), {
target: {value: '0'},
});
await userEvent.click(screen.getByLabelText('Save'));
await waitForModalToHide();
const updatedSubscription = await new Promise(resolve => {
SubscriptionStore.get(onDemandOrg.slug, resolve);
});
expect(updatedSubscription.onDemandMaxSpend).toBe(0);
rerender(
);
expect(await screen.findByText('Set Up On-Demand')).toBeInTheDocument();
expect(screen.queryByTestId('per-category-budget-info')).not.toBeInTheDocument();
expect(screen.queryByTestId('shared-budget-info')).not.toBeInTheDocument();
});
it('disable per-category on-demand budget', async function () {
MockApiClient.addMockResponse({
url: `/customers/${onDemandOrg.slug}/ondemand-budgets/`,
method: 'POST',
statusCode: 200,
body: {
enabled: false,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 0,
},
});
const subscription = SubscriptionFixture({
plan: 'am1_business',
planTier: PlanTier.AM1,
isFree: false,
isTrial: false,
supportsOnDemand: true,
organization: onDemandOrg,
onDemandBudgets: {
enabled: true,
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
errorsBudget: 1000,
transactionsBudget: 2000,
attachmentsBudget: 3000,
replaysBudget: 0,
budgets: {
errors: 1000,
transactions: 2000,
attachments: 3000,
replays: 0,
monitorSeats: 5000,
},
attachmentSpendUsed: 0,
errorSpendUsed: 0,
transactionSpendUsed: 0,
usedSpends: {},
},
});
SubscriptionStore.set(onDemandOrg.slug, subscription);
MockApiClient.addMockResponse({
url: `/subscriptions/${onDemandOrg.slug}/`,
method: 'GET',
statusCode: 200,
body: {
...subscription,
onDemandMaxSpend: 0,
onDemandSpendUsed: 0,
onDemandBudgets: {
enabled: false,
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 0,
onDemandSpendUsed: 0,
},
},
});
const {rerender} = render(
,
{
organization: onDemandOrg,
}
);
const {waitForModalToHide} = renderGlobalModal();
expect(await screen.findByTestId('per-category-budget-info')).toBeInTheDocument();
expect(screen.getByText('$10')).toBeInTheDocument();
expect(screen.getByText('$20')).toBeInTheDocument();
expect(screen.getByText('$30')).toBeInTheDocument();
await userEvent.click(screen.getByText('Edit'));
expect(await screen.findByText('Edit On-Demand Budgets')).toBeInTheDocument();
expect(screen.getByTestId('per-category-budget-radio')).toBeChecked();
expect(screen.getByRole('textbox', {name: 'Errors budget'})).toHaveValue('10');
expect(screen.getByRole('textbox', {name: 'Transactions budget'})).toHaveValue('20');
expect(screen.getByRole('textbox', {name: 'Attachments budget'})).toHaveValue('30');
expect(screen.getByRole('textbox', {name: 'Cron monitors budget'})).toHaveValue('50');
// Disable on-demand budgets
fireEvent.change(screen.getByRole('textbox', {name: 'Errors budget'}), {
target: {value: '0'},
});
fireEvent.change(screen.getByRole('textbox', {name: 'Transactions budget'}), {
target: {value: '0'},
});
fireEvent.change(screen.getByRole('textbox', {name: 'Attachments budget'}), {
target: {value: '0'},
});
fireEvent.change(screen.getByRole('textbox', {name: 'Cron monitors budget'}), {
target: {value: '0'},
});
await userEvent.click(screen.getByLabelText('Save'));
await waitForModalToHide();
const updatedSubscription = await new Promise(resolve => {
SubscriptionStore.get(onDemandOrg.slug, resolve);
});
expect(updatedSubscription.onDemandMaxSpend).toBe(0);
rerender(
);
expect(await screen.findByText('Set Up On-Demand')).toBeInTheDocument();
expect(screen.queryByTestId('per-category-budget-info')).not.toBeInTheDocument();
expect(screen.queryByTestId('shared-budget-info')).not.toBeInTheDocument();
});
});