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(
);
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();
// 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();
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();
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();
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();
// 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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
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();
});
});