Skip to content

Commit 0ef83ff

Browse files
ref(billing): Convert CreditType to dynamic type union (#103815)
Closes https://linear.app/getsentry/issue/BIL-970/dynamic-credittype-fe Follow up to #103664 This converts `CreditType` from a hardcoded enum to a dynamically generated type union, following the same pattern as #103664 (`InvoiceItemType`) and `EventType`. Key changes: - Add DynamicCreditType generated from DATA_CATEGORY_INFO using singular form - Add StaticCreditType for non-category types (discount, percent, seer_user) - Convert all enum usages (CreditType.VALUE) to string literals ('value') - Update test fixtures and component code - Fix eslint unused-vars warning for DOMAttributes<T> with disable comment This automatically includes new billing categories (like seer_user which was previously missing from the frontend) and eliminates manual enum maintenance. --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 3c83451 commit 0ef83ff

File tree

4 files changed

+60
-41
lines changed

4 files changed

+60
-41
lines changed

static/gsApp/types/index.tsx

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -864,19 +864,39 @@ export type PreviewInvoiceItem = BaseInvoiceItem & {
864864
period_start: string;
865865
};
866866

867-
// TODO(data categories): BIL-970
868-
export enum CreditType {
869-
ERROR = 'error',
870-
TRANSACTION = 'transaction',
871-
SPAN = 'span',
872-
PROFILE_DURATION = 'profile_duration',
873-
PROFILE_DURATION_UI = 'profile_duration_ui',
874-
ATTACHMENT = 'attachment',
875-
REPLAY = 'replay',
876-
DISCOUNT = 'discount',
877-
PERCENT = 'percent',
878-
LOG_BYTE = 'log_byte',
879-
}
867+
/**
868+
* Dynamically generate credit types from DATA_CATEGORY_INFO.
869+
* Uses SINGULAR form (unlike InvoiceItemType which uses plural).
870+
* This automatically includes new billing categories without manual enum updates.
871+
*
872+
* Follows the pattern: snake_case of singular
873+
* Example: DATA_CATEGORY_INFO.ERROR (singular: "error") -> "error"
874+
* Example: DATA_CATEGORY_INFO.LOG_BYTE (singular: "logByte") -> "log_byte"
875+
*/
876+
type DynamicCreditType = {
877+
[K in keyof typeof DATA_CATEGORY_INFO]: (typeof DATA_CATEGORY_INFO)[K]['isBilledCategory'] extends true
878+
? CamelToSnake<(typeof DATA_CATEGORY_INFO)[K]['singular']>
879+
: never;
880+
}[keyof typeof DATA_CATEGORY_INFO];
881+
882+
/**
883+
* Static credit types not tied to data categories.
884+
* These must be manually maintained but change infrequently.
885+
*/
886+
type StaticCreditType =
887+
| 'discount' // Dollar-based recurring discount
888+
| 'percent' // Percentage-based recurring discount
889+
| 'seer_user'; // Special: maps to PREVENT_USER category (temporary until category renamed)
890+
891+
/**
892+
* Complete credit type union.
893+
* Automatically stays in sync with backend when new billing categories are added.
894+
*
895+
* Migration from enum: Use string literals instead of enum members.
896+
* Before: CreditType.ERROR
897+
* After: 'error'
898+
*/
899+
export type CreditType = DynamicCreditType | StaticCreditType;
880900

881901
type BaseRecurringCredit = {
882902
amount: number;
@@ -887,26 +907,27 @@ type BaseRecurringCredit = {
887907

888908
interface RecurringDiscount extends BaseRecurringCredit {
889909
totalAmountRemaining: number;
890-
type: CreditType.DISCOUNT;
910+
type: 'discount';
891911
}
892912

893913
interface RecurringPercentDiscount extends BaseRecurringCredit {
894914
percentPoints: number;
895915
totalAmountRemaining: number;
896-
type: CreditType.PERCENT;
916+
type: 'percent';
897917
}
898918

899919
interface RecurringEventCredit extends BaseRecurringCredit {
900920
totalAmountRemaining: null;
901921
type:
902-
| CreditType.ERROR
903-
| CreditType.TRANSACTION
904-
| CreditType.SPAN
905-
| CreditType.PROFILE_DURATION
906-
| CreditType.PROFILE_DURATION_UI
907-
| CreditType.ATTACHMENT
908-
| CreditType.REPLAY
909-
| CreditType.LOG_BYTE;
922+
| 'error'
923+
| 'transaction'
924+
| 'span'
925+
| 'profile_duration'
926+
| 'profile_duration_ui'
927+
| 'attachment'
928+
| 'replay'
929+
| 'log_byte'
930+
| 'seer_user';
910931
}
911932

912933
export type RecurringCredit =

static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';
77

88
import {DataCategory} from 'sentry/types/core';
99

10-
import {CreditType, type Plan} from 'getsentry/types';
10+
import type {Plan} from 'getsentry/types';
1111
import RecurringCredits from 'getsentry/views/subscriptionPage/recurringCredits';
1212

1313
describe('Recurring Credits', () => {
@@ -34,7 +34,7 @@ describe('Recurring Credits', () => {
3434
periodStart: moment().format(),
3535
periodEnd: moment().utc().add(3, 'months').format(),
3636
amount: 1500,
37-
type: CreditType.DISCOUNT,
37+
type: 'discount',
3838
totalAmountRemaining: 7500,
3939
}),
4040
],
@@ -61,7 +61,7 @@ describe('Recurring Credits', () => {
6161
periodStart: moment().format(),
6262
periodEnd: moment().utc().add(3, 'months').format(),
6363
amount: 100_000,
64-
type: CreditType.TRANSACTION,
64+
type: 'transaction',
6565
totalAmountRemaining: null,
6666
}),
6767
],
@@ -88,7 +88,7 @@ describe('Recurring Credits', () => {
8888
periodStart: moment().format(),
8989
periodEnd: moment().utc().add(3, 'months').format(),
9090
amount: 10,
91-
type: CreditType.PROFILE_DURATION,
91+
type: 'profile_duration',
9292
totalAmountRemaining: null,
9393
}),
9494
],
@@ -125,7 +125,7 @@ describe('Recurring Credits', () => {
125125
periodStart: moment().format(),
126126
periodEnd: moment().utc().add(3, 'months').format(),
127127
amount: 10,
128-
type: CreditType.PROFILE_DURATION_UI,
128+
type: 'profile_duration_ui',
129129
totalAmountRemaining: null,
130130
}),
131131
],
@@ -162,7 +162,7 @@ describe('Recurring Credits', () => {
162162
periodStart: moment().format(),
163163
periodEnd: moment().utc().add(3, 'months').format(),
164164
amount: 1.5,
165-
type: CreditType.ATTACHMENT,
165+
type: 'attachment',
166166
totalAmountRemaining: null,
167167
}),
168168
],
@@ -189,7 +189,7 @@ describe('Recurring Credits', () => {
189189
periodStart: moment().format(),
190190
periodEnd: moment().utc().add(3, 'months').format(),
191191
amount: 3_000_000,
192-
type: CreditType.REPLAY,
192+
type: 'replay',
193193
totalAmountRemaining: null,
194194
}),
195195
],
@@ -216,7 +216,7 @@ describe('Recurring Credits', () => {
216216
periodStart: moment().format(),
217217
periodEnd: moment().utc().add(3, 'months').format(),
218218
amount: 2.5,
219-
type: CreditType.LOG_BYTE,
219+
type: 'log_byte',
220220
totalAmountRemaining: null,
221221
}),
222222
],
@@ -243,15 +243,15 @@ describe('Recurring Credits', () => {
243243
periodStart: moment().format(),
244244
periodEnd: '2021-12-01',
245245
amount: 50000,
246-
type: CreditType.ERROR,
246+
type: 'error',
247247
totalAmountRemaining: null,
248248
}),
249249
RecurringCreditFixture({
250250
id: 1,
251251
periodStart: moment().format(),
252252
periodEnd: '2022-01-01',
253253
amount: 100000,
254-
type: CreditType.ERROR,
254+
type: 'error',
255255
totalAmountRemaining: null,
256256
}),
257257
],
@@ -287,7 +287,7 @@ describe('Recurring Credits', () => {
287287
periodStart: moment().format(),
288288
periodEnd: moment().utc().add(3, 'months').format(),
289289
amount: 1500,
290-
type: CreditType.DISCOUNT,
290+
type: 'discount',
291291
totalAmountRemaining: 7500,
292292
}),
293293
],

