123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966 |
- 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 {InvoicePreviewFixture} from 'getsentry-test/fixtures/invoicePreview';
- import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory';
- import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup';
- import {ProjectFixture} from 'getsentry-test/fixtures/project';
- import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
- import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
- import {addErrorMessage} from 'sentry/actionCreators/indicator';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import SubscriptionStore from 'getsentry/stores/subscriptionStore';
- import {PlanTier} from 'getsentry/types';
- import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
- import AMCheckout from 'getsentry/views/amCheckout/';
- import {getCheckoutAPIData} from '../utils';
- import ReviewAndConfirm from './reviewAndConfirm';
- jest.mock('sentry/actionCreators/indicator');
- jest.mock('getsentry/utils/trackGetsentryAnalytics');
- jest.mock('getsentry/utils/stripe', () => ({
- loadStripe: (cb: any) => {
- if (!cb) {
- return;
- }
- cb(() => ({
- handleCardAction(secretKey: string, _options: any) {
- if (secretKey === 'ERROR') {
- return new Promise(resolve => {
- resolve({error: {message: 'Invalid card', type: 'card_error'}});
- });
- }
- if (secretKey === 'GENERIC_ERROR') {
- return new Promise(resolve => {
- resolve({
- error: {
- message: 'Something bad that users should not see',
- type: 'internal_error',
- },
- });
- });
- }
- return new Promise(resolve => {
- resolve({setupIntent: {payment_method: 'pm_abc123'}});
- });
- },
- }));
- },
- }));
- describe('AmCheckout > ReviewAndConfirm', function () {
- const api = new MockApiClient();
- const organization = OrganizationFixture();
- const subscription = SubscriptionFixture({organization});
- const params = {};
- const bizPlan = PlanDetailsLookupFixture('am1_business')!;
- const billingConfig = BillingConfigFixture(PlanTier.AM2);
- const formData = {
- plan: billingConfig.defaultPlan,
- reserved: billingConfig.defaultReserved,
- };
- const stepProps = {
- stepNumber: 6,
- onUpdate: jest.fn(),
- onCompleteStep: jest.fn(),
- onEdit: jest.fn(),
- billingConfig,
- formData,
- activePlan: bizPlan,
- organization,
- subscription,
- isActive: false,
- isCompleted: false,
- prevStepCompleted: false,
- };
- function mockPreviewGet(slug = organization.slug, effectiveAt: Date | null = null) {
- const preview = InvoicePreviewFixture();
- if (effectiveAt) {
- preview.effectiveAt = effectiveAt.toISOString();
- }
- const mockPreview = MockApiClient.addMockResponse({
- url: `/customers/${slug}/subscription/preview/`,
- method: 'GET',
- body: preview,
- });
- return {mockPreview, preview};
- }
- function mockSubscriptionPut(mockParams = {}, slug = organization.slug) {
- return MockApiClient.addMockResponse({
- url: `/customers/${slug}/subscription/`,
- method: 'PUT',
- ...mockParams,
- });
- }
- beforeEach(function () {
- SubscriptionStore.set(organization.slug, subscription);
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- url: `/subscriptions/${organization.slug}/`,
- method: 'GET',
- body: {},
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/`,
- method: 'GET',
- body: organization,
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/projects/`,
- method: 'GET',
- body: [ProjectFixture({})],
- });
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/teams/`,
- method: 'GET',
- body: [],
- });
- MockApiClient.addMockResponse({
- url: `/customers/${organization.slug}/billing-config/`,
- method: 'GET',
- body: BillingConfigFixture(PlanTier.AM2),
- });
- MockApiClient.addMockResponse({
- url: `/customers/${organization.slug}/plan-migrations/?applied=0`,
- method: 'GET',
- body: [],
- });
- });
- it('cannot skip to review step', async function () {
- mockPreviewGet();
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/promotions/trigger-check/`,
- method: 'POST',
- });
- render(
- <AMCheckout
- {...RouteComponentPropsFixture()}
- params={params}
- api={api}
- onToggleLegacy={jest.fn()}
- checkoutTier={subscription.planTier as PlanTier}
- />
- );
- const heading = await screen.findByText('Review & Confirm');
- expect(heading).toBeInTheDocument();
- // Submit should not be visible
- expect(screen.queryByText('Confirm Changes')).not.toBeInTheDocument();
- // Clicking the heading should not reveal the submit button
- await userEvent.click(heading);
- expect(screen.queryByText('Confirm Changes')).not.toBeInTheDocument();
- });
- it('renders closed', function () {
- const {mockPreview} = mockPreviewGet();
- render(<ReviewAndConfirm {...stepProps} />);
- // Submit should not be visible
- expect(screen.queryByText('Confirm Changes')).not.toBeInTheDocument();
- expect(mockPreview).not.toHaveBeenCalled();
- });
- it('renders open when active', async function () {
- const {preview, mockPreview} = mockPreviewGet();
- render(<ReviewAndConfirm {...stepProps} isActive />);
- expect(
- await screen.findByText(preview.invoiceItems[0]!.description)
- ).toBeInTheDocument();
- expect(screen.getByText(preview.invoiceItems[1]!.description)).toBeInTheDocument();
- expect(screen.getByText(preview.invoiceItems[2]!.description)).toBeInTheDocument();
- expect(screen.getByText(preview.invoiceItems[3]!.description)).toBeInTheDocument();
- expect(screen.getByTestId('dates')).toBeInTheDocument();
- expect(screen.getAllByText('$89')).toHaveLength(2);
- expect(screen.getByRole('button', {name: 'Confirm Changes'})).toBeInTheDocument();
- expect(screen.queryByRole('button', {name: 'Migrate Now'})).not.toBeInTheDocument();
- expect(mockPreview).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/preview/`,
- expect.objectContaining({
- method: 'GET',
- data: getCheckoutAPIData({formData, isPreview: true}),
- })
- );
- });
- it('requests preview with ondemand spend', async function () {
- const {mockPreview, preview} = mockPreviewGet();
- const updatedData = {...formData, onDemandMaxSpend: 5000};
- render(<ReviewAndConfirm {...stepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(preview.invoiceItems[0]!.description)
- ).toBeInTheDocument();
- expect(mockPreview).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/preview/`,
- expect.objectContaining({
- method: 'GET',
- data: getCheckoutAPIData({formData: updatedData, isPreview: true}),
- })
- );
- });
- it('updates preview with formData change when active', async function () {
- const {preview, mockPreview} = mockPreviewGet();
- const {rerender} = render(<ReviewAndConfirm {...stepProps} />);
- expect(await screen.findByText('Review & Confirm')).toBeInTheDocument();
- expect(screen.queryByText('Confirm Changes')).not.toBeInTheDocument();
- expect(mockPreview).not.toHaveBeenCalled();
- const updatedData = {...formData, plan: 'am1_business_auf'};
- rerender(<ReviewAndConfirm {...stepProps} isActive formData={updatedData} />);
- // Wait for invoice to render.
- expect(
- await screen.findByText(preview.invoiceItems[0]!.description)
- ).toBeInTheDocument();
- expect(screen.getByText('Confirm Changes')).toBeInTheDocument();
- expect(mockPreview).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/preview/`,
- expect.objectContaining({
- method: 'GET',
- data: getCheckoutAPIData({formData: updatedData, isPreview: true}),
- })
- );
- });
- it('can confirm changes', async function () {
- const {preview} = mockPreviewGet();
- const mockConfirm = mockSubscriptionPut();
- const reservedErrors = 100000;
- const updatedData = {
- ...formData,
- reserved: {...formData.reserved, errors: reservedErrors},
- };
- render(<ReviewAndConfirm {...stepProps} formData={updatedData} isActive />);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- // No DOM updates to wait on, but we can use this.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${organization.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith('checkout.upgrade', {
- organization,
- subscription,
- previous_plan: 'am1_f',
- previous_errors: 5000,
- previous_transactions: 10_000,
- previous_attachments: 1,
- previous_replays: 50,
- previous_monitorSeats: 1,
- previous_profileDuration: undefined,
- previous_spans: undefined,
- previous_uptime: 1,
- plan: updatedData.plan,
- errors: updatedData.reserved.errors,
- transactions: updatedData.reserved.transactions,
- attachments: updatedData.reserved.attachments,
- replays: updatedData.reserved.replays,
- monitorSeats: updatedData.reserved.monitorSeats,
- spans: undefined,
- uptime: 1,
- });
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith(
- 'checkout.transactions_upgrade',
- {
- organization,
- subscription,
- plan: updatedData.plan,
- transactions: updatedData.reserved.transactions,
- previous_transactions: 10_000,
- }
- );
- });
- it('can schedule changes for partner migration', async function () {
- const partnerOrg = OrganizationFixture({features: ['partner-billing-migration']});
- const partnerSub = SubscriptionFixture({
- organization: partnerOrg,
- partner: {
- externalId: 'whateva',
- isActive: true,
- partnership: {
- id: 'FOO',
- displayName: 'FOO',
- supportNote: '',
- },
- name: '',
- },
- contractPeriodEnd: moment().add(7, 'days').toString(),
- });
- const {preview} = mockPreviewGet(partnerOrg.slug);
- const mockConfirm = mockSubscriptionPut(partnerOrg.slug);
- const updatedData = {
- plan: 'am3_business',
- reserved: {
- errors: 100_000,
- replays: 5000,
- spans: 10_000_000,
- attachments: 1,
- monitorSeats: 1,
- profileDuration: 0,
- uptime: 1,
- },
- };
- const partnerStepProps = {
- ...stepProps,
- organization: partnerOrg,
- subscription: partnerSub,
- };
- render(<ReviewAndConfirm {...partnerStepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(
- `These changes will take effect at the end of your current FOO sponsored plan on ${moment(partnerSub.contractPeriodEnd).add(1, 'days').format('ll')}. If you want these changes to apply immediately, select Migrate Now.`
- )
- ).toBeInTheDocument();
- await userEvent.click(await screen.findByText('Schedule Changes'));
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${partnerOrg.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- // No DOM updates to wait on, but we can use this.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${partnerOrg.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith('checkout.upgrade', {
- organization: partnerOrg,
- subscription: partnerSub,
- previous_plan: 'am1_f',
- previous_errors: 5000,
- previous_transactions: 10_000,
- previous_attachments: 1,
- previous_replays: 50,
- previous_monitorSeats: 1,
- previous_profileDuration: undefined,
- previous_spans: undefined,
- previous_uptime: 1,
- plan: updatedData.plan,
- errors: updatedData.reserved.errors,
- transactions: undefined,
- attachments: updatedData.reserved.attachments,
- replays: updatedData.reserved.replays,
- monitorSeats: updatedData.reserved.monitorSeats,
- spans: updatedData.reserved.spans,
- profileDuration: updatedData.reserved.profileDuration,
- uptime: updatedData.reserved.uptime,
- });
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith(
- 'partner_billing_migration.checkout.completed',
- {
- organization: partnerOrg,
- subscription: partnerSub,
- applyNow: false,
- daysLeft: 7,
- partner: 'FOO',
- }
- );
- });
- it('can migrate immediately for partner migration', async function () {
- const partnerOrg = OrganizationFixture({features: ['partner-billing-migration']});
- const partnerSub = SubscriptionFixture({
- organization: partnerOrg,
- partner: {
- externalId: 'whateva',
- isActive: true,
- partnership: {
- id: 'FOO',
- displayName: 'FOO',
- supportNote: '',
- },
- name: '',
- },
- contractPeriodEnd: moment().add(20, 'days').toString(),
- });
- const {preview} = mockPreviewGet(partnerOrg.slug);
- const mockConfirm = mockSubscriptionPut(partnerOrg.slug);
- const updatedData = {
- plan: 'am3_business',
- reserved: {
- errors: 100_000,
- replays: 5000,
- spans: 10_000_000,
- attachments: 1,
- monitorSeats: 1,
- },
- };
- const partnerStepProps = {
- ...stepProps,
- organization: partnerOrg,
- subscription: partnerSub,
- };
- render(<ReviewAndConfirm {...partnerStepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(
- `These changes will take effect at the end of your current FOO sponsored plan on ${moment(partnerSub.contractPeriodEnd).add(1, 'days').format('ll')}. If you want these changes to apply immediately, select Migrate Now.`
- )
- ).toBeInTheDocument();
- await userEvent.click(await screen.findByText('Migrate Now'));
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${partnerOrg.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: {...updatedData, applyNow: true},
- previewToken: preview.previewToken,
- }),
- })
- );
- // No DOM updates to wait on, but we can use this.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${partnerOrg.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith('checkout.upgrade', {
- organization: partnerOrg,
- subscription: partnerSub,
- previous_plan: 'am1_f',
- previous_errors: 5000,
- previous_transactions: 10_000,
- previous_attachments: 1,
- previous_replays: 50,
- previous_monitorSeats: 1,
- previous_profileDuration: undefined,
- previous_spans: undefined,
- plan: updatedData.plan,
- errors: updatedData.reserved.errors,
- transactions: undefined,
- attachments: updatedData.reserved.attachments,
- replays: updatedData.reserved.replays,
- monitorSeats: updatedData.reserved.monitorSeats,
- spans: updatedData.reserved.spans,
- previous_uptime: 1,
- });
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith(
- 'partner_billing_migration.checkout.completed',
- {
- organization: partnerOrg,
- subscription: partnerSub,
- applyNow: true,
- daysLeft: 20,
- partner: 'FOO',
- }
- );
- });
- it('should render immediate copy for effectiveNow', async function () {
- mockPreviewGet(organization.slug, new Date());
- mockSubscriptionPut(organization.slug);
- const updatedData = {
- plan: 'am3_business',
- reserved: {
- errors: 100_000,
- replays: 5000,
- spans: 10_000_000,
- attachments: 1,
- monitorSeats: 1,
- },
- };
- render(<ReviewAndConfirm {...stepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(
- `These changes will apply immediately, and you will be billed today.`
- )
- ).toBeInTheDocument();
- });
- it('should render contract end copy for effective later', async function () {
- mockPreviewGet(organization.slug);
- mockSubscriptionPut(organization.slug);
- const updatedData = {
- plan: 'am3_business',
- reserved: {
- errors: 100_000,
- replays: 5000,
- spans: 10_000_000,
- attachments: 1,
- monitorSeats: 1,
- },
- };
- render(<ReviewAndConfirm {...stepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(
- `This change will take effect at the end of your current contract period.`
- )
- ).toBeInTheDocument();
- });
- it('should render billed through self serve partner copy for effectiveNow', async function () {
- const partnerSub = SubscriptionFixture({
- organization,
- contractPeriodEnd: moment().add(20, 'days').toString(),
- plan: 'am3_f',
- planTier: PlanTier.AM3,
- isSelfServePartner: true,
- partner: {
- externalId: 'whateva',
- isActive: true,
- partnership: {
- id: 'FOO',
- displayName: 'FOO',
- supportNote: '',
- },
- name: '',
- },
- });
- mockPreviewGet(organization.slug, new Date());
- mockSubscriptionPut(organization.slug);
- const updatedData = {
- plan: 'am3_business',
- reserved: {
- errors: 100_000,
- replays: 5000,
- spans: 10_000_000,
- attachments: 1,
- monitorSeats: 1,
- },
- };
- const partnerStepProps = {
- ...stepProps,
- subscription: partnerSub,
- };
- render(<ReviewAndConfirm {...partnerStepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(
- `These changes will apply immediately, and you will be billed by FOO monthly for any recurring subscription fees and incurred pay-as-you-go fees.`
- )
- ).toBeInTheDocument();
- });
- it('should render billed through self serve partner copy for effective later', async function () {
- mockPreviewGet(organization.slug);
- mockSubscriptionPut(organization.slug);
- const updatedData = {
- plan: 'am3_business',
- reserved: {
- errors: 100_000,
- replays: 5000,
- spans: 10_000_000,
- attachments: 1,
- monitorSeats: 1,
- },
- };
- const partnerSub = SubscriptionFixture({
- organization,
- contractPeriodEnd: moment().add(20, 'days').toString(),
- plan: 'am3_f',
- planTier: PlanTier.AM3,
- isSelfServePartner: true,
- partner: {
- externalId: 'whateva',
- isActive: true,
- partnership: {
- id: 'FOO',
- displayName: 'FOO',
- supportNote: '',
- },
- name: '',
- },
- });
- const partnerStepProps = {
- ...stepProps,
- subscription: partnerSub,
- };
- render(<ReviewAndConfirm {...partnerStepProps} formData={updatedData} isActive />);
- expect(
- await screen.findByText(
- `These changes will apply on the date above, and you will be billed by FOO monthly for any recurring subscription fees and incurred pay-as-you-go fees.`
- )
- ).toBeInTheDocument();
- });
- it('does not send transactions upgrade event for plan upgrade', async function () {
- const {preview} = mockPreviewGet();
- const mockConfirm = mockSubscriptionPut();
- const sub = SubscriptionFixture({
- organization,
- plan: 'am1_team',
- categories: {
- errors: MetricHistoryFixture({reserved: 100_000}),
- transactions: MetricHistoryFixture({reserved: 250_000}),
- attachments: MetricHistoryFixture({reserved: 1}),
- replays: MetricHistoryFixture({reserved: 500}),
- monitorSeats: MetricHistoryFixture({reserved: 1}),
- },
- });
- SubscriptionStore.set(organization.slug, sub);
- const updatedData = {...formData, plan: 'am1_business'};
- const props = {...stepProps, subscription: sub, formData: updatedData};
- render(<ReviewAndConfirm {...props} isActive />);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- // No DOM updates to wait on, but we can use this.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${organization.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith('checkout.upgrade', {
- organization,
- subscription: sub,
- previous_plan: 'am1_team',
- previous_errors: 100000,
- previous_transactions: 250000,
- previous_attachments: 1,
- previous_replays: 500,
- previous_monitorSeats: 1,
- previous_profileDuration: undefined,
- previous_spans: undefined,
- plan: 'am1_business',
- errors: updatedData.reserved.errors,
- transactions: updatedData.reserved.transactions,
- attachments: updatedData.reserved.attachments,
- replays: updatedData.reserved.replays,
- monitorSeats: updatedData.reserved.monitorSeats,
- uptime: updatedData.reserved.uptime,
- spans: undefined,
- });
- expect(trackGetsentryAnalytics).not.toHaveBeenCalledWith(
- 'checkout.transactions_upgrade'
- );
- });
- it('does not send transactions upgrade event for transactions downgrade', async function () {
- const {preview} = mockPreviewGet();
- const mockConfirm = mockSubscriptionPut();
- const sub = SubscriptionFixture({
- organization,
- plan: 'am1_team',
- categories: {
- errors: MetricHistoryFixture({reserved: 100000}),
- transactions: MetricHistoryFixture({reserved: 500000}),
- attachments: MetricHistoryFixture({reserved: 1}),
- replays: MetricHistoryFixture({reserved: 500}),
- monitorSeats: MetricHistoryFixture({reserved: 1}),
- },
- });
- SubscriptionStore.set(organization.slug, sub);
- const updatedData = {...formData};
- const props = {...stepProps, subscription: sub, formData: updatedData};
- render(<ReviewAndConfirm {...props} isActive />);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- // No DOM updates to wait on, but we can use this.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${organization.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- expect(trackGetsentryAnalytics).toHaveBeenCalledWith('checkout.upgrade', {
- organization,
- subscription: sub,
- previous_plan: 'am1_team',
- previous_errors: 100000,
- previous_transactions: 500000,
- previous_attachments: 1,
- previous_replays: 500,
- previous_monitorSeats: 1,
- previous_profileDuration: undefined,
- previous_spans: undefined,
- plan: updatedData.plan,
- errors: updatedData.reserved.errors,
- transactions: updatedData.reserved.transactions,
- attachments: updatedData.reserved.attachments,
- replays: updatedData.reserved.replays,
- monitorSeats: updatedData.reserved.monitorSeats,
- uptime: updatedData.reserved.uptime,
- spans: undefined,
- });
- expect(trackGetsentryAnalytics).not.toHaveBeenCalledWith(
- 'checkout.transactions_upgrade'
- );
- });
- it('can confirm with ondemand spend', async function () {
- const {preview} = mockPreviewGet();
- const mockConfirm = mockSubscriptionPut();
- const updatedData = {...formData, reserved: {errors: 100000}, onDemandMaxSpend: 5000};
- render(<ReviewAndConfirm {...stepProps} isActive formData={updatedData} />);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- // No DOM updates to wait on, but we can use this.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${organization.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- });
- it('handles expired token on confirm', async function () {
- const {preview, mockPreview} = mockPreviewGet();
- const mockConfirm = MockApiClient.addMockResponse({
- url: `/customers/${organization.slug}/subscription/`,
- method: 'PUT',
- statusCode: 400,
- body: {
- previewToken: ['The preview token is invalid or has expired.'],
- },
- });
- const updatedData = {...formData, reservedErrors: 100000};
- render(<ReviewAndConfirm {...stepProps} formData={updatedData} isActive />);
- expect(mockPreview).toHaveBeenCalledTimes(1);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- await waitFor(() => {
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- });
- expect(mockPreview).toHaveBeenCalledTimes(2);
- expect(addErrorMessage).toHaveBeenCalledWith(
- 'Your preview expired, please review changes and submit again'
- );
- expect(browserHistory.push).not.toHaveBeenCalled();
- });
- it('handles unknown error when updating subscription', async function () {
- const {preview, mockPreview} = mockPreviewGet();
- const mockConfirm = MockApiClient.addMockResponse({
- url: `/customers/${organization.slug}/subscription/`,
- method: 'PUT',
- statusCode: 500,
- });
- const updatedData = {...formData, reservedTransactions: 1500000};
- render(<ReviewAndConfirm {...stepProps} formData={updatedData} isActive />);
- expect(mockPreview).toHaveBeenCalledTimes(1);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- await waitFor(() => {
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- });
- expect(mockPreview).toHaveBeenCalledTimes(1);
- expect(addErrorMessage).toHaveBeenCalledWith(
- 'An unknown error occurred while saving your subscription'
- );
- expect(browserHistory.push).not.toHaveBeenCalled();
- });
- it('handles completing a card action when required', async function () {
- const {preview} = mockPreviewGet();
- // We make two API calls. The first fails with a card action required
- // which we have mocked to succeed. The second request will have
- // the intent to complete payment with.
- const mockConfirm = mockSubscriptionPut({
- statusCode: 402,
- body: {
- detail: 'Card action required',
- paymentIntent: 'pi_abc123',
- paymentSecret: 'pi_abc123-secret',
- },
- });
- const mockComplete = mockSubscriptionPut({
- statusCode: 200,
- body: subscription,
- match: [MockApiClient.matchData({paymentIntent: 'pi_abc123'})],
- });
- const updatedData = {...formData, reserved: {errors: 100000}, onDemandMaxSpend: 5000};
- render(<ReviewAndConfirm {...stepProps} isActive formData={updatedData} />);
- await userEvent.click(await screen.findByText('Confirm Changes'));
- // Wait for URL to change as that signals completion.
- await waitFor(() =>
- expect(browserHistory.push).toHaveBeenCalledWith(
- `/settings/${organization.slug}/billing/overview/?open_codecov_modal=1&referrer=checkout`
- )
- );
- expect(mockConfirm).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- }),
- })
- );
- expect(mockComplete).toHaveBeenCalledWith(
- `/customers/${organization.slug}/subscription/`,
- expect.objectContaining({
- method: 'PUT',
- data: getCheckoutAPIData({
- formData: updatedData,
- previewToken: preview.previewToken,
- paymentIntent: 'pi_abc123',
- }),
- })
- );
- });
- it('handles payment intent errors', async function () {
- mockPreviewGet();
- const mockConfirm = mockSubscriptionPut({
- statusCode: 402,
- body: {
- detail: 'Card action required',
- paymentIntent: 'pi_abc123',
- paymentSecret: 'ERROR',
- },
- });
- const updatedData = {...formData, reserved: {errors: 100000}, onDemandMaxSpend: 5000};
- render(<ReviewAndConfirm {...stepProps} isActive formData={updatedData} />);
- const button = await screen.findByRole('button', {name: 'Confirm Changes'});
- await userEvent.click(button);
- expect(await screen.findByText('Invalid card')).toBeInTheDocument();
- // Because our payment confirmation failed we can't continue
- expect(button).toBeDisabled();
- expect(mockConfirm).toHaveBeenCalled();
- });
- it('shows generic intent errors for odd types', async function () {
- mockPreviewGet();
- const mockConfirm = mockSubscriptionPut({
- statusCode: 402,
- body: {
- detail: 'Card action required',
- paymentIntent: 'pi_abc123',
- paymentSecret: 'GENERIC_ERROR',
- },
- });
- const updatedData = {...formData, reserved: {errors: 100000}, onDemandMaxSpend: 5000};
- render(<ReviewAndConfirm {...stepProps} isActive formData={updatedData} />);
- const button = await screen.findByRole('button', {name: 'Confirm Changes'});
- await userEvent.click(button);
- expect(
- await screen.findByText(/Your payment could not be authorized/)
- ).toBeInTheDocument();
- // Because our payment confirmation failed we can't continue
- await waitFor(() => {
- expect(button).toBeDisabled();
- });
- expect(mockConfirm).toHaveBeenCalled();
- });
- });
|