diff --git a/static/app/components/forms/fields/inputField.tsx b/static/app/components/forms/fields/inputField.tsx index d09c4ef6bfff8f..ef1492a9f52659 100644 --- a/static/app/components/forms/fields/inputField.tsx +++ b/static/app/components/forms/fields/inputField.tsx @@ -49,7 +49,7 @@ function defaultField({ onBlur(e.target.value, e)} - onKeyDown={e => onKeyDown((e.target as any).value, e)} + onKeyDown={e => onKeyDown((e.target as HTMLInputElement).value, e)} onChange={e => onChange(e.target.value, e)} name={name} {...rest} diff --git a/static/app/components/forms/fields/numberField.tsx b/static/app/components/forms/fields/numberField.tsx index abba5ad7ee22f2..ade8cee1aad3d1 100644 --- a/static/app/components/forms/fields/numberField.tsx +++ b/static/app/components/forms/fields/numberField.tsx @@ -62,7 +62,7 @@ function createFieldWithSuffix({suffix}: {suffix: React.ReactNode}) { onBlur(e.target.value, e)} - onKeyDown={e => onKeyDown((e.target as any).value, e)} + onKeyDown={e => onKeyDown((e.target as HTMLInputElement).value, e)} onChange={e => onChange(e.target.value, e)} name={name} {...rest} diff --git a/static/app/views/onboarding/components/stepper.tsx b/static/app/views/onboarding/components/stepper.tsx index ef6e5f4b15f1a4..15ba34ca7ed954 100644 --- a/static/app/views/onboarding/components/stepper.tsx +++ b/static/app/views/onboarding/components/stepper.tsx @@ -27,7 +27,7 @@ const StepperTransitionIndicator = styled(motion.span)` position: absolute; `; -type Props = React.HTMLAttributes & { +type Props = Omit, 'onClick'> & { currentStepIndex: number; numSteps: number; onClick: (stepIndex: number) => void; diff --git a/static/app/views/onboarding/onboarding.tsx b/static/app/views/onboarding/onboarding.tsx index 2f5781ff2df11d..9a1486fbec7083 100644 --- a/static/app/views/onboarding/onboarding.tsx +++ b/static/app/views/onboarding/onboarding.tsx @@ -215,12 +215,12 @@ export function OnboardingWithoutContext(props: Props) { numSteps={onboardingSteps.length} currentStepIndex={stepIndex} onClick={i => { - if ((i as number) < stepIndex && shallProjectBeDeleted) { - handleGoBack(i as number); + if (i < stepIndex && shallProjectBeDeleted) { + handleGoBack(i); return; } - goToStep(onboardingSteps[i as number]!); + goToStep(onboardingSteps[i]!); }} /> )} diff --git a/static/gsApp/__fixtures__/previewData.tsx b/static/gsApp/__fixtures__/previewData.tsx index 0e0f68113151a3..3b3889c0b0e247 100644 --- a/static/gsApp/__fixtures__/previewData.tsx +++ b/static/gsApp/__fixtures__/previewData.tsx @@ -1,5 +1,4 @@ import type {PreviewData} from 'getsentry/types'; -import {InvoiceItemType} from 'getsentry/types'; export function PreviewDataFixture(fields: Partial): PreviewData { return { @@ -11,7 +10,7 @@ export function PreviewDataFixture(fields: Partial): PreviewData { invoiceItems: [ { amount: 8900, - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', data: {}, period_end: '', diff --git a/static/gsApp/components/promotionModal.spec.tsx b/static/gsApp/components/promotionModal.spec.tsx index 382369df7d4a9e..7a4b4fa2fb3fec 100644 --- a/static/gsApp/components/promotionModal.spec.tsx +++ b/static/gsApp/components/promotionModal.spec.tsx @@ -5,7 +5,6 @@ import {PromotionFixture} from 'getsentry-test/fixtures/promotion'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import PromotionModal from 'getsentry/components/promotionModal'; -import {InvoiceItemType} from 'getsentry/types'; describe('Promotion Modal', () => { const organization = OrganizationFixture(); @@ -24,7 +23,7 @@ describe('Promotion Modal', () => { amount: 2500, billingInterval: 'monthly', billingPeriods: 3, - creditCategory: InvoiceItemType.SUBSCRIPTION, + creditCategory: 'subscription', discountType: 'percentPoints', disclaimerText: "*Receive 40% off the monthly price of Sentry's Team or Business plan subscriptions for your first three months if you upgrade today", diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index 5a22e78ab6e53d..f0d916b86ee2fc 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -1,5 +1,6 @@ import type {StripeConstructor} from '@stripe/stripe-js'; +import type {DATA_CATEGORY_INFO} from 'sentry/constants'; import type {DataCategory, DataCategoryInfo} from 'sentry/types/core'; import type {User} from 'sentry/types/user'; @@ -649,51 +650,141 @@ export type InvoiceItem = BaseInvoiceItem & { periodStart: string; }; -// TODO(data categories): BIL-969 -export enum InvoiceItemType { - UNKNOWN = '', - SUBSCRIPTION = 'subscription', - ONDEMAND = 'ondemand', - RESERVED_EVENTS = 'reserved', - DAILY_EVENTS = 'daily_events', - BALANCE_CHANGE = 'balance_change', - CANCELLATION_FEE = 'cancellation_fee', - SUBSCRIPTION_CREDIT = 'subscription_credit', - CREDIT_APPLIED = 'credit_applied', - RECURRING_DISCOUNT = 'recurring_discount', - DISCOUNT = 'discount', - SALES_TAX = 'sales_tax', - /** - * Used for AM plans - */ - ATTACHMENTS = 'attachments', - TRANSACTIONS = 'transactions', - ONDEMAND_ATTACHMENTS = 'ondemand_attachments', - ONDEMAND_ERRORS = 'ondemand_errors', - ONDEMAND_TRANSACTIONS = 'ondemand_transactions', - ONDEMAND_REPLAYS = 'ondemand_replays', - ONDEMAND_SPANS = 'ondemand_spans', - ONDEMAND_SPANS_INDEXED = 'ondemand_spans_indexed', - ONDEMAND_MONITOR_SEATS = 'ondemand_monitor_seats', - ONDEMAND_UPTIME = 'ondemand_uptime', - ONDEMAND_PROFILE_DURATION = 'ondemand_profile_duration', - ONDEMAND_SEER_AUTOFIX = 'ondemand_seer_autofix', - ONDEMAND_SEER_SCANNER = 'ondemand_seer_scanner', - RESERVED_ATTACHMENTS = 'reserved_attachments', - RESERVED_ERRORS = 'reserved_errors', - RESERVED_TRANSACTIONS = 'reserved_transactions', - RESERVED_REPLAYS = 'reserved_replays', - RESERVED_SPANS = 'reserved_spans', - RESERVED_SPANS_INDEXED = 'reserved_spans_indexed', - RESERVED_MONITOR_SEATS = 'reserved_monitor_seats', - RESERVED_UPTIME = 'reserved_uptime', - RESERVED_PROFILE_DURATION = 'reserved_profile_duration', - RESERVED_SEER_AUTOFIX = 'reserved_seer_autofix', - RESERVED_SEER_SCANNER = 'reserved_seer_scanner', - RESERVED_SEER_BUDGET = 'reserved_seer_budget', - RESERVED_PREVENT_USERS = 'reserved_prevent_users', - RESERVED_LOG_BYTES = 'reserved_log_bytes', -} +/** + * Converts camelCase string to snake_case. Consecutive capitals are treated as + * a single acronym (e.g. "profileDurationUI" -> "profile_duration_ui"). + * Examples: "monitorSeats" -> "monitor_seats", "errors" -> "errors" + */ +type CamelToSnake< + S extends string, + Prev extends 'lower' | 'upper' | '' = '', +> = S extends `${infer First}${infer Rest}` + ? First extends Lowercase + ? `${First}${CamelToSnake}` + : First extends Uppercase + ? Rest extends '' + ? `${Prev extends '' ? '' : Prev extends 'lower' ? '_' : ''}${Lowercase}` + : Rest extends `${infer Next}${infer _Tail}` + ? Next extends Lowercase + ? `${Prev extends '' ? '' : '_'}${Lowercase}${CamelToSnake}` + : `${Prev extends 'lower' ? '_' : ''}${Lowercase}${CamelToSnake}` + : never + : `${First}${CamelToSnake}` + : S; + +/** + * Dynamically generate ondemand invoice item types from DATA_CATEGORY_INFO. + * This automatically includes new billing categories without manual enum updates. + * + * Follows the pattern: `ondemand_${snake_case_plural}` + * Example: DATA_CATEGORY_INFO.ERROR (plural: "errors") -> "ondemand_errors" + * Example: DATA_CATEGORY_INFO.MONITOR_SEAT (plural: "monitorSeats") -> "ondemand_monitor_seats" + */ +type OnDemandInvoiceItemType = { + [K in keyof typeof DATA_CATEGORY_INFO]: (typeof DATA_CATEGORY_INFO)[K]['isBilledCategory'] extends true + ? `ondemand_${CamelToSnake<(typeof DATA_CATEGORY_INFO)[K]['plural']>}` + : never; +}[keyof typeof DATA_CATEGORY_INFO]; + +/** + * Dynamically generate reserved invoice item types from DATA_CATEGORY_INFO. + * This automatically includes new billing categories without manual enum updates. + * + * Follows the pattern: `reserved_${snake_case_plural}` + * Example: DATA_CATEGORY_INFO.ERROR (plural: "errors") -> "reserved_errors" + * Example: DATA_CATEGORY_INFO.MONITOR_SEAT (plural: "monitorSeats") -> "reserved_monitor_seats" + */ +type ReservedInvoiceItemType = { + [K in keyof typeof DATA_CATEGORY_INFO]: (typeof DATA_CATEGORY_INFO)[K]['isBilledCategory'] extends true + ? `reserved_${CamelToSnake<(typeof DATA_CATEGORY_INFO)[K]['plural']>}` + : never; +}[keyof typeof DATA_CATEGORY_INFO]; + +/** + * Credit-related invoice item types (discounts, credits, refunds). + * Exported as const array to enable runtime usage in filters. + */ +export const CREDIT_INVOICE_ITEM_TYPES = [ + 'subscription_credit', + 'recurring_discount', + 'discount', + 'credit_applied', // Deprecated: replaced by balance_change +] as const; + +type CreditInvoiceItemType = (typeof CREDIT_INVOICE_ITEM_TYPES)[number]; + +/** + * Fee-related invoice item types (taxes, penalties). + * Exported as const array to enable runtime usage in filters. + */ +export const FEE_INVOICE_ITEM_TYPES = ['sales_tax', 'cancellation_fee'] as const; + +type FeeInvoiceItemType = (typeof FEE_INVOICE_ITEM_TYPES)[number]; + +/** + * Seer/AI-related invoice item types (special billing for AI features). + */ +const _SEER_INVOICE_ITEM_TYPES = [ + 'reserved_seer_budget', // Special case: shared budget for seer_autofix and seer_scanner + 'reserved_seer_users', // Special case: reserved prevent users (PREVENT_USER category maps to this) + 'activated_seer_users', // Activation-based prevent users billing (PREVENT_USER category) +] as const; + +type SeerInvoiceItemType = (typeof _SEER_INVOICE_ITEM_TYPES)[number]; + +/** + * Legacy/deprecated invoice item types (AM1 plans and old formats). + */ +const _LEGACY_INVOICE_ITEM_TYPES = [ + 'ondemand', // Legacy: generic ondemand for AM1 plans + 'attachments', // Legacy: AM1 plans + 'transactions', // Legacy: AM1 plans + 'daily_events', // Deprecated + 'reserved', // Deprecated: legacy name for reserved_events +] as const; + +type LegacyInvoiceItemType = (typeof _LEGACY_INVOICE_ITEM_TYPES)[number]; + +/** + * Core subscription type. + */ +type SubscriptionInvoiceItemType = 'subscription'; + +/** + * Balance change can be both credit (negative) or fee (positive). + */ +type BalanceChangeInvoiceItemType = 'balance_change'; + +/** + * Unknown invoice item type (empty string). + */ +type UnknownInvoiceItemType = ''; + +/** + * Static invoice item types that are not tied to data categories. + * These must be manually maintained but change infrequently. + */ +type StaticInvoiceItemType = + | UnknownInvoiceItemType + | SubscriptionInvoiceItemType + | BalanceChangeInvoiceItemType + | CreditInvoiceItemType + | FeeInvoiceItemType + | SeerInvoiceItemType + | LegacyInvoiceItemType; + +/** + * Complete invoice item type union. + * Automatically stays in sync with backend when new billing categories are added. + * + * Migration from enum: Use string literals instead of enum members. + * Before: InvoiceItemType.SUBSCRIPTION + * After: 'subscription' + */ +export type InvoiceItemType = + | OnDemandInvoiceItemType + | ReservedInvoiceItemType + | StaticInvoiceItemType; export enum InvoiceStatus { PAID = 'paid', diff --git a/static/gsApp/utils/billing.spec.tsx b/static/gsApp/utils/billing.spec.tsx index 05c906a7044387..0fcf27e501d82c 100644 --- a/static/gsApp/utils/billing.spec.tsx +++ b/static/gsApp/utils/billing.spec.tsx @@ -8,7 +8,8 @@ import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; import {DataCategory} from 'sentry/types/core'; import {BILLION, GIGABYTE, MILLION, UNLIMITED} from 'getsentry/constants'; -import {InvoiceItemType, OnDemandBudgetMode, type ProductTrial} from 'getsentry/types'; +import {OnDemandBudgetMode} from 'getsentry/types'; +import type {ProductTrial} from 'getsentry/types'; import { convertUsageToReservedUnit, formatReservedWithUnits, @@ -1097,7 +1098,7 @@ describe('getCreditApplied', () => { creditApplied: 100, invoiceItems: [ { - type: InvoiceItemType.SUBSCRIPTION_CREDIT, + type: 'subscription_credit', ...commonCreditProps, }, ], @@ -1108,7 +1109,7 @@ describe('getCreditApplied', () => { creditApplied: 100, invoiceItems: [ { - type: InvoiceItemType.BALANCE_CHANGE, + type: 'balance_change', ...commonCreditProps, }, ], @@ -1119,7 +1120,7 @@ describe('getCreditApplied', () => { creditApplied: 100, invoiceItems: [ { - type: InvoiceItemType.BALANCE_CHANGE, + type: 'balance_change', ...commonCreditProps, amount: -50, }, diff --git a/static/gsApp/utils/billing.tsx b/static/gsApp/utils/billing.tsx index 3a6bdc58c4d97e..2df749265d4c28 100644 --- a/static/gsApp/utils/billing.tsx +++ b/static/gsApp/utils/billing.tsx @@ -20,21 +20,24 @@ import { } from 'getsentry/constants'; import { AddOnCategory, - InvoiceItemType, + CREDIT_INVOICE_ITEM_TYPES, + FEE_INVOICE_ITEM_TYPES, OnDemandBudgetMode, PlanName, PlanTier, ReservedBudgetCategoryType, - type BillingConfig, - type BillingDetails, - type BillingMetricHistory, - type BillingStatTotal, - type EventBucket, - type InvoiceItem, - type Plan, - type PreviewInvoiceItem, - type ProductTrial, - type Subscription, +} from 'getsentry/types'; +import type { + BillingConfig, + BillingDetails, + BillingMetricHistory, + BillingStatTotal, + EventBucket, + InvoiceItem, + Plan, + PreviewInvoiceItem, + ProductTrial, + Subscription, } from 'getsentry/types'; import { getCategoryInfoFromPlural, @@ -803,13 +806,8 @@ export function getCredits({ }) { return invoiceItems.filter( item => - [ - InvoiceItemType.SUBSCRIPTION_CREDIT, - InvoiceItemType.CREDIT_APPLIED, // TODO(isabella): This is deprecated and replaced by BALANCE_CHANGE - InvoiceItemType.DISCOUNT, - InvoiceItemType.RECURRING_DISCOUNT, - ].includes(item.type) || - (item.type === InvoiceItemType.BALANCE_CHANGE && item.amount < 0) + CREDIT_INVOICE_ITEM_TYPES.includes(item.type as any) || + (item.type === 'balance_change' && item.amount < 0) ); } @@ -826,7 +824,7 @@ export function getCreditApplied({ invoiceItems: InvoiceItem[] | PreviewInvoiceItem[]; }) { const credits = getCredits({invoiceItems}); - if (credits.some(item => item.type === InvoiceItemType.BALANCE_CHANGE)) { + if (credits.some(item => item.type === 'balance_change')) { return 0; } return creditApplied; @@ -843,8 +841,8 @@ export function getFees({ }) { return invoiceItems.filter( item => - [InvoiceItemType.CANCELLATION_FEE, InvoiceItemType.SALES_TAX].includes(item.type) || - (item.type === InvoiceItemType.BALANCE_CHANGE && item.amount > 0) + FEE_INVOICE_ITEM_TYPES.includes(item.type as any) || + (item.type === 'balance_change' && item.amount > 0) ); } diff --git a/static/gsApp/utils/promotionUtils.tsx b/static/gsApp/utils/promotionUtils.tsx index 88412534ecc0ab..cb1374d9366c82 100644 --- a/static/gsApp/utils/promotionUtils.tsx +++ b/static/gsApp/utils/promotionUtils.tsx @@ -10,14 +10,13 @@ import { openPromotionModal, openPromotionReminderModal, } from 'getsentry/actionCreators/modal'; -import { - InvoiceItemType, - type DiscountInfo, - type Plan, - type Promotion, - type PromotionClaimed, - type PromotionData, - type Subscription, +import type { + DiscountInfo, + Plan, + Promotion, + PromotionClaimed, + PromotionData, + Subscription, } from 'getsentry/types'; import {isBizPlanFamily} from 'getsentry/utils/billing'; import {createPromotionCheckQueryKey} from 'getsentry/utils/usePromotionTriggerCheck'; @@ -98,7 +97,7 @@ export function showSubscriptionDiscount({ discountInfo?.durationText && discountInfo.discountType === 'percentPoints' && activePlan.billingInterval === discountInfo.billingInterval && - discountInfo.creditCategory === InvoiceItemType.SUBSCRIPTION + discountInfo.creditCategory === 'subscription' ); } diff --git a/static/gsApp/views/amCheckout/components/cart.spec.tsx b/static/gsApp/views/amCheckout/components/cart.spec.tsx index 0dde16b67e4f7b..bf1da81fe12cc4 100644 --- a/static/gsApp/views/amCheckout/components/cart.spec.tsx +++ b/static/gsApp/views/amCheckout/components/cart.spec.tsx @@ -18,12 +18,7 @@ import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {PAYG_BUSINESS_DEFAULT} from 'getsentry/constants'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; -import { - AddOnCategory, - InvoiceItemType, - OnDemandBudgetMode, - PlanTier, -} from 'getsentry/types'; +import {AddOnCategory, OnDemandBudgetMode, PlanTier} from 'getsentry/types'; import AMCheckout from 'getsentry/views/amCheckout/'; import Cart from 'getsentry/views/amCheckout/components/cart'; import {type CheckoutFormData} from 'getsentry/views/amCheckout/types'; @@ -280,13 +275,13 @@ describe('Cart', () => { { amount: 2_00, description: 'Tax', - type: InvoiceItemType.SALES_TAX, + type: 'sales_tax', }, { amount: 89_00, description: 'Business Plan', period_end: moment(MOCK_TODAY).add(1, 'day').format('YYYY-MM-DD'), - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', }, ], }, @@ -329,7 +324,7 @@ describe('Cart', () => { amount: 89_00, description: 'Business Plan', period_end: moment(MOCK_TODAY).add(2, 'year').format('YYYY-MM-DD'), - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', }, ], }, @@ -613,13 +608,13 @@ describe('Cart', () => { amount: 25_00, description: '500 pay-as-you-go replays', data: {quantity: 500}, - type: InvoiceItemType.ONDEMAND_REPLAYS, + type: 'ondemand_replays', }, { amount: 25_00, description: '50 GB pay-as-you-go attachments', data: {quantity: 53687091200}, - type: InvoiceItemType.ONDEMAND_ATTACHMENTS, + type: 'ondemand_attachments', }, ], }, diff --git a/static/gsApp/views/amCheckout/components/cart.tsx b/static/gsApp/views/amCheckout/components/cart.tsx index a9468fe1e29336..e89624a0dfae68 100644 --- a/static/gsApp/views/amCheckout/components/cart.tsx +++ b/static/gsApp/views/amCheckout/components/cart.tsx @@ -22,13 +22,8 @@ import useMedia from 'sentry/utils/useMedia'; import {PAYG_BUSINESS_DEFAULT, PAYG_TEAM_DEFAULT} from 'getsentry/constants'; import {useBillingDetails} from 'getsentry/hooks/useBillingDetails'; import {useStripeInstance} from 'getsentry/hooks/useStripeInstance'; -import { - InvoiceItemType, - OnDemandBudgetMode, - type Plan, - type PreviewData, - type Subscription, -} from 'getsentry/types'; +import {OnDemandBudgetMode} from 'getsentry/types'; +import type {Plan, PreviewData, Subscription} from 'getsentry/types'; import { displayBudgetName, formatReservedWithUnits, @@ -772,9 +767,7 @@ function Cart({ // for immediate changes, effectiveAt is the current day const {effectiveAt, atPeriodEnd, invoiceItems, billedAmount, proratedAmount} = data; - const planItem = invoiceItems.find( - item => item.type === InvoiceItemType.SUBSCRIPTION - ); + const planItem = invoiceItems.find(item => item.type === 'subscription'); const renewalDate = moment( planItem?.period_end ?? subscription.contractPeriodEnd ) diff --git a/static/gsApp/views/amCheckout/components/checkoutSuccess.spec.tsx b/static/gsApp/views/amCheckout/components/checkoutSuccess.spec.tsx index 3a9f276334191b..ca64b207b48fd9 100644 --- a/static/gsApp/views/amCheckout/components/checkoutSuccess.spec.tsx +++ b/static/gsApp/views/amCheckout/components/checkoutSuccess.spec.tsx @@ -6,7 +6,6 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {PreviewDataFixture} from 'getsentry/__fixtures__/previewData'; -import {InvoiceItemType} from 'getsentry/types'; import CheckoutSuccess from 'getsentry/views/amCheckout/components/checkoutSuccess'; describe('CheckoutSuccess', () => { @@ -73,7 +72,7 @@ describe('CheckoutSuccess', () => { const invoiceWithOnDemand = InvoiceFixture({ items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Team Plan', amount: 31200, data: {quantity: null}, @@ -81,7 +80,7 @@ describe('CheckoutSuccess', () => { periodEnd: '2026-01-01T00:00:00Z', }, { - type: InvoiceItemType.ONDEMAND_ERRORS, + type: 'ondemand_errors', description: '4,901,066 pay-as-you-go errors', amount: 94022, data: {quantity: 4901066}, @@ -89,7 +88,7 @@ describe('CheckoutSuccess', () => { periodEnd: '2026-01-01T00:00:00Z', }, { - type: InvoiceItemType.ONDEMAND_MONITOR_SEATS, + type: 'ondemand_monitor_seats', description: '2 pay-as-you-go cron monitors', amount: 156, data: {quantity: 2}, diff --git a/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx b/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx index a3ae4d71211d73..d3038618ce1744 100644 --- a/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx +++ b/static/gsApp/views/amCheckout/components/checkoutSuccess.tsx @@ -17,14 +17,13 @@ import {defined} from 'sentry/utils'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; import {GIGABYTE} from 'getsentry/constants'; -import { - InvoiceItemType, - type Charge, - type Invoice, - type InvoiceItem, - type Plan, - type PreviewData, - type PreviewInvoiceItem, +import type { + Charge, + Invoice, + InvoiceItem, + Plan, + PreviewData, + PreviewInvoiceItem, } from 'getsentry/types'; import { displayBudgetName, @@ -217,7 +216,7 @@ function ScheduledChanges({ })} {fees.map(item => { const adjustedAmount = - item.type === InvoiceItemType.BALANCE_CHANGE ? item.amount * -1 : item.amount; + item.type === 'balance_change' ? item.amount * -1 : item.amount; return ( { const adjustedAmount = - item.type === InvoiceItemType.BALANCE_CHANGE ? item.amount * -1 : item.amount; + item.type === 'balance_change' ? item.amount * -1 : item.amount; return ( item.type === InvoiceItemType.SUBSCRIPTION); + const planItem = invoiceItems.find(item => item.type === 'subscription'); const reservedVolume = invoiceItems.filter( item => item.type.startsWith('reserved_') && !item.type.endsWith('_budget') ); // TODO(prevent): This needs to be updated once we determine how to display Prevent enablement and PAYG changes on this page - const products = invoiceItems.filter( - item => item.type === InvoiceItemType.RESERVED_SEER_BUDGET - ); + const products = invoiceItems.filter(item => item.type === 'reserved_seer_budget'); const onDemandItems = getOnDemandItems({invoiceItems}); const fees = getFees({invoiceItems}); const credits = getCredits({invoiceItems}); - // TODO(isabella): PreviewData never has the InvoiceItemType.BALANCE_CHANGE type - // and instead populates creditApplied with the value of the InvoiceItemType.CREDIT_APPLIED type + // TODO(isabella): PreviewData never has the 'balance_change' type + // and instead populates creditApplied with the value of the 'credit_applied' type // this is a temporary fix to ensure we only display CreditApplied if it's not already in the credits array const creditApplied = getCreditApplied({ creditApplied: data?.creditApplied ?? 0, diff --git a/static/gsApp/views/amCheckout/index.tsx b/static/gsApp/views/amCheckout/index.tsx index 4600bc73b59263..dc08fc8d77f592 100644 --- a/static/gsApp/views/amCheckout/index.tsx +++ b/static/gsApp/views/amCheckout/index.tsx @@ -40,21 +40,17 @@ import { PAYG_BUSINESS_DEFAULT, PAYG_TEAM_DEFAULT, } from 'getsentry/constants'; -import { - CheckoutType, - InvoiceItemType, - OnDemandBudgetMode, - PlanName, - PlanTier, - type BillingConfig, - type CheckoutAddOns, - type EventBucket, - type Invoice, - type OnDemandBudgets, - type Plan, - type PreviewData, - type PromotionData, - type Subscription, +import {CheckoutType, OnDemandBudgetMode, PlanName, PlanTier} from 'getsentry/types'; +import type { + BillingConfig, + CheckoutAddOns, + EventBucket, + Invoice, + OnDemandBudgets, + Plan, + PreviewData, + PromotionData, + Subscription, } from 'getsentry/types'; import { hasActiveVCFeature, @@ -811,9 +807,7 @@ class AMCheckout extends Component { } if (isSubmitted && isNewCheckout) { - const purchasedPlanItem = invoice?.items.find( - item => item.type === InvoiceItemType.SUBSCRIPTION - ); + const purchasedPlanItem = invoice?.items.find(item => item.type === 'subscription'); const basePlan = purchasedPlanItem ? this.getPlan(purchasedPlanItem.data.plan) : this.getPlan(formData.plan); diff --git a/static/gsApp/views/amCheckout/steps/contractSelect.tsx b/static/gsApp/views/amCheckout/steps/contractSelect.tsx index 42a42745c8eb07..a52cc4c568390d 100644 --- a/static/gsApp/views/amCheckout/steps/contractSelect.tsx +++ b/static/gsApp/views/amCheckout/steps/contractSelect.tsx @@ -10,7 +10,7 @@ import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {ANNUAL, MONTHLY} from 'getsentry/constants'; -import {InvoiceItemType, type Plan} from 'getsentry/types'; +import type {InvoiceItemType, Plan} from 'getsentry/types'; import PlanSelectRow from 'getsentry/views/amCheckout/components/planSelectRow'; import StepHeader from 'getsentry/views/amCheckout/components/stepHeader'; import type {StepProps} from 'getsentry/views/amCheckout/types'; @@ -99,7 +99,7 @@ class ContractSelect extends Component { } = { // default to subscription discount // since we need a credit category to calculate the price after discount - creditCategory: InvoiceItemType.SUBSCRIPTION, + creditCategory: 'subscription', }; if ( promotion?.showDiscountInfo && diff --git a/static/gsApp/views/amCheckout/steps/reviewAndConfirm.tsx b/static/gsApp/views/amCheckout/steps/reviewAndConfirm.tsx index ff24e219f723e4..4cf8f8f36cb3b4 100644 --- a/static/gsApp/views/amCheckout/steps/reviewAndConfirm.tsx +++ b/static/gsApp/views/amCheckout/steps/reviewAndConfirm.tsx @@ -17,7 +17,6 @@ import TextBlock from 'sentry/views/settings/components/text/textBlock'; import {useStripeInstance} from 'getsentry/hooks/useStripeInstance'; import type {PreviewData, Subscription} from 'getsentry/types'; -import {InvoiceItemType} from 'getsentry/types'; import {hasPartnerMigrationFeature} from 'getsentry/utils/billing'; import StepHeader from 'getsentry/views/amCheckout/components/stepHeader'; import type {StepPropsWithApi} from 'getsentry/views/amCheckout/types'; @@ -252,7 +251,7 @@ function ReviewAndConfirmItems({previewData}: Pick) { idx ) => { const price = displayPrice({cents: amount}); - const showDates = type === InvoiceItemType.SUBSCRIPTION; + const showDates = type === 'subscription'; return ( diff --git a/static/gsApp/views/amCheckout/utils.spec.tsx b/static/gsApp/views/amCheckout/utils.spec.tsx index fdadfd98f71282..99ce834b7f2634 100644 --- a/static/gsApp/views/amCheckout/utils.spec.tsx +++ b/static/gsApp/views/amCheckout/utils.spec.tsx @@ -1,6 +1,6 @@ import {PlanDetailsLookupFixture} from 'getsentry-test/fixtures/planDetailsLookup'; -import {AddOnCategory, InvoiceItemType, PlanTier} from 'getsentry/types'; +import {AddOnCategory, PlanTier} from 'getsentry/types'; import * as utils from 'getsentry/views/amCheckout/utils'; import {getCheckoutAPIData} from 'getsentry/views/amCheckout/utils'; @@ -140,7 +140,7 @@ describe('utils', () => { basePrice: 1000, amount: 10 * 100, discountType: 'percentPoints', - creditCategory: InvoiceItemType.SUBSCRIPTION, + creditCategory: 'subscription', }) ).toBe(900); expect( @@ -148,7 +148,7 @@ describe('utils', () => { basePrice: 8900, amount: 40 * 100, discountType: 'percentPoints', - creditCategory: InvoiceItemType.SUBSCRIPTION, + creditCategory: 'subscription', }) ).toBe(5340); expect( @@ -156,7 +156,7 @@ describe('utils', () => { basePrice: 10000, amount: 1000, discountType: 'amountCents', - creditCategory: InvoiceItemType.SUBSCRIPTION, + creditCategory: 'subscription', }) ).toBe(9000); }); diff --git a/static/gsApp/views/amCheckout/utils.tsx b/static/gsApp/views/amCheckout/utils.tsx index 9a8a9891a23945..25d3196b8a9e0b 100644 --- a/static/gsApp/views/amCheckout/utils.tsx +++ b/static/gsApp/views/amCheckout/utils.tsx @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/react'; -import {type PaymentIntentResult, type Stripe} from '@stripe/stripe-js'; +import type {PaymentIntentResult, Stripe} from '@stripe/stripe-js'; import camelCase from 'lodash/camelCase'; import moment from 'moment-timezone'; @@ -11,7 +11,7 @@ import { import {fetchOrganizationDetails} from 'sentry/actionCreators/organization'; import {Client} from 'sentry/api'; import {t} from 'sentry/locale'; -import {DataCategory} from 'sentry/types/core'; +import type {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {browserHistory} from 'sentry/utils/browserHistory'; import {useMutation} from 'sentry/utils/queryClient'; @@ -27,18 +27,16 @@ import { SUPPORTED_TIERS, } from 'getsentry/constants'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; -import { - AddOnCategory, +import {AddOnCategory, PlanTier, ReservedBudgetCategoryType} from 'getsentry/types'; +import type { + BillingDetails, + CheckoutAddOns, + EventBucket, InvoiceItemType, - PlanTier, - type BillingDetails, - type CheckoutAddOns, - type EventBucket, - type OnDemandBudgets, - type Plan, - type PreviewData, - type ReservedBudgetCategoryType, - type Subscription, + OnDemandBudgets, + Plan, + PreviewData, + Subscription, } from 'getsentry/types'; import { getAmPlanTier, @@ -53,10 +51,10 @@ import { import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics'; import trackMarketingEvent from 'getsentry/utils/trackMarketingEvent'; import type {State as CheckoutState} from 'getsentry/views/amCheckout/'; -import { - type CheckoutAPIData, - type CheckoutFormData, - type PlanContent, +import type { + CheckoutAPIData, + CheckoutFormData, + PlanContent, } from 'getsentry/views/amCheckout/types'; import { normalizeOnDemandBudget, @@ -325,10 +323,7 @@ export function getDiscountedPrice({ creditCategory, }: DiscountedPriceProps): number { let price = basePrice; - if ( - discountType === 'percentPoints' && - creditCategory === InvoiceItemType.SUBSCRIPTION - ) { + if (discountType === 'percentPoints' && creditCategory === 'subscription') { const discount = (basePrice * amount) / 10000; price = basePrice - discount; } else if (discountType === 'amountCents') { @@ -877,9 +872,9 @@ export function invoiceItemTypeToDataCategory( export function invoiceItemTypeToAddOn(type: InvoiceItemType): AddOnCategory | null { switch (type) { - case InvoiceItemType.RESERVED_SEER_BUDGET: + case 'reserved_seer_budget': return AddOnCategory.SEER; - case InvoiceItemType.RESERVED_PREVENT_USERS: + case 'reserved_prevent_users': return AddOnCategory.PREVENT; default: return null; diff --git a/static/gsApp/views/invoiceDetails/index.spec.tsx b/static/gsApp/views/invoiceDetails/index.spec.tsx index f3b762f855277f..a34b17adaa9999 100644 --- a/static/gsApp/views/invoiceDetails/index.spec.tsx +++ b/static/gsApp/views/invoiceDetails/index.spec.tsx @@ -12,7 +12,6 @@ import { import {PlanFixture} from 'getsentry/__fixtures__/plan'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; -import {InvoiceItemType} from 'getsentry/types'; import InvoiceDetails from 'getsentry/views/invoiceDetails'; describe('InvoiceDetails', () => { @@ -22,7 +21,7 @@ describe('InvoiceDetails', () => { dateCreated: '2021-09-20T22:33:38.042Z', items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', amount: 8900, periodEnd: '2021-10-21', @@ -40,7 +39,7 @@ describe('InvoiceDetails', () => { creditApplied: 500, items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', amount: 8900, periodEnd: '2021-10-21', @@ -48,7 +47,7 @@ describe('InvoiceDetails', () => { data: {}, }, { - type: InvoiceItemType.CREDIT_APPLIED, + type: 'credit_applied', description: 'Credit applied', amount: 500, periodEnd: '2021-10-21', @@ -163,7 +162,7 @@ describe('InvoiceDetails', () => { isPaid: false, items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', amount: 8900, periodEnd: '2021-10-21', @@ -235,7 +234,7 @@ describe('InvoiceDetails', () => { isPaid: false, items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', amount: 8900, periodEnd: '2021-10-21', diff --git a/static/gsApp/views/invoiceDetails/index.tsx b/static/gsApp/views/invoiceDetails/index.tsx index 86fc99ba1f9ac4..a5e54d93e1f923 100644 --- a/static/gsApp/views/invoiceDetails/index.tsx +++ b/static/gsApp/views/invoiceDetails/index.tsx @@ -15,8 +15,8 @@ import {keepPreviousData, useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; +import {InvoiceStatus} from 'getsentry/types'; import type {BillingDetails, Invoice} from 'getsentry/types'; -import {InvoiceItemType, InvoiceStatus} from 'getsentry/types'; import {getTaxFieldInfo} from 'getsentry/utils/salesTax'; import {displayPriceWithCents} from 'getsentry/views/amCheckout/utils'; import SubscriptionPageContainer from 'getsentry/views/subscriptionPage/components/subscriptionPageContainer'; @@ -230,7 +230,7 @@ function InvoiceDetailsContents({billingDetails, invoice}: ContentsProps) { {invoice.items.map((item, i) => { - if (item.type === InvoiceItemType.SUBSCRIPTION) { + if (item.type === 'subscription') { return ( diff --git a/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx b/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx index 2a87e78aa445ae..ee759a2e3bbc4a 100644 --- a/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx +++ b/static/gsApp/views/invoiceDetails/paymentForm.spec.tsx @@ -7,7 +7,6 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar import {ModalBody} from 'sentry/components/globalModal/components'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; -import {InvoiceItemType} from 'getsentry/types'; import InvoiceDetailsPaymentForm from 'getsentry/views/invoiceDetails/paymentForm'; // Stripe mocks handled by global setup.ts @@ -18,7 +17,7 @@ describe('InvoiceDetails > Payment Form', () => { { items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', amount: 8900, periodEnd: '2021-10-21', diff --git a/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.spec.tsx b/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.spec.tsx index 044048d090e513..12845e29eb1699 100644 --- a/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.spec.tsx +++ b/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.spec.tsx @@ -5,7 +5,6 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {PreviewDataFixture} from 'getsentry/__fixtures__/previewData'; -import {InvoiceItemType} from 'getsentry/types'; import NextBillCard from 'getsentry/views/subscriptionPage/headerCards/nextBillCard'; describe('NextBillCard', () => { @@ -49,7 +48,7 @@ describe('NextBillCard', () => { invoiceItems: [ { amount: 89_00, - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', data: { plan: 'am3_business', @@ -59,7 +58,7 @@ describe('NextBillCard', () => { }, { amount: 7_50, - type: InvoiceItemType.RESERVED_REPLAYS, + type: 'reserved_replays', data: {}, period_start: '', period_end: '', @@ -67,7 +66,7 @@ describe('NextBillCard', () => { }, { amount: 5_00, - type: InvoiceItemType.RESERVED_ATTACHMENTS, + type: 'reserved_attachments', data: {}, period_start: '', period_end: '', @@ -75,7 +74,7 @@ describe('NextBillCard', () => { }, { amount: 1_00, - type: InvoiceItemType.ONDEMAND_ERRORS, + type: 'ondemand_errors', data: {}, period_start: '', period_end: '', @@ -83,7 +82,7 @@ describe('NextBillCard', () => { }, { amount: 11_00, - type: InvoiceItemType.ONDEMAND_REPLAYS, + type: 'ondemand_replays', data: {}, period_start: '', period_end: '', @@ -91,7 +90,7 @@ describe('NextBillCard', () => { }, { amount: 20_00, - type: InvoiceItemType.SALES_TAX, + type: 'sales_tax', data: {}, period_start: '', period_end: '', diff --git a/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.tsx b/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.tsx index e10bbc560ccc55..36f5cf6a4c113b 100644 --- a/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.tsx +++ b/static/gsApp/views/subscriptionPage/headerCards/nextBillCard.tsx @@ -10,7 +10,7 @@ import type {Organization} from 'sentry/types/organization'; import getDaysSinceDate from 'sentry/utils/getDaysSinceDate'; import {useApiQuery} from 'sentry/utils/queryClient'; -import {InvoiceItemType, type PreviewData, type Subscription} from 'getsentry/types'; +import type {PreviewData, Subscription} from 'getsentry/types'; import { displayBudgetName, getCreditApplied, @@ -42,7 +42,7 @@ function NextBillCard({ // recurring fees, PAYG, and credits are grouped together // only additional fees (ie. taxes) are listed individually const invoiceItems = nextBill?.invoiceItems ?? []; - const planItem = invoiceItems.find(item => item.type === InvoiceItemType.SUBSCRIPTION); + const planItem = invoiceItems.find(item => item.type === 'subscription'); const plan = planItem?.data.plan; const isAnnualPlan = plan?.endsWith('_auf'); const reservedTotal = diff --git a/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx b/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx index da606a56ff8213..5ea411f6a24a53 100644 --- a/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx +++ b/static/gsApp/views/subscriptionPage/paymentHistory.spec.tsx @@ -7,7 +7,7 @@ import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription'; import {render, screen} from 'sentry-test/reactTestingLibrary'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; -import {InvoiceItemType, PlanTier} from 'getsentry/types'; +import {PlanTier} from 'getsentry/types'; import PaymentHistory from 'getsentry/views/subscriptionPage/paymentHistory'; describe('Subscription > PaymentHistory', () => { @@ -57,7 +57,7 @@ describe('Subscription > PaymentHistory', () => { dateCreated: '2021-09-20T22:33:38.042Z', items: [ { - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', description: 'Subscription to Business', amount: 8900, periodEnd: '2021-10-21', diff --git a/tests/js/getsentry-test/fixtures/invoicePreview.ts b/tests/js/getsentry-test/fixtures/invoicePreview.ts index d28366bc52245a..47805c2b9f4d48 100644 --- a/tests/js/getsentry-test/fixtures/invoicePreview.ts +++ b/tests/js/getsentry-test/fixtures/invoicePreview.ts @@ -1,5 +1,4 @@ import type {PreviewData} from 'getsentry/types'; -import {InvoiceItemType} from 'getsentry/types'; export function InvoicePreviewFixture(params: Partial = {}): PreviewData { return { @@ -18,7 +17,7 @@ export function InvoicePreviewFixture(params: Partial = {}): Previe description: 'Subscription to Business', period_end: '2020-07-07', period_start: '2020-06-08', - type: InvoiceItemType.SUBSCRIPTION, + type: 'subscription', }, { amount: 0, @@ -26,7 +25,7 @@ export function InvoicePreviewFixture(params: Partial = {}): Previe description: '50,000 prepaid errors', period_end: '2020-07-07', period_start: '2020-06-08', - type: InvoiceItemType.RESERVED_ERRORS, + type: 'reserved_errors', }, { amount: 0, @@ -34,7 +33,7 @@ export function InvoicePreviewFixture(params: Partial = {}): Previe description: '150,000 prepaid transactions', period_end: '2020-07-07', period_start: '2020-06-08', - type: InvoiceItemType.RESERVED_TRANSACTIONS, + type: 'reserved_transactions', }, { amount: 0, @@ -42,7 +41,7 @@ export function InvoicePreviewFixture(params: Partial = {}): Previe description: '5 GB prepaid attachments', period_end: '2020-07-07', period_start: '2020-06-08', - type: InvoiceItemType.RESERVED_ATTACHMENTS, + type: 'reserved_attachments', }, ], ...params,