import moment from 'moment-timezone'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture'; import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig'; import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory'; import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; import { act, render, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; import type {Subscription as SubscriptionType} from 'getsentry/types'; import {OnDemandBudgetMode, PlanTier} from 'getsentry/types'; import AMCheckout from 'getsentry/views/amCheckout'; import {getCheckoutAPIData} from 'getsentry/views/amCheckout/utils'; import {hasOnDemandBudgetsFeature} from 'getsentry/views/onDemandBudgets/utils'; describe('AM1 Checkout', function () { let mockResponse: any; const api = new MockApiClient(); const organization = OrganizationFixture({features: []}); const subscription = SubscriptionFixture({organization}); const params = {}; beforeEach(function () { SubscriptionStore.set(organization.slug, subscription); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/subscriptions/${organization.slug}/`, method: 'GET', body: {}, }); mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM2), }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/promotions/trigger-check/`, method: 'POST', body: {}, }); MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/plan-migrations/?applied=0`, method: 'GET', body: {}, }); }); it('renders', async function () { render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByTestId('checkout-steps')).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument(); await waitFor(() => { expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: 'am1'}, }) ); }); }); it('can skip to step and continue', async function () { render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); await userEvent.click(screen.getByText('Reserved Volumes')); await userEvent.click(screen.getByRole('button', {name: 'Continue'})); // Both steps are complete expect( within(screen.getByTestId('header-choose-your-plan')).getByTestId('icon-check-mark') ).toBeInTheDocument(); expect( within(screen.getByTestId('header-reserved-volumes')).getByTestId('icon-check-mark') ).toBeInTheDocument(); }); it('renders cancel subscription button', async function () { const sub: SubscriptionType = {...subscription, canCancel: true}; SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Cancel Subscription'})).toBeInTheDocument(); }); it('renders pending cancellation button', async function () { const sub: SubscriptionType = { ...subscription, canCancel: true, cancelAtPeriodEnd: true, }; SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(await screen.findByText('Pending Cancellation')).toBeInTheDocument(); }); it('does not renders cancel subscription button if cannot cancel', async function () { render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect( screen.queryByRole('button', {name: 'Cancel Subscription'}) ).not.toBeInTheDocument(); }); it('renders annual terms for annual plan', async function () { const sub: SubscriptionType = { ...subscription, plan: 'am1_team_auf', contractInterval: 'annual', billingInterval: 'annual', }; SubscriptionStore.set(organization.slug, sub); const {container} = render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(container).toHaveTextContent( 'Annual subscriptions require a one-year non-cancellable commitment' ); }); it('does not render annual terms for monthly plan', async function () { const sub = {...subscription}; SubscriptionStore.set(organization.slug, sub); const {container} = render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(container).not.toHaveTextContent( 'Annual subscriptions require a one-year non-cancellable commitment' ); }); it('renders default plan data', async function () { render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '50000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '500' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue(''); }); it('prefills with am1 team subscription data', async function () { const sub: SubscriptionType = SubscriptionFixture({ organization, plan: 'am1_business', planTier: 'am1', categories: { errors: MetricHistoryFixture({reserved: 200000}), transactions: MetricHistoryFixture({reserved: 250000}), replays: MetricHistoryFixture({reserved: 10_000}), attachments: MetricHistoryFixture({reserved: 25}), monitorSeats: MetricHistoryFixture({reserved: 1}), }, onDemandMaxSpend: 10000, }); SubscriptionStore.set(organization.slug, sub); render( , { organization, } ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '200000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '250000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '10000' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '25' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('100'); }); it('prefills with am1 business subscription data', async function () { const sub = SubscriptionFixture({ organization, plan: 'am1_business', planTier: 'am1', categories: { errors: MetricHistoryFixture({reserved: 50000}), transactions: MetricHistoryFixture({reserved: 250000}), replays: MetricHistoryFixture({reserved: 500}), attachments: MetricHistoryFixture({reserved: 50}), monitorSeats: MetricHistoryFixture({reserved: 1}), }, onDemandMaxSpend: 10000, }); SubscriptionStore.set(organization.slug, sub); render( , { organization, } ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '50000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '250000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '500' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '50' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('100'); }); it('prefills with mm2 team subscription data', async function () { const sub = SubscriptionFixture({ organization, plan: 'mm2_b_100k', planTier: 'mm2', categories: {errors: MetricHistoryFixture({reserved: 100000})}, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , { organization, } ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('prefills with mm2 biz subscription data', async function () { const sub = SubscriptionFixture({ organization, plan: 'mm2_a_100k', planTier: 'mm2', categories: {errors: MetricHistoryFixture({reserved: 100_000})}, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('prefills with s1 subscription data', async function () { const sub = SubscriptionFixture({ organization, plan: 's1', planTier: 'mm1', categories: {errors: MetricHistoryFixture({reserved: 100000})}, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , { organization, } ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('prefills with l1 subscription data', async function () { const sub = SubscriptionFixture({ organization, plan: 'l1', planTier: 'mm1', categories: {errors: MetricHistoryFixture({reserved: 100000})}, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , { organization, } ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('handles subscription with unlimited ondemand', async function () { const sub = {...subscription, onDemandMaxSpend: -1}; SubscriptionStore.set(organization.slug, sub); render( , { organization, } ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue(''); }); }); describe('AM2 Checkout', function () { let mockResponse: any; const api = new MockApiClient(); const organization = OrganizationFixture(); const subscription = SubscriptionFixture({organization}); const params = {}; beforeEach(function () { SubscriptionStore.set(organization.slug, subscription); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/subscriptions/${organization.slug}/`, method: 'GET', body: {}, }); mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM2), }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/promotions/trigger-check/`, method: 'POST', }); MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/plan-migrations/?applied=0`, method: 'GET', body: {}, }); }); it('renders for am1 team plan', async function () { const sub = SubscriptionFixture({organization, plan: 'am1_team'}); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByText('Choose Your Plan')).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument(); expect(screen.getByText('Cross-project visibility')).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Team'})).toBeInTheDocument(); expect(screen.getByText('Unlimited members')).toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM2}, }) ); }); it('renders for am2 free plan', async function () { const sub = SubscriptionFixture({organization, plan: 'am2_f'}); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByText('Choose Your Plan')).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeInTheDocument(); expect(screen.getByText('Cross-project visibility')).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Team'})).toBeInTheDocument(); expect(screen.getByText('Unlimited members')).toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM2}, }) ); }); it('prefills subscription data based on price with same plan type', async function () { const sub = SubscriptionFixture({ organization, plan: 'am1_business', planTier: 'am1', categories: { errors: MetricHistoryFixture({reserved: 50_000}), transactions: MetricHistoryFixture({reserved: 20_000_000}), replays: MetricHistoryFixture({reserved: 500}), attachments: MetricHistoryFixture({reserved: 1}), monitorSeats: MetricHistoryFixture({reserved: 1}), }, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '50000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '35000000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '500' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('prefills subscription data based on price with annual plan', async function () { const sub = SubscriptionFixture({ organization, plan: 'am1_business_auf', planTier: 'am1', categories: { errors: MetricHistoryFixture({reserved: 100_000}), transactions: MetricHistoryFixture({reserved: 20_000_000}), replays: MetricHistoryFixture({reserved: 500}), attachments: MetricHistoryFixture({reserved: 1}), monitorSeats: MetricHistoryFixture({reserved: 1}), }, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '35000000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '500' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('prefills subscription data based on events with different plan type', async function () { const sub = SubscriptionFixture({ organization, plan: 'am1_team', planTier: 'am1', categories: { errors: MetricHistoryFixture({reserved: 100_000}), transactions: MetricHistoryFixture({reserved: 20_000_000}), attachments: MetricHistoryFixture({reserved: 1}), replays: MetricHistoryFixture({reserved: 500}), monitorSeats: MetricHistoryFixture({reserved: 1}), }, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByText('Reserved Volumes')); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '20000000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '500' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); it('displays 40% india promotion', async function () { const promotionData = { completedPromotions: [ { promotion: { name: 'Test Promotion', slug: 'test_promotion', timeLimit: null, startDate: null, endDate: null, showDiscountInfo: true, discountInfo: { amount: 4000, billingInterval: 'monthly', billingPeriods: 3, creditCategory: 'subscription', discountType: 'percentPoints', disclaimerText: "*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today", durationText: 'First three months', }, }, }, ], }; MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/promotions/trigger-check/`, method: 'POST', body: promotionData, }); render( , {organization} ); await screen.findByText('Choose Your Plan'); expect( screen.getByText( "*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today" ) ).toBeInTheDocument(); expect(screen.getByText('First three months 40% off')).toBeInTheDocument(); expect(screen.getAllByText('53.40')).toHaveLength(2); }); it('skips step 1 for business plan in same tier', async function () { const am2BizSubscription = SubscriptionFixture({ organization, plan: 'am2_business', planTier: 'am2', categories: { errors: MetricHistoryFixture({reserved: 100_000}), transactions: MetricHistoryFixture({reserved: 20_000_000}), attachments: MetricHistoryFixture({reserved: 1}), monitorSeats: MetricHistoryFixture({reserved: 1}), profileDuration: MetricHistoryFixture({reserved: 1}), replays: MetricHistoryFixture({reserved: 10_000}), }, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, am2BizSubscription); render( , {organization} ); await screen.findByText('Choose Your Plan'); expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument(); expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument(); }); it('test business bundle standard checkout', async function () { const am2BizSubscription = SubscriptionFixture({ organization, plan: 'am2_business_bundle', planTier: 'am2', categories: { errors: MetricHistoryFixture({reserved: 100_000}), transactions: MetricHistoryFixture({reserved: 20_000_000}), attachments: MetricHistoryFixture({reserved: 1}), monitorSeats: MetricHistoryFixture({reserved: 1}), profileDuration: MetricHistoryFixture({reserved: 1}), replays: MetricHistoryFixture({reserved: 10_000}), }, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, am2BizSubscription); render( , {organization} ); // wait for page load await screen.findByText('Choose Your Plan'); // "Choose Your Plan" should be skipped and "Reserved Volumes" should be visible // This is existing behavior to skip "Choose Your Plan" step for existing business customers expect(screen.queryByTestId('body-choose-your-plan')).not.toBeInTheDocument(); expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument(); // Click on "Choose Your Plan" and verify that Business is selected await userEvent.click(screen.getByText('Choose Your Plan')); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); }); it('handles missing categories in subscription.categories', async function () { /** * In this test, we create a subscription where some categories are missing from * `subscription.categories`. We then verify that the component renders correctly * without throwing errors, and that the missing categories default to a reserved * value of 0. */ const sub = SubscriptionFixture({ organization, plan: 'am2_business', planTier: 'am2', categories: { // Intentionally omitting 'transactions' and 'replays' categories errors: MetricHistoryFixture({reserved: 100_000}), attachments: MetricHistoryFixture({reserved: 1}), monitorSeats: MetricHistoryFixture({reserved: 1}), }, onDemandMaxSpend: 2000, }); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); // Verify that the component renders without errors expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument(); // Open 'Reserved Volumes' section await userEvent.click(screen.getByText('Reserved Volumes')); // Check that missing categories default to 0 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); // For missing 'Performance units', should default to 100,000 units expect(screen.getByRole('slider', {name: 'Performance units'})).toHaveAttribute( 'aria-valuetext', '100000' ); // For missing 'Replays', should default to 500 expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '500' ); // Check that 'Attachments' category is correctly set expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); // Open 'On-Demand Max Spend' section await userEvent.click(screen.getByText('On-Demand Max Spend')); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toHaveValue('20'); }); }); describe('AM3 Checkout', function () { const api = new MockApiClient(); const organization = OrganizationFixture({ features: ['ondemand-budgets', 'am3-billing'], }); const params = {}; beforeEach(function () { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: `/subscriptions/${organization.slug}/`, method: 'GET', body: {}, }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/promotions/trigger-check/`, method: 'POST', }); MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/plan-migrations/?applied=0`, method: 'GET', body: {}, }); }); it('renders for new customers (AM3 free plan)', async function () { const sub = SubscriptionFixture({ organization, plan: 'am3_f', planTier: PlanTier.AM3, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); render( , {organization} ); expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM3}, }) ); }); it('renders for customers migrating from partner billing', async function () { organization.features.push('partner-billing-migration'); const contractPeriodEnd = moment(); const sub = SubscriptionFixture({ organization, contractPeriodEnd: contractPeriodEnd.toString(), plan: 'am2_sponsored_team_auf', planTier: PlanTier.AM2, isSponsored: true, partner: { isActive: true, externalId: 'yuh', partnership: { id: 'FOO', displayName: 'FOO', supportNote: '', }, name: '', }, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); render( , {organization} ); expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument(); expect( screen.getByText( 'Your promotional plan with FOO ends on ' + contractPeriodEnd.format('ll') + '.' ) ).toBeInTheDocument(); // 500 replays from sponsored plan becomes 50 on am3 expect(screen.getByText('50')).toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM3}, }) ); }); it('renders for self-serve partners', async function () { const contractPeriodEnd = moment(); const sub = SubscriptionFixture({ organization, contractPeriodEnd: contractPeriodEnd.toString(), plan: 'am3_f', planTier: PlanTier.AM3, isSelfServePartner: true, partner: { isActive: true, externalId: 'foo', partnership: { id: 'XX', displayName: 'BAR', supportNote: '', }, name: '', }, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); render( , {organization} ); expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument(); expect(await screen.findByText('Contract Term & Discounts')).toBeInTheDocument(); expect(screen.getByText('Review & Confirm')).toBeInTheDocument(); expect(screen.queryByText('Payment Method')).not.toBeInTheDocument(); expect(screen.queryByText('Billing Details')).not.toBeInTheDocument(); expect( screen.queryByText( 'Your promotional plan with BAR ends on ' + contractPeriodEnd.format('ll') + '.' ) ).not.toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM3}, }) ); }); it('renders for VC partners', async function () { organization.features.push('vc-marketplace-active-customer'); const contractPeriodEnd = moment(); const sub = SubscriptionFixture({ organization, contractPeriodEnd: contractPeriodEnd.toString(), plan: 'am3_f', planTier: PlanTier.AM3, isSelfServePartner: true, partner: { isActive: true, externalId: 'foo', partnership: { id: 'XX', displayName: 'XX', supportNote: '', }, name: '', }, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); render( , {organization} ); expect(await screen.findByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument(); expect(screen.getByText('Review & Confirm')).toBeInTheDocument(); expect(screen.queryByText('Payment Method')).not.toBeInTheDocument(); expect(screen.queryByText('Billing Details')).not.toBeInTheDocument(); expect(screen.queryByText('Contract Term & Discounts')).not.toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM3}, }) ); }); it('does not render for AM2 customers', async function () { const sub = SubscriptionFixture({ organization, plan: 'am2_f', planTier: PlanTier.AM2, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM2), }); render( , {organization} ); expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument(); expect(screen.queryByText('Set Your Pay-as-you-go Budget')).not.toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM2}, }) ); }); it('does not render for AM1 customers', async function () { const sub = SubscriptionFixture({ organization, plan: 'am1_f', planTier: PlanTier.AM1, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM1), }); render( , {organization} ); expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument(); expect(screen.queryByText('Set Your Pay-as-you-go Budget')).not.toBeInTheDocument(); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM1}, }) ); }); it('prefills with existing subscription data', async function () { MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); const sub = SubscriptionFixture({ organization, plan: 'am3_business', planTier: PlanTier.AM3, categories: { errors: MetricHistoryFixture({reserved: 100_000}), attachments: MetricHistoryFixture({reserved: 25}), replays: MetricHistoryFixture({reserved: 50}), monitorSeats: MetricHistoryFixture({reserved: 1}), spans: MetricHistoryFixture({reserved: 20_000_000}), profileDuration: MetricHistoryFixture({reserved: 1}), }, onDemandBudgets: { onDemandSpendUsed: 0, sharedMaxBudget: 2000, budgetMode: OnDemandBudgetMode.SHARED, enabled: true, }, onDemandMaxSpend: 2000, supportsOnDemand: true, }); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); expect(screen.getByTestId('errors-volume-item')).toBeInTheDocument(); // skips over first step when subscription is already on Business plan expect(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})).toHaveValue('20'); // TODO: Can better write this once we have // https://github.com/testing-library/jest-dom/issues/478 expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '100000' ); expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '50' ); expect(screen.getByRole('slider', {name: 'Spans'})).toHaveAttribute( 'aria-valuetext', '20000000' ); expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '25' ); expect( screen.queryByRole('slider', {name: 'Accepted Spans'}) ).not.toBeInTheDocument(); expect(screen.queryByRole('slider', {name: 'Stored Spans'})).not.toBeInTheDocument(); expect(screen.queryByRole('slider', {name: 'Cron Monitors'})).not.toBeInTheDocument(); }); it('allows setting PAYG for customers switching to AM3', async function () { const sub = SubscriptionFixture({ organization, // This plan does not have hasOnDemandModes plan: 'mm2_b_100k', planTier: PlanTier.AM2, }); act(() => SubscriptionStore.set(organization.slug, sub)); const mockResponse = MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); render( , {organization} ); expect(hasOnDemandBudgetsFeature(organization, sub)).toBe(false); expect(await screen.findByText('Choose Your Plan')).toBeInTheDocument(); expect(screen.getByRole('radio', {name: 'Business'})).toBeChecked(); await userEvent.click(screen.getByRole('button', {name: 'Continue'})); await userEvent.clear(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})); await userEvent.type( screen.getByRole('textbox', {name: 'Pay-as-you-go budget'}), '20' ); expect(await screen.findByTestId('additional-monthly-charge')).toHaveTextContent( '+ up to $20/mo based on PAYG usage' ); expect(mockResponse).toHaveBeenCalledWith( `/customers/${organization.slug}/billing-config/`, expect.objectContaining({ method: 'GET', data: {tier: PlanTier.AM3}, }) ); }); it('handles missing categories in subscription.categories', async function () { // Add billing config mock response MockApiClient.addMockResponse({ url: `/customers/${organization.slug}/billing-config/`, method: 'GET', body: BillingConfigFixture(PlanTier.AM3), }); /** * In this test, we create a subscription where some categories are missing from * `subscription.categories`. We then verify that the component renders correctly * without throwing errors, and that the missing categories default to a reserved * value of 0. */ const sub = SubscriptionFixture({ organization, plan: 'am3_business', planTier: PlanTier.AM3, categories: { // Intentionally omitting 'errors' and 'attachments' categories replays: MetricHistoryFixture({reserved: 50}), monitorSeats: MetricHistoryFixture({reserved: 1}), spans: MetricHistoryFixture({reserved: 1}), profileDuration: MetricHistoryFixture({reserved: 1}), }, onDemandBudgets: { onDemandSpendUsed: 0, sharedMaxBudget: 2000, budgetMode: OnDemandBudgetMode.SHARED, enabled: true, }, supportsOnDemand: true, }); SubscriptionStore.set(organization.slug, sub); render( , {organization} ); expect( await screen.findByRole('heading', {name: 'Change Subscription'}) ).toBeInTheDocument(); // Verify that the component renders without errors expect(screen.getByTestId('replays-volume-item')).toBeInTheDocument(); // For AM3, we should see "Set Your Pay-as-you-go Budget" first expect(screen.getByText('Set Your Pay-as-you-go Budget')).toBeInTheDocument(); // Check that missing 'Errors' category defaults to 50,000 errors expect(screen.getByRole('slider', {name: 'Errors'})).toHaveAttribute( 'aria-valuetext', '50000' ); // For 'Replays', should be set to 50 as per the subscription expect(screen.getByRole('slider', {name: 'Replays'})).toHaveAttribute( 'aria-valuetext', '50' ); // Check that missing 'Attachments' category defaults to 1 GB expect(screen.getByRole('slider', {name: 'Attachments'})).toHaveAttribute( 'aria-valuetext', '1' ); // Verify that the 'Pay-as-you-go budget' is correctly set expect(screen.getByRole('textbox', {name: 'Pay-as-you-go budget'})).toHaveValue('20'); }); it('handles zero platform reserve', function () { const formData = { plan: 'am3_business', reserved: { errors: 10000, transactions: 0, attachments: 0, replays: 0, monitorSeats: 0, profileDuration: 0, spans: 0, }, }; expect(getCheckoutAPIData({formData})).toEqual({ onDemandBudget: undefined, onDemandMaxSpend: 0, plan: 'am3_business', referrer: 'billing', reservedErrors: 10000, reservedTransactions: 0, reservedAttachments: 0, reservedReplays: 0, reservedMonitorSeats: 0, reservedProfileDuration: 0, reservedSpans: 0, }); }); });