diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index 6d6bae5261fd44..1eb4c4e7247219 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -865,19 +865,39 @@ export type PreviewInvoiceItem = BaseInvoiceItem & { period_start: string; }; -// TODO(data categories): BIL-970 -export enum CreditType { - ERROR = 'error', - TRANSACTION = 'transaction', - SPAN = 'span', - PROFILE_DURATION = 'profile_duration', - PROFILE_DURATION_UI = 'profile_duration_ui', - ATTACHMENT = 'attachment', - REPLAY = 'replay', - DISCOUNT = 'discount', - PERCENT = 'percent', - LOG_BYTE = 'log_byte', -} +/** + * Dynamically generate credit types from DATA_CATEGORY_INFO. + * Uses SINGULAR form (unlike InvoiceItemType which uses plural). + * This automatically includes new billing categories without manual enum updates. + * + * Follows the pattern: snake_case of singular + * Example: DATA_CATEGORY_INFO.ERROR (singular: "error") -> "error" + * Example: DATA_CATEGORY_INFO.LOG_BYTE (singular: "logByte") -> "log_byte" + */ +type DynamicCreditType = { + [K in keyof typeof DATA_CATEGORY_INFO]: (typeof DATA_CATEGORY_INFO)[K]['isBilledCategory'] extends true + ? CamelToSnake<(typeof DATA_CATEGORY_INFO)[K]['singular']> + : never; +}[keyof typeof DATA_CATEGORY_INFO]; + +/** + * Static credit types not tied to data categories. + * These must be manually maintained but change infrequently. + */ +type StaticCreditType = + | 'discount' // Dollar-based recurring discount + | 'percent' // Percentage-based recurring discount + | 'seer_user'; // Special: maps to PREVENT_USER category (temporary until category renamed) + +/** + * Complete credit 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: CreditType.ERROR + * After: 'error' + */ +export type CreditType = DynamicCreditType | StaticCreditType; type BaseRecurringCredit = { amount: number; @@ -888,26 +908,27 @@ type BaseRecurringCredit = { interface RecurringDiscount extends BaseRecurringCredit { totalAmountRemaining: number; - type: CreditType.DISCOUNT; + type: 'discount'; } interface RecurringPercentDiscount extends BaseRecurringCredit { percentPoints: number; totalAmountRemaining: number; - type: CreditType.PERCENT; + type: 'percent'; } interface RecurringEventCredit extends BaseRecurringCredit { totalAmountRemaining: null; type: - | CreditType.ERROR - | CreditType.TRANSACTION - | CreditType.SPAN - | CreditType.PROFILE_DURATION - | CreditType.PROFILE_DURATION_UI - | CreditType.ATTACHMENT - | CreditType.REPLAY - | CreditType.LOG_BYTE; + | 'error' + | 'transaction' + | 'span' + | 'profile_duration' + | 'profile_duration_ui' + | 'attachment' + | 'replay' + | 'log_byte' + | 'seer_user'; } export type RecurringCredit = diff --git a/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx b/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx index 9b6ed49849b6dd..cef9258b3b0d36 100644 --- a/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx +++ b/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx @@ -7,7 +7,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {DataCategory} from 'sentry/types/core'; -import {CreditType, type Plan} from 'getsentry/types'; +import type {Plan} from 'getsentry/types'; import RecurringCredits from 'getsentry/views/subscriptionPage/recurringCredits'; describe('Recurring Credits', () => { @@ -34,7 +34,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 1500, - type: CreditType.DISCOUNT, + type: 'discount', totalAmountRemaining: 7500, }), ], @@ -61,7 +61,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 100_000, - type: CreditType.TRANSACTION, + type: 'transaction', totalAmountRemaining: null, }), ], @@ -88,7 +88,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 10, - type: CreditType.PROFILE_DURATION, + type: 'profile_duration', totalAmountRemaining: null, }), ], @@ -125,7 +125,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 10, - type: CreditType.PROFILE_DURATION_UI, + type: 'profile_duration_ui', totalAmountRemaining: null, }), ], @@ -162,7 +162,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 1.5, - type: CreditType.ATTACHMENT, + type: 'attachment', totalAmountRemaining: null, }), ], @@ -189,7 +189,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 3_000_000, - type: CreditType.REPLAY, + type: 'replay', totalAmountRemaining: null, }), ], @@ -216,7 +216,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 2.5, - type: CreditType.LOG_BYTE, + type: 'log_byte', totalAmountRemaining: null, }), ], @@ -243,7 +243,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: '2021-12-01', amount: 50000, - type: CreditType.ERROR, + type: 'error', totalAmountRemaining: null, }), RecurringCreditFixture({ @@ -251,7 +251,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: '2022-01-01', amount: 100000, - type: CreditType.ERROR, + type: 'error', totalAmountRemaining: null, }), ], @@ -287,7 +287,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 1500, - type: CreditType.DISCOUNT, + type: 'discount', totalAmountRemaining: 7500, }), ], diff --git a/static/gsApp/views/subscriptionPage/recurringCredits.tsx b/static/gsApp/views/subscriptionPage/recurringCredits.tsx index ff8381c511c49e..d189e6cabdde36 100644 --- a/static/gsApp/views/subscriptionPage/recurringCredits.tsx +++ b/static/gsApp/views/subscriptionPage/recurringCredits.tsx @@ -10,8 +10,7 @@ import {space} from 'sentry/styles/space'; import type {DataCategory} from 'sentry/types/core'; import {useRecurringCredits} from 'getsentry/hooks/useRecurringCredits'; -import type {Plan, RecurringCredit} from 'getsentry/types'; -import {CreditType} from 'getsentry/types'; +import type {CreditType, Plan, RecurringCredit} from 'getsentry/types'; import {formatReservedWithUnits} from 'getsentry/utils/billing'; import {getCreditDataCategory, getPlanCategoryName} from 'getsentry/utils/dataCategory'; import {displayPrice} from 'getsentry/views/amCheckout/utils'; @@ -25,7 +24,7 @@ const isExpired = (date: moment.MomentInput) => { const getActiveDiscounts = (recurringCredits: RecurringCredit[]) => recurringCredits.filter( credit => - (credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT) && + (credit.type === 'discount' || credit.type === 'percent') && credit.totalAmountRemaining > 0 && !isExpired(credit.periodEnd) ); @@ -58,7 +57,7 @@ function RecurringCredits({displayType, planDetails}: Props) { } const getTooltipTitle = (credit: RecurringCredit) => { - return credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT + return credit.type === 'discount' || credit.type === 'percent' ? tct('[amount] per month or [annualAmount] remaining towards an annual plan.', { amount: displayPrice({cents: credit.amount}), annualAmount: displayPrice({ @@ -69,7 +68,7 @@ function RecurringCredits({displayType, planDetails}: Props) { }; const getAmount = (credit: RecurringCredit, category: DataCategory | CreditType) => { - if (credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT) { + if (credit.type === 'discount' || credit.type === 'percent') { return ( {tct('[amount]/mo', { diff --git a/tests/js/getsentry-test/fixtures/recurringCredit.ts b/tests/js/getsentry-test/fixtures/recurringCredit.ts index 5f6d9894899841..31a2e00176a0db 100644 --- a/tests/js/getsentry-test/fixtures/recurringCredit.ts +++ b/tests/js/getsentry-test/fixtures/recurringCredit.ts @@ -1,7 +1,6 @@ import moment from 'moment-timezone'; import type {RecurringCredit as TRecurringCredit} from 'getsentry/types'; -import {CreditType} from 'getsentry/types'; export function RecurringCreditFixture(params?: TRecurringCredit): TRecurringCredit { return { @@ -9,7 +8,7 @@ export function RecurringCreditFixture(params?: TRecurringCredit): TRecurringCre periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 50000, - type: CreditType.ERROR, + type: 'error', totalAmountRemaining: null, ...params, };