Skip to content
Merged
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
25 changes: 8 additions & 17 deletions static/gsAdmin/components/changePlanAction.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {UserFixture} from 'sentry-fixture/user';
import {BillingConfigFixture} from 'getsentry-test/fixtures/billingConfig';
import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory';
import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup';
import {SeerReservedBudgetFixture} from 'getsentry-test/fixtures/reservedBudget';
import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
import {
SubscriptionFixture,
SubscriptionWithSeerFixture,
} from 'getsentry-test/fixtures/subscription';
import {
renderGlobalModal,
screen,
Expand Down Expand Up @@ -239,7 +241,7 @@ describe('ChangePlanAction', () => {
});

it('completes form with addOns', async () => {
mockOrg.features = ['seer-billing', 'seer-user-billing'];
mockOrg.features = ['seer-billing', 'seer-user-billing']; // this won't happen IRL, but doing this for testing multiple addons
const putMock = MockApiClient.addMockResponse({
url: `/customers/${mockOrg.slug}/subscription/`,
method: 'PUT',
Expand Down Expand Up @@ -378,7 +380,7 @@ describe('ChangePlanAction', () => {
expect(requestData).toHaveProperty('reservedTransactions', 25000);
});

describe('Seer Budget', () => {
describe('Legacy Seer', () => {
beforeEach(() => {
mockOrg.features = ['seer-billing'];
jest.clearAllMocks();
Expand Down Expand Up @@ -445,21 +447,10 @@ describe('ChangePlanAction', () => {

it('initializes Seer budget checkbox based on current subscription', async () => {
// Create subscription with Seer budget
const subscriptionWithSeer = SubscriptionFixture({
const subscriptionWithSeer = SubscriptionWithSeerFixture({
organization: mockOrg,
planTier: PlanTier.AM3,
plan: 'am3_business',
billingInterval: 'monthly',
contractInterval: 'monthly',
reservedBudgets: [SeerReservedBudgetFixture({})],
categories: {
errors: MetricHistoryFixture({
category: DataCategory.ERRORS,
reserved: 1000000,
prepaid: 1000000,
order: 1,
}),
},
});

SubscriptionStore.set(mockOrg.slug, subscriptionWithSeer);
Expand Down Expand Up @@ -555,7 +546,7 @@ describe('ChangePlanAction', () => {
expect(requestData).toHaveProperty('addOnSeer', true);
});

it('does not include seer parameter in form submission when checkbox is unchecked', async () => {
it('does not include add-on parameter in form submission when checkbox is unchecked', async () => {
// Mock the PUT endpoint response
const putMock = MockApiClient.addMockResponse({
url: `/customers/${mockOrg.slug}/subscription/`,
Expand Down
30 changes: 3 additions & 27 deletions static/gsAdmin/components/changePlanAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import useApi from 'sentry/utils/useApi';
import PlanList from 'admin/components/planList';
import {ANNUAL, MONTHLY} from 'getsentry/constants';
import type {BillingConfig, Plan, Subscription} from 'getsentry/types';
import {CheckoutType, PlanTier, ReservedBudgetCategoryType} from 'getsentry/types';
import {CheckoutType, PlanTier} from 'getsentry/types';

const ALLOWED_TIERS = [PlanTier.MM2, PlanTier.AM1, PlanTier.AM2, PlanTier.AM3];

Expand All @@ -49,23 +49,6 @@ function ChangePlanAction({
const [formModel] = useState(() => new FormModel());
const orgId = organization.slug;

/**
* Check if the current subscription has Seer budget enabled
*/
const hasCurrentSeerBudget = useMemo(() => {
return (
subscription.reservedBudgets?.some(
budget =>
budget.apiName === ReservedBudgetCategoryType.SEER && budget.reservedBudget > 0
) ?? false
);
}, [subscription.reservedBudgets]);

// Initialize Seer budget value in form model
React.useEffect(() => {
formModel.setValue('addOnSeer', hasCurrentSeerBudget);
}, [formModel, hasCurrentSeerBudget]);

const api = useApi({persistInFlight: true});
const {
data: configs,
Expand Down Expand Up @@ -214,17 +197,11 @@ function ChangePlanAction({
return;
}

// Add Seer budget parameter for AM plans and TEST tier
const submitData = {
...data,
addOnSeer: formModel.getValue('addOnSeer'),
};

if (activeTier === PlanTier.MM2) {
try {
await api.requestPromise(`/customers/${orgId}/`, {
method: 'PUT',
data: submitData,
data,
});
onSubmitSuccess(data);
} catch (error) {
Expand All @@ -237,7 +214,7 @@ function ChangePlanAction({
try {
await api.requestPromise(`/customers/${orgId}/subscription/`, {
method: 'PUT',
data: submitData,
data,
});
onSubmitSuccess(data);
onSuccess?.();
Expand Down Expand Up @@ -281,7 +258,6 @@ function ChangePlanAction({
setActiveTier(tab);
setBillingInterval(MONTHLY);
setContractInterval(MONTHLY);
formModel.setValue('addOnSeer', hasCurrentSeerBudget);
}}
>
<TabList>
Expand Down
32 changes: 23 additions & 9 deletions static/gsAdmin/components/planList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useEffect, useMemo} from 'react';
import styled from '@emotion/styled';

import CheckboxField from 'sentry/components/forms/fields/checkboxField';
Expand Down Expand Up @@ -78,14 +79,27 @@ function PlanList({
100000: '100K',
};

const availableProducts = Object.values(activePlan?.addOnCategories || {})
.filter(
productInfo =>
productInfo.billingFlag && organization.features.includes(productInfo.billingFlag)
)
.map(productInfo => {
return productInfo;
const availableProducts = useMemo(
() =>
Object.values(activePlan?.addOnCategories || {})
.filter(
productInfo =>
productInfo.billingFlag &&
organization.features.includes(productInfo.billingFlag)
)
.map(productInfo => {
return productInfo;
}),
[activePlan?.addOnCategories, organization.features]
);

useEffect(() => {
availableProducts.forEach(productInfo => {
const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`;
const enabled = subscription.addOns?.[productInfo.apiName]?.enabled;
formModel.setValue(addOnKey, enabled);
});
}, [availableProducts, subscription.addOns, formModel]);

return (
<Form
Expand Down Expand Up @@ -177,10 +191,10 @@ function PlanList({
<StyledFormSection>
<h4>Available Products</h4>
{availableProducts.map(productInfo => {
const addOnKey = `addOn${toTitleCase(productInfo.apiName)}`;
const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`;
return (
<CheckboxField
key={productInfo.productName}
key={productInfo.apiName}
data-test-id={`checkbox-${productInfo.productName}`}
label={toTitleCase(productInfo.productName)}
name={addOnKey}
Expand Down
1 change: 1 addition & 0 deletions static/gsApp/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export type ReservedBudgetCategory = {
export enum AddOnCategory {
SEER = 'seer',
PREVENT = 'prevent',
LEGACY_SEER = 'legacySeer',
}

export type AddOnCategoryInfo = {
Expand Down
52 changes: 27 additions & 25 deletions static/gsApp/views/amCheckout/steps/productSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,33 +96,35 @@ function ProductSelect({
);

const theme = useTheme();
const PRODUCT_CHECKOUT_INFO = {
[AddOnCategory.SEER]: {
color: theme.pink400 as Color,
gradientEndColor: theme.pink100 as Color,
buttonBorderColor: theme.pink200 as Color,
getProductDescription: (includedBudget: string) =>
getProductCheckoutDescription({
product: AddOnCategory.SEER,
isNewCheckout: !!isNewCheckout,
withPunctuation: false,
includedBudget,
}),
categoryInfo: {
[DataCategory.SEER_AUTOFIX]: {
description: t(
'Uses the latest AI models with Sentry data to find root causes & proposes PRs'
),
maxEventPriceDigits: 0,
},
[DataCategory.SEER_SCANNER]: {
description: t(
'Triages issues as they happen, automatically flagging highly-fixable ones for followup'
),
maxEventPriceDigits: 3,
},
const SEER_CHECKOUT_INFO = {
color: theme.pink400 as Color,
gradientEndColor: theme.pink100 as Color,
buttonBorderColor: theme.pink200 as Color,
getProductDescription: (includedBudget: string) =>
getProductCheckoutDescription({
product: AddOnCategory.SEER,
isNewCheckout: !!isNewCheckout,
withPunctuation: false,
includedBudget,
}),
categoryInfo: {
[DataCategory.SEER_AUTOFIX]: {
description: t(
'Uses the latest AI models with Sentry data to find root causes & proposes PRs'
),
maxEventPriceDigits: 0,
},
[DataCategory.SEER_SCANNER]: {
description: t(
'Triages issues as they happen, automatically flagging highly-fixable ones for followup'
),
maxEventPriceDigits: 3,
},
},
};
const PRODUCT_CHECKOUT_INFO = {
[AddOnCategory.SEER]: SEER_CHECKOUT_INFO,
[AddOnCategory.LEGACY_SEER]: SEER_CHECKOUT_INFO,
[AddOnCategory.PREVENT]: {
getProductDescription: (includedBudget: string) =>
getProductCheckoutDescription({
Expand Down
2 changes: 1 addition & 1 deletion tests/js/getsentry-test/fixtures/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ export const AM_ADD_ON_CATEGORIES = {
order: 2,
productName: 'prevent',
},
} satisfies Record<AddOnCategory, AddOnCategoryInfo>;
} satisfies Record<Exclude<AddOnCategory, AddOnCategory.LEGACY_SEER>, AddOnCategoryInfo>; // TODO(seer): Add LEGACY_SEER once the backend is updated to use the new value and the rest of the frontend can be updated
Loading