static/gsApp/views/subscriptionPage/recurringCredits.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {space} from 'sentry/styles/space';
1010
import type {DataCategory} from 'sentry/types/core';
1111

1212
import {useRecurringCredits} from 'getsentry/hooks/useRecurringCredits';
13-
import type {Plan, RecurringCredit} from 'getsentry/types';
14-
import {CreditType} from 'getsentry/types';
13+
import type {CreditType, Plan, RecurringCredit} from 'getsentry/types';
1514
import {formatReservedWithUnits} from 'getsentry/utils/billing';
1615
import {getCreditDataCategory, getPlanCategoryName} from 'getsentry/utils/dataCategory';
1716
import {displayPrice} from 'getsentry/views/amCheckout/utils';
@@ -25,7 +24,7 @@ const isExpired = (date: moment.MomentInput) => {
2524
const getActiveDiscounts = (recurringCredits: RecurringCredit[]) =>
2625
recurringCredits.filter(
2726
credit =>
28-
(credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT) &&
27+
(credit.type === 'discount' || credit.type === 'percent') &&
2928
credit.totalAmountRemaining > 0 &&
3029
!isExpired(credit.periodEnd)
3130
);
@@ -58,7 +57,7 @@ function RecurringCredits({displayType, planDetails}: Props) {
5857
}
5958

6059
const getTooltipTitle = (credit: RecurringCredit) => {
61-
return credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT
60+
return credit.type === 'discount' || credit.type === 'percent'
6261
? tct('[amount] per month or [annualAmount] remaining towards an annual plan.', {
6362
amount: displayPrice({cents: credit.amount}),
6463
annualAmount: displayPrice({
@@ -69,7 +68,7 @@ function RecurringCredits({displayType, planDetails}: Props) {
6968
};
7069

7170
const getAmount = (credit: RecurringCredit, category: DataCategory | CreditType) => {
72-
if (credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT) {
71+
if (credit.type === 'discount' || credit.type === 'percent') {
7372
return (
7473
<Fragment>
7574
{tct('[amount]/mo', {

tests/js/getsentry-test/fixtures/recurringCredit.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import moment from 'moment-timezone';
22

33
import type {RecurringCredit as TRecurringCredit} from 'getsentry/types';
4-
import {CreditType} from 'getsentry/types';
54

65
export function RecurringCreditFixture(params?: TRecurringCredit): TRecurringCredit {
76
return {
87
id: 1,
98
periodStart: moment().format(),
109
periodEnd: moment().utc().add(3, 'months').format(),
1110
amount: 50000,
12-
type: CreditType.ERROR,
11+
type: 'error',
1312
totalAmountRemaining: null,
1413
...params,
1514
};

0 commit comments

Comments
 (0)