diff --git a/static/app/views/insights/crons/components/processingErrors/processingErrorItem.tsx b/static/app/views/insights/crons/components/processingErrors/processingErrorItem.tsx index 21f6b2871fc499..e50ac274c5f0d8 100644 --- a/static/app/views/insights/crons/components/processingErrors/processingErrorItem.tsx +++ b/static/app/views/insights/crons/components/processingErrors/processingErrorItem.tsx @@ -55,8 +55,10 @@ export function ProcessingErrorItem({error, checkinTooltip}: Props) { {checkinTooltip} ); case ProcessingErrorType.MONITOR_DISABLED_NO_QUOTA: + // TODO: this should really be a gsApp hook so we have subscription context, but + // for now we'll just default to "pay-as-you-go" since it's the modern term return tct( - 'A [checkinTooltip:check-in] upsert was sent, but due to insufficient quota a new monitor could not be enabled. Increase your Crons on-demand budget in your [link: subscription settings], and then enable this monitor.', + 'A [checkinTooltip:check-in] upsert was sent, but due to insufficient quota a new monitor could not be enabled. Increase your Crons pay-as-you-go budget in your [link: subscription settings], and then enable this monitor.', {checkinTooltip, link: } ); case ProcessingErrorType.MONITOR_INVALID_CONFIG: diff --git a/static/gsAdmin/components/customerStatus.tsx b/static/gsAdmin/components/customerStatus.tsx index 94757084e545eb..21cdacf9d11382 100644 --- a/static/gsAdmin/components/customerStatus.tsx +++ b/static/gsAdmin/components/customerStatus.tsx @@ -43,8 +43,6 @@ const getTooltip = ({planDetails, trialPlan}: Subscription) => ( )}
Base Price:
{formatCurrency(planDetails?.price)}
-
On-Demand:
-
{formatCurrency(planDetails?.onDemandEventPrice)} / event
Contract:
{planDetails?.contractInterval}
Billed:
diff --git a/static/gsAdmin/components/customers/customerHistory.tsx b/static/gsAdmin/components/customers/customerHistory.tsx index f25abe47797ee8..dcd74628ce3f77 100644 --- a/static/gsAdmin/components/customers/customerHistory.tsx +++ b/static/gsAdmin/components/customers/customerHistory.tsx @@ -32,7 +32,7 @@ function CustomerHistory({orgId, ...props}: Props) { columns={[ Period, - On-Demand + Pay-as-you-go , Reserved diff --git a/static/gsAdmin/components/customers/customerOverview.spec.tsx b/static/gsAdmin/components/customers/customerOverview.spec.tsx index ba8a291655b145..7fbccdcafa4792 100644 --- a/static/gsAdmin/components/customers/customerOverview.spec.tsx +++ b/static/gsAdmin/components/customers/customerOverview.spec.tsx @@ -190,20 +190,18 @@ describe('CustomerOverview', () => { expect(screen.getByText('Total: $0.00 / $3,000,000.00')).toBeInTheDocument(); // CPE information - expect(screen.getByText('Pay-as-you-go Cost-Per-Event Errors:')).toBeInTheDocument(); + expect(screen.getByText('On-Demand Cost-Per-Event Errors:')).toBeInTheDocument(); expect(screen.getByText('$0.12345678')).toBeInTheDocument(); expect( - screen.getByText('Pay-as-you-go Cost-Per-Event Performance units:') + screen.getByText('On-Demand Cost-Per-Event Performance units:') ).toBeInTheDocument(); expect(screen.getByText('$1.00000000')).toBeInTheDocument(); - expect(screen.getByText('Pay-as-you-go Cost-Per-Event Replays:')).toBeInTheDocument(); + expect(screen.getByText('On-Demand Cost-Per-Event Replays:')).toBeInTheDocument(); expect(screen.getByText('$0.50000000')).toBeInTheDocument(); - expect( - screen.getByText('Pay-as-you-go Cost-Per-Event Attachments:') - ).toBeInTheDocument(); + expect(screen.getByText('On-Demand Cost-Per-Event Attachments:')).toBeInTheDocument(); expect(screen.getByText('$0.20300000')).toBeInTheDocument(); expect( - screen.getByText('Pay-as-you-go Cost-Per-Event Cron monitors:') + screen.getByText('On-Demand Cost-Per-Event Cron monitors:') ).toBeInTheDocument(); expect(screen.getByText('$0.07550000')).toBeInTheDocument(); }); diff --git a/static/gsAdmin/components/customers/customerOverview.tsx b/static/gsAdmin/components/customers/customerOverview.tsx index 383b8dd3b61ecd..a7ea8693073b89 100644 --- a/static/gsAdmin/components/customers/customerOverview.tsx +++ b/static/gsAdmin/components/customers/customerOverview.tsx @@ -32,6 +32,7 @@ import type { } from 'getsentry/types'; import {BillingType, OnDemandBudgetMode} from 'getsentry/types'; import { + displayBudgetName, formatBalance, formatReservedWithUnits, getActiveProductTrial, @@ -130,6 +131,7 @@ function SubscriptionSummary({customer, onAction}: SubscriptionSummaryProps) { {customer.contractInterval} )} + {/* TODO(billing): Should we start calling On-Demand periods "Pay-as-you-go" periods? */} @@ -213,7 +215,9 @@ function ReservedData({customer}: ReservedDataProps) { : 'None'} {customer.onDemandInvoicedManual && ( - + {typeof categoryHistory.paygCpe === 'number' ? displayPriceWithCents({ cents: categoryHistory.paygCpe, diff --git a/static/gsAdmin/components/customers/customerStatsFilters.tsx b/static/gsAdmin/components/customers/customerStatsFilters.tsx index 2ef00cca06f8c8..6cc88ca35f2ca1 100644 --- a/static/gsAdmin/components/customers/customerStatsFilters.tsx +++ b/static/gsAdmin/components/customers/customerStatsFilters.tsx @@ -92,6 +92,7 @@ export function CustomerStatsFilters({ const {start, end, period, utc} = pageDateTime; + // TODO(billing): Should we start calling On-Demand periods "Pay-as-you-go" periods? const onDemandLabel = ( On-Demand ( diff --git a/static/gsAdmin/components/customers/pendingChanges.spec.tsx b/static/gsAdmin/components/customers/pendingChanges.spec.tsx index 16cecdf8a12994..5c415eea713e0d 100644 --- a/static/gsAdmin/components/customers/pendingChanges.spec.tsx +++ b/static/gsAdmin/components/customers/pendingChanges.spec.tsx @@ -46,6 +46,7 @@ describe('PendingChanges', () => { name: 'Team (Enterprise)', contractInterval: 'annual', billingInterval: 'annual', + budgetTerm: 'on-demand', }), plan: 'am1_team_ent', planName: 'Team (Enterprise)', @@ -87,7 +88,7 @@ describe('PendingChanges', () => { expect(container).toHaveTextContent( 'The following changes will take effect on Feb 16, 2022' ); - expect(container).toHaveTextContent('On-demand maximum — $0.00 → $500.00'); + expect(container).toHaveTextContent('On-Demand maximum — $0.00 → $500.00'); }); it('renders pending changes with all categories', () => { @@ -100,6 +101,7 @@ describe('PendingChanges', () => { name: 'Team (Enterprise)', contractInterval: 'annual', billingInterval: 'annual', + budgetTerm: 'on-demand', }), plan: 'am3_team_ent', planName: 'Team (Enterprise)', @@ -140,7 +142,7 @@ describe('PendingChanges', () => { expect(container).toHaveTextContent( 'The following changes will take effect on Feb 20, 2024' ); - expect(container).toHaveTextContent('On-demand maximum — $0.00 → $500.00'); + expect(container).toHaveTextContent('On-Demand maximum — $0.00 → $500.00'); }); it('renders on-demand budgets', () => { @@ -189,7 +191,7 @@ describe('PendingChanges', () => { 'The following changes will take effect on Feb 16, 2022' ); expect(container).toHaveTextContent( - 'On-demand budget — shared on-demand budget of $100 → per-category on-demand budget (errors at $3, transactions at $2, and attachments at $1)' + 'On-Demand Budget — shared on-demand budget of $100 → per-category on-demand budget (errors at $3, transactions at $2, and attachments at $1)' ); }); @@ -234,7 +236,7 @@ describe('PendingChanges', () => { ); expect(container).toHaveTextContent('Plan changes — Developer → Team (Enterprise)'); expect(container).toHaveTextContent( - 'On-demand budget — shared on-demand budget of $100 → per-category on-demand budget (errors at $3, transactions at $2, and attachments at $1)' + 'On-Demand Budget — shared on-demand budget of $100 → per-category on-demand budget (errors at $3, transactions at $2, and attachments at $1)' ); expect(screen.getAllByText(/The following changes will take effect on/)).toHaveLength( 1 diff --git a/static/gsAdmin/components/customers/pendingChanges.tsx b/static/gsAdmin/components/customers/pendingChanges.tsx index 9b174a06eb93e2..94508208364239 100644 --- a/static/gsAdmin/components/customers/pendingChanges.tsx +++ b/static/gsAdmin/components/customers/pendingChanges.tsx @@ -10,7 +10,7 @@ import {DataCategory} from 'sentry/types/core'; import {RESERVED_BUDGET_QUOTA} from 'getsentry/constants'; import {usePlanMigrations} from 'getsentry/hooks/usePlanMigrations'; import type {Plan, PlanMigration, Subscription} from 'getsentry/types'; -import {formatReservedWithUnits} from 'getsentry/utils/billing'; +import {displayBudgetName, formatReservedWithUnits} from 'getsentry/utils/billing'; import { getPlanCategoryName, getReservedBudgetDisplayName, @@ -307,7 +307,8 @@ function getOnDemandChanges(subscription: Subscription) { ); changes.push( - On-demand budget — {current} → {change} + {displayBudgetName(pendingChanges.planDetails, {title: true, withBudget: true})}{' '} + — {current} → {change} ); } @@ -316,7 +317,8 @@ function getOnDemandChanges(subscription: Subscription) { const change = getStringForPrice(pendingChanges.onDemandMaxSpend); changes.push( - On-demand maximum — {old} → {change} + {displayBudgetName(pendingChanges.planDetails, {title: true})} maximum — {old} →{' '} + {change} ); } diff --git a/static/gsAdmin/components/provisionSubscriptionAction.spec.tsx b/static/gsAdmin/components/provisionSubscriptionAction.spec.tsx index 68c2bdbad17645..28ce92d7d96c00 100644 --- a/static/gsAdmin/components/provisionSubscriptionAction.spec.tsx +++ b/static/gsAdmin/components/provisionSubscriptionAction.spec.tsx @@ -281,22 +281,22 @@ describe('provisionSubscriptionAction', () => { ); expect( - within(container).getByLabelText('On-Demand Max Spend Setting') + within(container).getByLabelText('Pay-as-you-go Max Spend Setting') ).toBeInTheDocument(); expect( - within(container).queryByLabelText('On-Demand Cost-Per-Event Errors') + within(container).queryByLabelText('Pay-as-you-go Cost-Per-Event Errors') ).not.toBeInTheDocument(); expect( - within(container).queryByLabelText('On-Demand Cost-Per-Event Performance Units') + within(container).queryByLabelText('Pay-as-you-go Cost-Per-Event Performance Units') ).not.toBeInTheDocument(); expect( - within(container).queryByLabelText('On-Demand Cost-Per-Event Replays') + within(container).queryByLabelText('Pay-as-you-go Cost-Per-Event Replays') ).not.toBeInTheDocument(); expect( - within(container).queryByLabelText('On-Demand Cost-Per-Event Attachments') + within(container).queryByLabelText('Pay-as-you-go Cost-Per-Event Attachments') ).not.toBeInTheDocument(); expect( - within(container).queryByLabelText('On-Demand Cost-Per-Event Cron Monitors') + within(container).queryByLabelText('Pay-as-you-go Cost-Per-Event Cron Monitors') ).not.toBeInTheDocument(); }); @@ -325,19 +325,19 @@ describe('provisionSubscriptionAction', () => { ).not.toBeInTheDocument(); await selectEvent.select( - screen.getByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + screen.getByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Shared' ); expect( - (await within(container).findAllByLabelText(/On-Demand Cost-Per-Event/)).length + (await within(container).findAllByLabelText(/Pay-as-you-go Cost-Per-Event/)).length ).toBeGreaterThan(0); await selectEvent.select( - screen.getByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + screen.getByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Disable' ); expect( - within(container).queryByLabelText(/On-Demand Cost-Per-Event/) + within(container).queryByLabelText(/Pay-as-you-go Cost-Per-Event/) ).not.toBeInTheDocument(); }); @@ -363,7 +363,7 @@ describe('provisionSubscriptionAction', () => { 'Invoiced' ); await selectEvent.select( - screen.getByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + screen.getByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Shared' ); const disabledSoftCapFields = screen.getAllByLabelText(/Soft Cap Type/); @@ -389,7 +389,7 @@ describe('provisionSubscriptionAction', () => { 'Invoiced' ); await selectEvent.select( - screen.getByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + screen.getByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Disable' ); const enabledSoftCapFields = screen.getAllByLabelText(/Soft Cap Type/); @@ -845,21 +845,21 @@ describe('provisionSubscriptionAction', () => { 'Invoiced' ); await selectEvent.select( - await screen.findByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + await screen.findByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Shared' ); expect( - within(container).getByLabelText('Retain On-Demand Budget') + within(container).getByLabelText('Retain Pay-as-you-go Budget') ).toBeInTheDocument(); await selectEvent.select( - await screen.findByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + await screen.findByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Per Category' ); expect( - within(container).queryByLabelText('Retain On-Demand Budget') + within(container).queryByLabelText('Retain Pay-as-you-go Budget') ).not.toBeInTheDocument(); await selectEvent.select( @@ -1251,7 +1251,7 @@ describe('provisionSubscriptionAction', () => { ); await selectEvent.select( - await screen.findByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + await screen.findByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Disable' ); @@ -1380,7 +1380,7 @@ describe('provisionSubscriptionAction', () => { ); await selectEvent.select( - await screen.findByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + await screen.findByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Disable' ); @@ -1500,7 +1500,7 @@ describe('provisionSubscriptionAction', () => { ); await selectEvent.select( - await screen.findByRole('textbox', {name: 'On-Demand Max Spend Setting'}), + await screen.findByRole('textbox', {name: 'Pay-as-you-go Max Spend Setting'}), 'Disable' ); diff --git a/static/gsAdmin/components/provisionSubscriptionAction.tsx b/static/gsAdmin/components/provisionSubscriptionAction.tsx index 367883c6449150..7d7312003fafcc 100644 --- a/static/gsAdmin/components/provisionSubscriptionAction.tsx +++ b/static/gsAdmin/components/provisionSubscriptionAction.tsx @@ -32,6 +32,7 @@ import { type Subscription, } from 'getsentry/types'; import { + displayBudgetName, getAmPlanTier, isAm3DsPlan, isAm3Plan, @@ -376,8 +377,8 @@ class ProvisionSubscriptionModal extends Component { this.state.data.seerBudget; /** - * If the user is changing the on-demand max spend mode or disabling it, - * don't retain the customer's existing on-demand max spend settings. + * If the user is changing the PAYG max spend mode or disabling it, + * don't retain the customer's existing PAYG max spend settings. */ disableRetainOnDemand = () => { if (this.state.data.onDemandInvoicedManual === null) { @@ -442,7 +443,7 @@ class ProvisionSubscriptionModal extends Component { } }); - // remove on-demand fields if the plan is not invoiced + // remove PAYG fields if the plan is not invoiced if (postData.type !== 'invoiced') { delete postData.onDemandInvoicedManual; const paygCpeFields = Object.keys(postData).filter(key => @@ -462,7 +463,7 @@ class ProvisionSubscriptionModal extends Component { })); } - // soft cap and on-demand max spend are mutually exclusive + // soft cap and PAYG max spend are mutually exclusive if (this.isEnablingOnDemandMaxSpend()) { Object.keys(postData).forEach(key => { if (key.startsWith('softCapType')) { @@ -534,7 +535,7 @@ class ProvisionSubscriptionModal extends Component { } } - // override retainOnDemandBudget based on whether user is changing the mode or disabling on-demand, or not + // override retainOnDemandBudget based on whether user is changing the mode or disabling PAYG, or not postData.retainOnDemandBudget = postData.retainOnDemandBudget ? !this.disableRetainOnDemand() : false; @@ -620,6 +621,7 @@ class ProvisionSubscriptionModal extends Component { const isAm3Ds = isAm3DsPlan(data.plan); const hasCustomSkuPrices = isAmEnt; const hasCustomPrice = hasCustomSkuPrices || !!data.managed; // Refers to ACV + const selectedPlan = this.state.provisionablePlans[data.plan]; if (this.state.isLoading) { return ; @@ -771,7 +773,7 @@ class ProvisionSubscriptionModal extends Component { /> {this.state.data.type === 'invoiced' && ( { ['DISABLE', 'Disable'], ] } - help="Used to enable (Shared or Per Category) or disable on-demand max spend for invoiced customers. Cannot be provisioned with soft cap." + help={`Used to enable (Shared or Per Category) or disable ${selectedPlan ? displayBudgetName(selectedPlan) : 'pay-as-you-go'} max spend for invoiced customers. Cannot be provisioned with soft cap.`} clearable disabled={ this.state.data.type === 'credit_card' || this.isEnablingSoftCap() @@ -802,10 +804,10 @@ class ProvisionSubscriptionModal extends Component { {!this.disableRetainOnDemand() && ( this.setState(state => ({ ...state, @@ -817,252 +819,235 @@ class ProvisionSubscriptionModal extends Component { } /> )} - {this.state.data.plan && - (this.state.provisionablePlans[this.state.data.plan]?.categories - .length ?? 0) > 0 && ( - - Plan Quotas - - Monthly quantities for each SKU - - {this.state.data.plan && - ( - this.state.provisionablePlans[this.state.data.plan] - ?.categories ?? [] - ).map(category => { - const categoryInfo = getCategoryInfoFromPlural(category); - if (!categoryInfo) { - return null; - } - const titleName = getPlanCategoryName({ - plan: this.state.provisionablePlans[this.state.data.plan], - category, - title: true, - hadCustomDynamicSampling: isAm3Ds, - }); - const suffix = isByteCategory(category) ? ' (in GB)' : ''; - const capitalizedApiName = this.capitalizeForApiName( - categoryInfo.plural - ); - return ( - - 0 && ( + + Plan Quotas + + Monthly quantities for each SKU + + {selectedPlan?.categories.map(category => { + const categoryInfo = getCategoryInfoFromPlural(category); + if (!categoryInfo) { + return null; + } + const titleName = getPlanCategoryName({ + plan: selectedPlan, + category, + title: true, + hadCustomDynamicSampling: isAm3Ds, + }); + const suffix = isByteCategory(category) ? ' (in GB)' : ''; + const capitalizedApiName = this.capitalizeForApiName( + categoryInfo.plural + ); + return ( + + + this.setState(state => ({ + ...state, + data: { + ...state.data, + [`reserved${capitalizedApiName}`]: v, + }, + })) + } + /> + + this.setState(state => ({ + ...state, + data: { + ...state.data, + [`softCapType${capitalizedApiName}`]: v ? v : null, + }, + })) + } + /> + {this.isReservedBudgetCategory(isAm3Ds, category) && ( + + this.setState(state => ({ + ...state, + data: { + ...state.data, + [`reservedCpe${capitalizedApiName}`]: v, + [`reserved${capitalizedApiName}`]: + RESERVED_BUDGET_QUOTA, + }, + })) + } + onBlur={() => { + const currentValue = parseFloat( this.state.data[`reservedCpe${capitalizedApiName}`] - } - value={this.state.data[`reserved${capitalizedApiName}`]} - onChange={v => + ); + if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, - [`reserved${capitalizedApiName}`]: v, + [`reservedCpe${capitalizedApiName}`]: + currentValue.toFixed(CPE_DECIMAL_PRECISION), }, - })) - } - /> - + }} + /> + )} + {this.isEnablingOnDemandMaxSpend() && ( + + this.setState(state => ({ + ...state, + data: { + ...state.data, + [`paygCpe${capitalizedApiName}`]: v, + }, + })) + } + required + onBlur={() => { + const currentValue = parseFloat( + this.state.data[`paygCpe${capitalizedApiName}`] + ); + if (!isNaN(currentValue)) { this.setState(state => ({ ...state, data: { ...state.data, - [`softCapType${capitalizedApiName}`]: v ? v : null, + [`paygCpe${capitalizedApiName}`]: + currentValue.toFixed(CPE_DECIMAL_PRECISION), }, - })) + })); } - /> - {this.isReservedBudgetCategory(isAm3Ds, category) && ( - - this.setState(state => ({ - ...state, - data: { - ...state.data, - [`reservedCpe${capitalizedApiName}`]: v, - [`reserved${capitalizedApiName}`]: - RESERVED_BUDGET_QUOTA, - }, - })) - } - onBlur={() => { - const currentValue = parseFloat( - this.state.data[`reservedCpe${capitalizedApiName}`] - ); - if (!isNaN(currentValue)) { - this.setState(state => ({ - ...state, - data: { - ...state.data, - [`reservedCpe${capitalizedApiName}`]: - currentValue.toFixed(CPE_DECIMAL_PRECISION), - }, - })); - } - }} - /> - )} - {this.isEnablingOnDemandMaxSpend() && ( - - this.setState(state => ({ - ...state, - data: { - ...state.data, - [`paygCpe${capitalizedApiName}`]: v, - }, - })) - } - required - onBlur={() => { - const currentValue = parseFloat( - this.state.data[`paygCpe${capitalizedApiName}`] - ); - if (!isNaN(currentValue)) { - this.setState(state => ({ - ...state, - data: { - ...state.data, - [`paygCpe${capitalizedApiName}`]: - currentValue.toFixed(CPE_DECIMAL_PRECISION), - }, - })); - } - }} - /> - )} - - ); - })} - {this.isSettingSeerBudget() && ( - - this.setState(state => ({ - ...state, - data: { - ...state.data, - seerBudget: v, - }, - })) - } - /> - )} - {isAm3DsPlan(this.state.data.plan) && ( - - this.setState(state => ({ - ...state, - data: { - ...state.data, - dynamicSamplingBudget: v, - }, - })) - } - /> - )} - - )} + }} + /> + )} + + ); + })} + {this.isSettingSeerBudget() && ( + + this.setState(state => ({ + ...state, + data: { + ...state.data, + seerBudget: v, + }, + })) + } + /> + )} + {isAm3DsPlan(selectedPlan.id) && ( + + this.setState(state => ({ + ...state, + data: { + ...state.data, + dynamicSamplingBudget: v, + }, + })) + } + /> + )} + + )}
Reserved Volume Prices Annual prices for reserved volumes, in whole dollars. - {this.state.data.plan && - this.state.provisionablePlans[this.state.data.plan]?.categories.map( - category => { - const categoryInfo = getCategoryInfoFromPlural(category); - if (!categoryInfo) { - return null; + {selectedPlan?.categories.map(category => { + const categoryInfo = getCategoryInfoFromPlural(category); + if (!categoryInfo) { + return null; + } + const titleName = getPlanCategoryName({ + plan: selectedPlan, + category, + title: true, + hadCustomDynamicSampling: isAm3Ds, + }); + const settingReservedBudget = this.isSettingReservedBudget(category); + const isDisabled = + settingReservedBudget && + (category === DataCategory.SPANS_INDEXED || + category === DataCategory.SEER_SCANNER); + const suffix = + settingReservedBudget && + (category === DataCategory.SPANS || + category === DataCategory.SEER_AUTOFIX) + ? ` (${toTitleCase( + Object.values( + selectedPlan?.availableReservedBudgetTypes ?? {} + ).find(budgetInfo => + budgetInfo.dataCategories.includes(category) + )?.productName ?? '' + )} ARR)` + : ''; + const capitalizedApiName = this.capitalizeForApiName( + categoryInfo.plural + ); + return ( + + this.setState(state => ({ + ...state, + data: { + ...state.data, + [`customPrice${capitalizedApiName}`]: v, + }, + })) } - const titleName = getPlanCategoryName({ - plan: this.state.provisionablePlans[this.state.data.plan], - category, - title: true, - hadCustomDynamicSampling: isAm3Ds, - }); - const settingReservedBudget = - this.isSettingReservedBudget(category); - const isDisabled = - settingReservedBudget && - (category === DataCategory.SPANS_INDEXED || - category === DataCategory.SEER_SCANNER); - const suffix = - settingReservedBudget && - (category === DataCategory.SPANS || - category === DataCategory.SEER_AUTOFIX) - ? ` (${toTitleCase( - Object.values( - this.state.provisionablePlans[this.state.data.plan] - ?.availableReservedBudgetTypes ?? {} - ).find(budgetInfo => - budgetInfo.dataCategories.includes(category) - )?.productName ?? '' - )} ARR)` - : ''; - const capitalizedApiName = this.capitalizeForApiName( - categoryInfo.plural - ); - return ( - - this.setState(state => ({ - ...state, - data: { - ...state.data, - [`customPrice${capitalizedApiName}`]: v, - }, - })) - } - /> - ); - } - )} + /> + ); + })} { expect(mockApiCall).toHaveBeenCalled(); expect( await screen.findByText( - "Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please ask your organization's owner or billing manager to set up an on-demand budget for cron monitoring." + "Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please ask your organization's owner or billing manager to set up on-demand for cron monitoring." ) ).toBeInTheDocument(); diff --git a/static/gsApp/components/crons/cronsBillingBanner.tsx b/static/gsApp/components/crons/cronsBillingBanner.tsx index 147b8cfe58a419..9425380d198e8e 100644 --- a/static/gsApp/components/crons/cronsBillingBanner.tsx +++ b/static/gsApp/components/crons/cronsBillingBanner.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import {Alert} from 'sentry/components/core/alert'; -import {t, tct, tn} from 'sentry/locale'; +import {tct, tn} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import getDaysSinceDate from 'sentry/utils/getDaysSinceDate'; import {useApiQuery} from 'sentry/utils/queryClient'; @@ -12,8 +12,13 @@ import { } from 'getsentry/components/crons/cronsBannerUpgradeCTA'; import withSubscription from 'getsentry/components/withSubscription'; import {useBillingConfig} from 'getsentry/hooks/useBillingConfig'; -import type {BillingConfig, MonitorCountResponse, Subscription} from 'getsentry/types'; -import {getTrialDaysLeft} from 'getsentry/utils/billing'; +import type { + BillingConfig, + MonitorCountResponse, + Plan, + Subscription, +} from 'getsentry/types'; +import {displayBudgetName, getTrialDaysLeft} from 'getsentry/utils/billing'; interface Props { organization: Organization; @@ -53,7 +58,7 @@ export function CronsBillingBanner({organization, subscription}: Props) { return null; } - // Show alert for when we have disabled all monitors due to insufficient on-demand + // Show alert for when we have disabled all monitors due to insufficient PAYG if ( data.enabledMonitorCount === 0 && data.overQuotaMonitorCount > 0 && @@ -84,13 +89,18 @@ export function CronsBillingBanner({organization, subscription}: Props) { ); } - // If the user's trial has ended and they aren't on a plan with on-demand + // If the user's trial has ended and they aren't on a plan with PAYG if ( daysSinceTrial >= 0 && daysSinceTrial <= 7 && !subscription.planDetails.allowOnDemand ) { - return ; + return ( + + ); } return null; @@ -131,15 +141,17 @@ function TrialEndingBanner({ ); } -function TrialEndedBanner({hasBillingAccess}: BannerProps) { +function TrialEndedBanner({hasBillingAccess, plan}: BannerProps & {plan: Plan}) { return ( {hasBillingAccess - ? t( - 'Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please increase your on-demand budget.' + ? tct( + 'Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please increase your [budgetTerm].', + {budgetTerm: displayBudgetName(plan, {withBudget: true})} ) - : t( - "Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please ask your organization's owner or billing manager to set up an on-demand budget for cron monitoring." + : tct( + "Your free business trial has ended. One cron job monitor is included in your current plan. If you want to monitor more than one cron job, please ask your organization's owner or billing manager to set up [budgetTerm] for cron monitoring.", + {budgetTerm: displayBudgetName(plan)} )} ); diff --git a/static/gsApp/components/cronsOnDemandStepWarning.tsx b/static/gsApp/components/cronsOnDemandStepWarning.tsx index f12e8729ebbd6a..96932934cc3d6a 100644 --- a/static/gsApp/components/cronsOnDemandStepWarning.tsx +++ b/static/gsApp/components/cronsOnDemandStepWarning.tsx @@ -28,7 +28,7 @@ export function CronsOnDemandStepWarning({ let reserved: number | null | undefined; let cronsPrice: number | null | undefined; if (isEnterprise(activePlan.id)) { - // this can only be reached for enterprise customers with invoiced on-demand + // this can only be reached for enterprise customers with invoiced PAYG // we want to make sure we use their actual reserved amount and not the minimum // for enterprise plans reserved = subscription.categories[cronCategoryName]?.reserved; diff --git a/static/gsApp/components/gsBanner.tsx b/static/gsApp/components/gsBanner.tsx index b492081247124c..b07c3ead3c94a2 100644 --- a/static/gsApp/components/gsBanner.tsx +++ b/static/gsApp/components/gsBanner.tsx @@ -722,7 +722,7 @@ class GSBanner extends Component { get overageWarningActive(): Record { const {subscription} = this.props; - // disable warnings if org has on-demand + // disable warnings if org has PAYG if ( subscription.hasOverageNotificationsDisabled || subscription.onDemandMaxSpend > 0 diff --git a/static/gsApp/components/profiling/alerts.tsx b/static/gsApp/components/profiling/alerts.tsx index 4a5db0586d25f4..80c19f9b78a4fd 100644 --- a/static/gsApp/components/profiling/alerts.tsx +++ b/static/gsApp/components/profiling/alerts.tsx @@ -310,29 +310,27 @@ function ContinuousProfilingBetaAlertBannerInner({ } > {subscription.isFree - ? isAm2Plan(subscription.plan) - ? tct( - '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. Profiling will require a on-demand budget after this date. To avoid disruptions, upgrade to a paid plan.', - {bold: } - ) - : tct( - '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. Profiling will require a pay-as-you-go budget after this date. To avoid disruptions, upgrade to a paid plan.', - {bold: } - ) + ? tct( + '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. Profiling will require a [budgetTerm] after this date. To avoid disruptions, upgrade to a paid plan.', + { + bold: , + budgetTerm: displayBudgetName(subscription.planDetails, {withBudget: true}), + } + ) : isEnterprise(subscription.plan) ? tct( '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. To avoid disruptions, contact your account manager before then to add it to your plan.', {bold: } ) - : isAm2Plan(subscription.plan) - ? tct( - '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. Profiling will require an on-demand budget after this date.', - {bold: } - ) - : tct( - '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. Profiling will require a pay-as-you-go budget after this date.', - {bold: } - )} + : tct( + '[bold:Profiling Beta Ending Soon:] Your free access ends May 19, 2025. Profiling will require a [budgetTerm] after this date.', + { + bold: , + budgetTerm: displayBudgetName(subscription.planDetails, { + withBudget: true, + }), + } + )} ); } diff --git a/static/gsApp/views/amCheckout/billingCycleSelectCard.tsx b/static/gsApp/views/amCheckout/billingCycleSelectCard.tsx index 46b11d9c53e2f0..dfe90bce224152 100644 --- a/static/gsApp/views/amCheckout/billingCycleSelectCard.tsx +++ b/static/gsApp/views/amCheckout/billingCycleSelectCard.tsx @@ -74,7 +74,7 @@ function BillingCycleSelectCard({ ? tct('[budgetTerm] usage billed monthly, discount does not apply', { budgetTerm: plan.budgetTerm === 'pay-as-you-go' - ? 'PAYG' + ? displayBudgetName(plan, {abbreviated: true}) : displayBudgetName(plan, {title: true}), }) : t('Cancel anytime'); diff --git a/static/gsApp/views/amCheckout/cartDiff.tsx b/static/gsApp/views/amCheckout/cartDiff.tsx index 773b2aadf09961..39f8434c02ad7e 100644 --- a/static/gsApp/views/amCheckout/cartDiff.tsx +++ b/static/gsApp/views/amCheckout/cartDiff.tsx @@ -21,7 +21,11 @@ import { type SharedOnDemandBudget, type Subscription, } from 'getsentry/types'; -import {formatReservedWithUnits, isNewPayingCustomer} from 'getsentry/utils/billing'; +import { + displayBudgetName, + formatReservedWithUnits, + isNewPayingCustomer, +} from 'getsentry/utils/billing'; import {getPlanCategoryName} from 'getsentry/utils/dataCategory'; import type {CheckoutFormData} from 'getsentry/views/amCheckout/types'; import * as utils from 'getsentry/views/amCheckout/utils'; @@ -210,9 +214,14 @@ function OnDemandDiff({ if (index === 0) { leftComponent = ( - {newPlan.budgetTerm === 'pay-as-you-go' - ? t('PAYG spend limit') - : t('Shared spend limit')} + {tct('[budgetTerm] spend limit', { + budgetTerm: displayBudgetName( + newPlan, + newPlan.budgetTerm === 'pay-as-you-go' + ? {abbreviated: true} + : {title: true} + ), + })} ); } diff --git a/static/gsApp/views/amCheckout/checkoutOverview.tsx b/static/gsApp/views/amCheckout/checkoutOverview.tsx index 092c67089ce663..cfbf6ae4712014 100644 --- a/static/gsApp/views/amCheckout/checkoutOverview.tsx +++ b/static/gsApp/views/amCheckout/checkoutOverview.tsx @@ -18,7 +18,7 @@ import type { Subscription, } from 'getsentry/types'; import {OnDemandBudgetMode} from 'getsentry/types'; -import {formatReservedWithUnits} from 'getsentry/utils/billing'; +import {displayBudgetName, formatReservedWithUnits} from 'getsentry/utils/billing'; import {getPlanCategoryName} from 'getsentry/utils/dataCategory'; import formatCurrency from 'getsentry/utils/formatCurrency'; import { @@ -156,19 +156,21 @@ class CheckoutOverview extends Component { return null; } - let title = t('On-Demand'); + let prefix = ''; if (displayNewOnDemandBudgetsUI) { if (onDemandBudget) { if (onDemandBudget.budgetMode === OnDemandBudgetMode.SHARED) { - title = t('Shared On-Demand'); + prefix = t('Shared '); } if (onDemandBudget.budgetMode === OnDemandBudgetMode.PER_CATEGORY) { - title = t('Per-Category On-Demand'); + prefix = t('Per-Category '); } } } + const title = `${prefix}${displayBudgetName(activePlan, {title: true})}`; + const details: React.ReactNode[] = []; if ( @@ -328,13 +330,14 @@ class CheckoutOverview extends Component { {onDemandBudget && getTotalBudget(onDemandBudget) > 0 ? ( - {tct('+ On-Demand charges up to [amount][break] based on usage', { + {tct('+ [budgetTerm] charges up to [amount][break] based on usage', { + budgetTerm: displayBudgetName(activePlan, {title: true}), amount: `${formatCurrency(getTotalBudget(onDemandBudget))}/mo`, break:
, })}
) : ( - // Placeholder to avoid jumping when on-demand charges are added + // Placeholder to avoid jumping when PAYG charges are added
)}
diff --git a/static/gsApp/views/amCheckout/checkoutOverviewV2.tsx b/static/gsApp/views/amCheckout/checkoutOverviewV2.tsx index 5b7d9decdf1624..bea7a92fd01460 100644 --- a/static/gsApp/views/amCheckout/checkoutOverviewV2.tsx +++ b/static/gsApp/views/amCheckout/checkoutOverviewV2.tsx @@ -16,6 +16,7 @@ import {toTitleCase} from 'sentry/utils/string/toTitleCase'; import {PAYG_BUSINESS_DEFAULT, PAYG_TEAM_DEFAULT} from 'getsentry/constants'; import type {BillingConfig, Plan, Promotion, Subscription} from 'getsentry/types'; import { + displayBudgetName, formatReservedWithUnits, getReservedBudgetCategoryForAddOn, isBizPlanFamily, @@ -83,7 +84,12 @@ function CheckoutOverviewV2({activePlan, formData, onUpdate: _onUpdate}: Props) {t('Additional Coverage')} - {t('Pay-as-you-go (PAYG) Budget')} + + {tct('[budgetTerm] ([abbreviation]) Budget', { + budgetTerm: displayBudgetName(activePlan, {title: true}), + abbreviation: displayBudgetName(activePlan, {abbreviated: true}), + })} + {t('Charges are applied at the end of your monthly usage cycle.')} @@ -259,11 +265,12 @@ function CheckoutOverviewV2({activePlan, formData, onUpdate: _onUpdate}: Props) ) : null} @@ -330,7 +337,8 @@ function CheckoutOverviewV2({activePlan, formData, onUpdate: _onUpdate}: Props) diff --git a/static/gsApp/views/amCheckout/steps/addDataVolume.tsx b/static/gsApp/views/amCheckout/steps/addDataVolume.tsx index e3e2dcfe55142e..5307952036d98d 100644 --- a/static/gsApp/views/amCheckout/steps/addDataVolume.tsx +++ b/static/gsApp/views/amCheckout/steps/addDataVolume.tsx @@ -11,7 +11,7 @@ import {t, tct} from 'sentry/locale'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; import {PlanTier} from 'getsentry/types'; -import {isAmPlan} from 'getsentry/utils/billing'; +import {displayBudgetName, isAmPlan} from 'getsentry/utils/billing'; import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics'; import StepHeader from 'getsentry/views/amCheckout/steps/stepHeader'; import VolumeSliders from 'getsentry/views/amCheckout/steps/volumeSliders'; @@ -83,7 +83,8 @@ function AddDataVolume({ {isLegacy && (
- {tct('Need more data? Add On-Demand Budget, or [link:Contact Sales]', { + {tct('Need more data? Add [budgetTerm], or [link:Contact Sales]', { + budgetTerm: displayBudgetName(activePlan, {title: true, withBudget: true}), link: , })}
diff --git a/static/gsApp/views/amCheckout/steps/onDemandBudgets.tsx b/static/gsApp/views/amCheckout/steps/onDemandBudgets.tsx index 90b0e5fc842066..1829d920729514 100644 --- a/static/gsApp/views/amCheckout/steps/onDemandBudgets.tsx +++ b/static/gsApp/views/amCheckout/steps/onDemandBudgets.tsx @@ -10,7 +10,7 @@ import {space} from 'sentry/styles/space'; import type {OnDemandBudgets} from 'getsentry/types'; import {OnDemandBudgetMode} from 'getsentry/types'; -import {isDeveloperPlan} from 'getsentry/utils/billing'; +import {displayBudgetName, isDeveloperPlan} from 'getsentry/utils/billing'; import StepHeader from 'getsentry/views/amCheckout/steps/stepHeader'; import type {StepProps} from 'getsentry/views/amCheckout/types'; import {getReservedPriceCents} from 'getsentry/views/amCheckout/utils'; @@ -24,10 +24,10 @@ import { type Props = StepProps; type State = { - // Once the on-demand budget is updated, we no longer suggest a new default on-demand value, + // Once the PAYG budget is updated, we no longer suggest a new default PAYG value, // regardless of whether there are further changes in the reserved value. // This is because if the user has seen and updated or clicked "Continue", - // that is considered the final on-demand value unless the user updates the value themselves. + // that is considered the final PAYG value unless the user updates the value themselves. isUpdated: boolean; }; @@ -40,7 +40,12 @@ class OnDemandBudgetsStep extends Component { state: State; get title() { - return t('On-Demand Budgets'); + const {activePlan} = this.props; + return displayBudgetName(activePlan, { + title: true, + withBudget: true, + pluralOndemand: true, + }); } setBudgetMode = (nextMode: OnDemandBudgetMode) => { @@ -123,9 +128,13 @@ class OnDemandBudgetsStep extends Component { return (
- {tct('Need more info? [link:See on-demand pricing chart]', { + {tct('Need more info? [link]', { link: ( - + + {tct('See [budgetTerm] pricing chart', { + budgetTerm: displayBudgetName(this.props.activePlan), + })} + ), })}
diff --git a/static/gsApp/views/amCheckout/steps/onDemandSpend.spec.tsx b/static/gsApp/views/amCheckout/steps/onDemandSpend.spec.tsx index e659f61448c081..abb17a6d7aa796 100644 --- a/static/gsApp/views/amCheckout/steps/onDemandSpend.spec.tsx +++ b/static/gsApp/views/amCheckout/steps/onDemandSpend.spec.tsx @@ -100,7 +100,7 @@ describe('OnDemandSpend', () => { await openPanel(); expect(screen.getByRole('textbox', {name: 'Monthly Max'})).toBeEnabled(); - expect(screen.queryByLabelText(/On-demand is not supported/)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/On-Demand is not supported/)).not.toBeInTheDocument(); expect(screen.getByPlaceholderText('e.g. 50')).toBeInTheDocument(); // Can type into the input. @@ -178,7 +178,7 @@ describe('OnDemandSpend', () => { // Check tooltip await userEvent.hover(screen.getByRole('textbox', {name: 'Monthly Max'})); - expect(await screen.findByText(/On-demand is not supported/)).toBeInTheDocument(); + expect(await screen.findByText(/On-Demand is not supported/)).toBeInTheDocument(); const input = screen.getByRole('textbox', {name: 'Monthly Max'}); expect(input).toBeInTheDocument(); @@ -194,7 +194,7 @@ describe('OnDemandSpend', () => { // Check tooltip await userEvent.hover(screen.getByRole('textbox', {name: 'Monthly Max'})); - expect(await screen.findByText(/On-demand is not supported/)).toBeInTheDocument(); + expect(await screen.findByText(/On-Demand is not supported/)).toBeInTheDocument(); const input = screen.getByRole('textbox', {name: 'Monthly Max'}); expect(input).toBeInTheDocument(); diff --git a/static/gsApp/views/amCheckout/steps/onDemandSpend.tsx b/static/gsApp/views/amCheckout/steps/onDemandSpend.tsx index c986f53e286c00..b8365a0e89324e 100644 --- a/static/gsApp/views/amCheckout/steps/onDemandSpend.tsx +++ b/static/gsApp/views/amCheckout/steps/onDemandSpend.tsx @@ -11,6 +11,7 @@ import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; +import {displayBudgetName} from 'getsentry/utils/billing'; import {listDisplayNames} from 'getsentry/utils/dataCategory'; import StepHeader from 'getsentry/views/amCheckout/steps/stepHeader'; import type {StepProps} from 'getsentry/views/amCheckout/types'; @@ -41,7 +42,7 @@ function OnDemandSpend({ const onDemandDollars = isNaN(dollars) ? '' : dollars.toString(); const disabled = !(activePlan.allowOnDemand && subscription.supportsOnDemand); - const title = t('On-Demand Max Spend'); + const title = t('%s Max Spend', displayBudgetName(activePlan, {title: true})); const oxfordCategories = listDisplayNames({ plan: activePlan, categories: activePlan.checkoutCategories, @@ -63,9 +64,12 @@ function OnDemandSpend({
{title}
- {t( - "On-Demand spend allows you to pay for additional data beyond your subscription's reserved event volume. Applies to %s.", - oxfordCategories + {tct( + "[budgetTerm] spend allows you to pay for additional data beyond your subscription's reserved event volume. Applies to [oxfordCategories].", + { + budgetTerm: displayBudgetName(activePlan, {title: true}), + oxfordCategories, + } )}
@@ -73,7 +77,9 @@ function OnDemandSpend({ {t('Monthly Max')} @@ -102,9 +108,13 @@ function OnDemandSpend({ {isActive && (
- {tct('Need more info? [link:See on-demand pricing chart]', { + {tct('Need more info? [link]', { link: ( - + + {tct('See [budgetTerm] pricing chart', { + budgetTerm: displayBudgetName(activePlan), + })} + ), })}
diff --git a/static/gsApp/views/onDemandBudgets/index.tsx b/static/gsApp/views/onDemandBudgets/index.tsx index 7817aa42874937..0823f8750eb974 100644 --- a/static/gsApp/views/onDemandBudgets/index.tsx +++ b/static/gsApp/views/onDemandBudgets/index.tsx @@ -43,11 +43,9 @@ class OnDemandBudgets extends Component { })} { onDemandBudgets.budgetMode === OnDemandBudgetMode.SHARED && onDemandBudgets.sharedMaxBudget > 0 ) { - const budgetType = subscription.planDetails.budgetTerm; - description = - budgetType === 'pay-as-you-go' - ? t( - 'Your pay-as-you-go budget is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.' - ) - : t( - 'Your on-demand budget is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.' - ); + description = t( + 'Your %s is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.', + displayBudgetName(subscription.planDetails, {withBudget: true}) + ); } else if (onDemandBudgets.budgetMode === OnDemandBudgetMode.PER_CATEGORY) { - description = t('You have dedicated on-demand budget for %s.', oxfordCategories); + description = t( + 'You have dedicated %s for %s.', + displayBudgetName(subscription.planDetails, {withBudget: true}), + oxfordCategories + ); } const keepInline = diff --git a/static/gsApp/views/onDemandBudgets/onDemandBudgetEdit.tsx b/static/gsApp/views/onDemandBudgets/onDemandBudgetEdit.tsx index c9dab74f1af8fa..47ef33911b58f1 100644 --- a/static/gsApp/views/onDemandBudgets/onDemandBudgetEdit.tsx +++ b/static/gsApp/views/onDemandBudgets/onDemandBudgetEdit.tsx @@ -209,8 +209,9 @@ class OnDemandBudgetEdit extends Component { {activePlan.onDemandCategories.length !== perCategoryCategories.length && ( {tct( - 'Additional [oxfordCategories] usage [isOrAre] only available through a shared on-demand budget. To enable on-demand usage switch to a shared on-demand budget.', + 'Additional [oxfordCategories] usage [isOrAre] only available through a shared [budgetTerm] budget. To enable [budgetTerm] usage switch to a shared [budgetTerm] budget.', { + budgetTerm: displayBudgetName(activePlan), isOrAre: nonPerCategory.length === 1 ? t('is') : t('are'), oxfordCategories: oxfordizeArray(nonPerCategory), } @@ -292,8 +293,11 @@ class OnDemandBudgetEdit extends Component { )} - {t( - 'The on-demand budget is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.' + {tct( + 'The [budgetTerm] is shared among all categories on a first come, first serve basis. There are no restrictions for any single category consuming the entire budget.', + { + budgetTerm: displayBudgetName(activePlan), + } )} {this.renderInputFields(OnDemandBudgetMode.SHARED)} @@ -328,9 +332,12 @@ class OnDemandBudgetEdit extends Component { )} - {t( - 'Dedicated on-demand budget for %s. Any overages in one category will not consume the budget of another category.', - perCategoryCategories + {tct( + 'Dedicated [budgetTerm] for [perCategoryCategories]. Any overages in one category will not consume the budget of another category.', + { + budgetTerm: displayBudgetName(activePlan, {withBudget: true}), + perCategoryCategories, + } )} {this.renderInputFields(OnDemandBudgetMode.PER_CATEGORY)} diff --git a/static/gsApp/views/onDemandBudgets/onDemandBudgetEditModal.tsx b/static/gsApp/views/onDemandBudgets/onDemandBudgetEditModal.tsx index 11cd88f068faed..6a06caa83ebb3a 100644 --- a/static/gsApp/views/onDemandBudgets/onDemandBudgetEditModal.tsx +++ b/static/gsApp/views/onDemandBudgets/onDemandBudgetEditModal.tsx @@ -13,7 +13,12 @@ import type {Organization} from 'sentry/types/organization'; import withApi from 'sentry/utils/withApi'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; -import type {OnDemandBudgetMode, OnDemandBudgets, Subscription} from 'getsentry/types'; +import type { + OnDemandBudgetMode, + OnDemandBudgets, + Plan, + Subscription, +} from 'getsentry/types'; import {displayBudgetName, hasNewBillingUI} from 'getsentry/utils/billing'; import OnDemandBudgetEdit from './onDemandBudgetEdit'; @@ -26,14 +31,22 @@ import { trackOnDemandBudgetAnalytics, } from './utils'; -const ONDEMAND_BUDGET_SAVE_ERROR = t('Unable to save your on-demand budgets.'); -const PAYG_BUDGET_SAVE_ERROR = t('Unable to save your pay-as-you-go budget.'); -const ONDEMAND_BUDGET_EXCEEDS_INVOICED_LIMIT = t( - 'Your on-demand budget cannot exceed 5 times your monthly plan price.' -); -const PAYG_BUDGET_EXCEEDS_INVOICED_LIMIT = t( - 'Your pay-as-you-go budget cannot exceed 5 times your monthly plan price.' -); +function getBudgetSaveError(plan: Plan) { + return t( + 'Unable to save your %s', + displayBudgetName(plan, { + pluralOndemand: true, + withBudget: true, + }) + ); +} + +function getBudgetExceededInvoicedLimitError(plan: Plan) { + return t( + 'Your %s cannot exceed 5 times your monthly plan price.', + displayBudgetName(plan, {withBudget: true}) + ); +} type Props = { api: Client; @@ -79,7 +92,7 @@ class OnDemandBudgetEditModal extends Component { if (listOfErrors.length === 0) { return ( - {ONDEMAND_BUDGET_SAVE_ERROR} + {getBudgetSaveError(this.props.subscription.planDetails)} ); } @@ -119,10 +132,7 @@ class OnDemandBudgetEditModal extends Component { const newOnDemandBudget = normalizeOnDemandBudget(this.state.onDemandBudget); if (exceedsInvoicedBudgetLimit(subscription, newOnDemandBudget)) { - const message = - subscription.planDetails.budgetTerm === 'pay-as-you-go' - ? PAYG_BUDGET_EXCEEDS_INVOICED_LIMIT - : ONDEMAND_BUDGET_EXCEEDS_INVOICED_LIMIT; + const message = getBudgetExceededInvoicedLimitError(subscription.planDetails); this.setState({ updateError: message, }); @@ -166,18 +176,11 @@ class OnDemandBudgetEditModal extends Component { return true; } catch (response: any) { const updateError = - (response?.responseJSON ?? - subscription.planDetails.budgetTerm === 'pay-as-you-go') - ? PAYG_BUDGET_SAVE_ERROR - : ONDEMAND_BUDGET_SAVE_ERROR; + response?.responseJSON ?? getBudgetSaveError(subscription.planDetails); this.setState({ updateError, }); - addErrorMessage( - subscription.planDetails.budgetTerm === 'pay-as-you-go' - ? PAYG_BUDGET_SAVE_ERROR - : ONDEMAND_BUDGET_SAVE_ERROR - ); + addErrorMessage(getBudgetSaveError(subscription.planDetails)); return false; } }; diff --git a/static/gsApp/views/onDemandBudgets/utils.tsx b/static/gsApp/views/onDemandBudgets/utils.tsx index 07e1d7edef112c..eded8d4705714e 100644 --- a/static/gsApp/views/onDemandBudgets/utils.tsx +++ b/static/gsApp/views/onDemandBudgets/utils.tsx @@ -131,8 +131,8 @@ export function hasOnDemandBudgetsFeature( organization: undefined | Organization, subscription: undefined | Subscription ) { - // This function determines if the org can access the on-demand budgets UI. - // Only orgs on the AM plan can access the on-demand budgets UI. + // This function determines if the org can access the PAYG budgets UI. + // Only orgs on the AM plan can access the PAYG budgets UI. return ( subscription?.planDetails?.hasOnDemandModes && organization?.features.includes('ondemand-budgets') @@ -154,7 +154,7 @@ export function exceedsInvoicedBudgetLimit( return false; } - // no limit for invoiced customers with CC-charged on-demand + // no limit for invoiced customers with CC-charged PAYG if (subscription.onDemandInvoiced && !subscription.onDemandInvoicedManual) { return false; } diff --git a/static/gsApp/views/spendAllocations/rootAllocationCard.tsx b/static/gsApp/views/spendAllocations/rootAllocationCard.tsx index 9f1e3673d13b9d..2ff26b201d13db 100644 --- a/static/gsApp/views/spendAllocations/rootAllocationCard.tsx +++ b/static/gsApp/views/spendAllocations/rootAllocationCard.tsx @@ -82,10 +82,10 @@ function RootAllocationCard({ {tct( - `The un-allocated pool represents the remaining Reserved Volume available for your projects. Excess project consumption will first consume events from your un-allocated pool, and then from your [odLink] volume, if available`, + `The un-allocated pool represents the remaining Reserved Volume available for your projects. Excess project consumption will first consume events from your un-allocated pool, and then from your [pricingLink] volume, if available`, { - odLink: ( - + pricingLink: ( + {displayBudgetName(subscription.planDetails, {title: true})} ), diff --git a/static/gsApp/views/subscriptionPage/onDemandSettings.tsx b/static/gsApp/views/subscriptionPage/onDemandSettings.tsx index 00cb0eadaa2d4c..724ab9c193dad6 100644 --- a/static/gsApp/views/subscriptionPage/onDemandSettings.tsx +++ b/static/gsApp/views/subscriptionPage/onDemandSettings.tsx @@ -114,7 +114,7 @@ export function OnDemandSettings({subscription, organization}: OnDemandSettingsP )} - {/* AM3 doesn't have on-demand-budgets, but we want them to see the newer ui */} + {/* AM3 doesn't have PAYG budget modes, but we want them to see the newer ui */} {hasOndemandBudgets || subscription.planTier === PlanTier.AM3 ? ( { this.setState({initialValue: value}); }; - renderLabel = () => ( - - ); + renderLabel = () => { + const {planDetails} = this.props.subscription; + return ( + + ); + }; renderNotEnabled() { const {organization} = this.props; + const {planDetails} = this.props.subscription; return (
@@ -138,11 +149,17 @@ class OnDemandSummary extends Component { renderNeedsPaymentSource() { const {organization, subscription} = this.props; + const {planDetails} = subscription; return (