Skip to content

feat(seer): change plan in admin with seer #92172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion static/gsAdmin/components/changePlanAction.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('ChangePlanAction', () => {
async function openAndLoadModal(props = {}) {
triggerChangePlanAction({
subscription,
orgId: mockOrg.slug,
organization: mockOrg,
onSuccess: jest.fn(),
partnerPlanId: null,
...props,
Expand Down Expand Up @@ -184,6 +184,7 @@ describe('ChangePlanAction', () => {
});

it('completes form submission flow', async () => {
mockOrg.features = [];
// Mock the PUT endpoint response
const putMock = MockApiClient.addMockResponse({
url: `/customers/${mockOrg.slug}/subscription/`,
Expand Down Expand Up @@ -212,6 +213,8 @@ describe('ChangePlanAction', () => {
'1'
);

expect(screen.queryByText('Available Products')).not.toBeInTheDocument();

expect(screen.getByRole('button', {name: 'Change Plan'})).toBeEnabled();
await userEvent.click(screen.getByRole('button', {name: 'Change Plan'}));

Expand All @@ -221,6 +224,44 @@ describe('ChangePlanAction', () => {
expect(requestData).toHaveProperty('plan', 'am3_business');
});

it('completes form with seer', async () => {
mockOrg.features = ['seer-billing'];
const putMock = MockApiClient.addMockResponse({
url: `/customers/${mockOrg.slug}/subscription/`,
method: 'PUT',
body: {success: true},
});

openAndLoadModal();

await waitFor(() => {
expect(screen.getByRole('tab', {name: 'AM3'})).toBeInTheDocument();
});

await userEvent.click(screen.getAllByRole('radio')[0] as HTMLElement);

await selectEvent.select(screen.getByRole('textbox', {name: 'Errors'}), '100,000');
await selectEvent.select(screen.getByRole('textbox', {name: 'Replays'}), '50');
await selectEvent.select(screen.getByRole('textbox', {name: 'Spans'}), '10,000,000');
await selectEvent.select(screen.getByRole('textbox', {name: 'Cron monitors'}), '1');
await selectEvent.select(screen.getByRole('textbox', {name: 'Uptime monitors'}), '1');
await selectEvent.select(
screen.getByRole('textbox', {name: 'Attachments (GB)'}),
'1'
);

expect(screen.getByText('Available Products')).toBeInTheDocument();
await userEvent.click(screen.getByText('Seer'));

expect(screen.getByRole('button', {name: 'Change Plan'})).toBeEnabled();
await userEvent.click(screen.getByRole('button', {name: 'Change Plan'}));

expect(putMock).toHaveBeenCalled();
const requestData = putMock.mock.calls[0][1].data;
expect(requestData).toHaveProperty('plan', 'am3_business');
expect(requestData).toHaveProperty('seer', true);
});

it('updates plan list when switching between tiers', async () => {
openAndLoadModal();

Expand Down Expand Up @@ -314,4 +355,69 @@ describe('ChangePlanAction', () => {
expect(requestData).toHaveProperty('reservedErrors', 50000);
expect(requestData).toHaveProperty('reservedTransactions', 25000);
});

describe('Seer Budget', () => {
beforeEach(() => {
mockOrg.features = ['seer-billing'];
jest.clearAllMocks();
MockApiClient.clearMockResponses();

const user = UserFixture();
user.permissions = new Set(['billing.provision']);
ConfigStore.set('user', user);
SubscriptionStore.set(mockOrg.slug, subscription);

// Set up default subscription response
MockApiClient.addMockResponse({
url: `/subscriptions/${mockOrg.slug}/`,
body: subscription,
});

MockApiClient.addMockResponse({
url: `/customers/${mockOrg.slug}/billing-config/?tier=all`,
body: BILLING_CONFIG,
});
});

it('shows Seer budget checkbox for AM tiers', async () => {
openAndLoadModal();

await waitFor(() => {
expect(screen.getByRole('tab', {name: 'AM3'})).toBeInTheDocument();
});
await userEvent.click(screen.getAllByRole('radio')[0] as HTMLElement);

expect(screen.getByText('Seer')).toBeInTheDocument();

const am2Tab = screen.getByRole('tab', {name: 'AM2'});
await userEvent.click(am2Tab);
await userEvent.click(screen.getAllByRole('radio')[0] as HTMLElement);

expect(screen.getByText('Seer')).toBeInTheDocument();

const am1Tab = screen.getByRole('tab', {name: 'AM1'});
await userEvent.click(am1Tab);
await userEvent.click(screen.getAllByRole('radio')[0] as HTMLElement);

expect(screen.getByText('Seer')).toBeInTheDocument();
});
it('hides Seer budget checkbox for MM2 tier', async () => {
openAndLoadModal();

await waitFor(() => {
expect(screen.getByRole('tab', {name: 'AM3'})).toBeInTheDocument();
});

const mm2Tab = screen.getByRole('tab', {name: 'MM2'});
await userEvent.click(mm2Tab);

await userEvent.click(screen.getAllByRole('radio')[0] as HTMLElement);

expect(
screen.queryByRole('checkbox', {
name: 'Seer Budget',
})
).not.toBeInTheDocument();
});
});
});
14 changes: 10 additions & 4 deletions static/gsAdmin/components/changePlanAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
import ConfigStore from 'sentry/stores/configStore';
import {space} from 'sentry/styles/space';
import type {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import {useApiQuery} from 'sentry/utils/queryClient';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';
import useApi from 'sentry/utils/useApi';
Expand All @@ -29,14 +30,14 @@ const ALLOWED_TIERS = [PlanTier.MM2, PlanTier.AM1, PlanTier.AM2, PlanTier.AM3];

type Props = {
onSuccess: () => void;
orgId: string;
organization: Organization;
partnerPlanId: string | null;
subscription: Subscription;
} & ModalRenderProps;

function ChangePlanAction({
subscription,
orgId,
organization,
partnerPlanId,
onSuccess,
closeModal,
Expand All @@ -46,6 +47,7 @@ function ChangePlanAction({
const [activeTier, setActiveTier] = useState<PlanTier>(PlanTier.AM3);
const [activePlan, setActivePlan] = useState<Plan | null>(null);
const [formModel] = useState(() => new FormModel());
const orgId = organization.slug;

const api = useApi({persistInFlight: true});
const {
Expand Down Expand Up @@ -156,7 +158,10 @@ function ChangePlanAction({
}

Object.entries(subscription.categories).forEach(([category, metricHistory]) => {
if (metricHistory.reserved) {
if (
metricHistory.reserved &&
plan.checkoutCategories.includes(category as DataCategory)
) {
const closestTier = findClosestTier(
plan,
category as DataCategory,
Expand Down Expand Up @@ -319,6 +324,7 @@ function ChangePlanAction({
formModel={formModel}
activePlan={activePlan}
subscription={subscription}
organization={organization}
onSubmit={handleSubmit}
onCancel={closeModal}
onSubmitSuccess={(data: Data) => {
Expand All @@ -340,7 +346,7 @@ function ChangePlanAction({

type Options = {
onSuccess: () => void;
orgId: string;
organization: Organization;
partnerPlanId: string | null;
subscription: Subscription;
};
Expand Down
33 changes: 33 additions & 0 deletions static/gsAdmin/components/planList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import styled from '@emotion/styled';

import CheckboxField from 'sentry/components/forms/fields/checkboxField';
import InputField from 'sentry/components/forms/fields/inputField';
import RadioField from 'sentry/components/forms/fields/radioField';
import SelectField from 'sentry/components/forms/fields/selectField';
Expand All @@ -9,6 +10,7 @@ import type FormModel from 'sentry/components/forms/model';
import type {Data, OnSubmitCallback} from 'sentry/components/forms/types';
import {space} from 'sentry/styles/space';
import {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';

import {ANNUAL} from 'getsentry/constants';
Expand All @@ -24,13 +26,15 @@ type Props = {
onSubmit: OnSubmitCallback;
onSubmitError: (error: any) => void;
onSubmitSuccess: (data: Data) => void;
organization: Organization;
subscription: Subscription;
tierPlans: BillingConfig['planList'];
};

function PlanList({
activePlan,
subscription,
organization,
onSubmit,
onCancel,
onSubmitSuccess,
Expand Down Expand Up @@ -74,6 +78,17 @@ function PlanList({
100000: '100K',
};

const availableProducts = Object.values(activePlan?.availableReservedBudgetTypes || {})
.filter(
productInfo =>
productInfo.isFixed && // NOTE: for now, we only supported fixed budget products in checkout
productInfo.billingFlag &&
organization.features.includes(productInfo.billingFlag)
)
.map(productInfo => {
return productInfo;
});

return (
<Form
onSubmit={onSubmit}
Expand Down Expand Up @@ -161,6 +176,24 @@ function PlanList({
})}
</StyledFormSection>
)}
{availableProducts.length > 0 && (
<StyledFormSection>
<h4>Available Products</h4>
{availableProducts.map(productInfo => {
return (
<CheckboxField
key={productInfo.productName}
data-test-id={`checkbox-${productInfo.productName}`}
label={toTitleCase(productInfo.productName)}
name={productInfo.productName}
onChange={(value: any) => {
formModel.setValue(productInfo.productName, value.target.checked);
}}
/>
);
})}
</StyledFormSection>
)}
<AuditFields>
<InputField
data-test-id="url-field"
Expand Down
2 changes: 1 addition & 1 deletion static/gsAdmin/views/customerDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ export default function CustomerDetails() {
skipConfirmModal: true,
onAction: () =>
triggerChangePlanAction({
orgId,
organization,
subscription,
partnerPlanId: subscription.partner?.isActive
? subscription.planDetails.id
Expand Down
Loading