Skip to content

Conversation

@dashed
Copy link
Member

@dashed dashed commented Nov 19, 2025

Closes https://linear.app/getsentry/issue/BIL-969/dynamic-invoiceitemtypefe

Replaces the hardcoded InvoiceItemType enum with a dynamically-generated type union that automatically syncs with DATA_CATEGORY_INFO. This eliminates the need for manual enum updates whenever new billing categories are added.

The implementation follows the existing EventType pattern and uses TypeScript mapped types to generate ondemand_* and reserved_* invoice item types from all billed categories. Added CamelToSnake helper type to convert DataCategory's camelCase values to backend's snake_case format.

This change also fixes 4 missing invoice types (ondemand_profile_duration_ui, ondemand_log_bytes, ondemand_prevent_users, reserved_profile_duration_ui) and prevents future category additions from causing missing type bugs.

@dashed dashed self-assigned this Nov 19, 2025
@dashed dashed requested a review from a team as a code owner November 19, 2025 18:28
@linear
Copy link

linear bot commented Nov 19, 2025

Copy link
Member

@isabellaenriquez isabellaenriquez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice

just one thing to address, everything else is nits

* Static invoice item types that are not tied to data categories.
* These must be manually maintained but change infrequently.
*/
type StaticInvoiceItemType =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll just need to add activated_seer_users

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the missing invoice types.

Comment on lines 696 to 701
| 'ondemand' // Legacy: generic ondemand for AM1 plans
| 'subscription_credit'
| 'balance_change'
| 'cancellation_fee'
| 'attachments' // Legacy: AM1 plans
| 'transactions' // Legacy: AM1 plans
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious, do we still use these types even today for AM1? or is it just on their old invoices?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just their old invoices.

Comment on lines 808 to 811
'subscription_credit',
'credit_applied', // TODO(isabella): This is deprecated and replaced by BALANCE_CHANGE
'discount',
'recurring_discount',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could also put theses in their own type (ie. CreditInvoiceItemType) rather than part of StaticInvoiceItemType and add it to the InvoiceItemType; then here instead of having the strings hardcoded, we could check the new sub type

same for the fees, but not a blocker just a nice to have

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's separated out now.

Replaces the hardcoded InvoiceItemType enum with a dynamically-generated
type union that automatically syncs with DATA_CATEGORY_INFO. This eliminates
the need for manual enum updates whenever new billing categories are added.

The implementation follows the existing EventType pattern and uses TypeScript
mapped types to generate ondemand_* and reserved_* invoice item types from
all billed categories. Added CamelToSnake helper type to convert DataCategory's
camelCase values to backend's snake_case format.

This change fixes 4 missing invoice types (ondemand_profile_duration_ui,
ondemand_log_bytes, ondemand_prevent_users, reserved_profile_duration_ui)
and prevents future category additions from causing missing type bugs.

Refs BIL-969
…s types

Adds two special-case invoice item types that were missing from the
initial dynamic type implementation:

- reserved_seer_users: Used for PREVENT_USER category reserved billing.
  Backend maps PREVENT_USER to this instead of reserved_prevent_users
  due to naming conventions that will be unified later.

- activated_seer_users: Used for activation-based billing model for
  Prevent users. This is a newer billing model that works similarly
  to PAYG but is tracked separately.

These types don't follow the standard category naming pattern due to
backend special-case logic in invoice_item_type.py for_reserved() and
to_activated_attr() methods.

Refs BIL-969
…gories

- Created const arrays for credit, fee, seer, and legacy invoice types
- Updated getCredits and getFees to use const arrays instead of hardcoded strings
- Composed StaticInvoiceItemType from organized sub-types
- Enables runtime usage in filters while maintaining type safety

This eliminates duplication between type definitions and runtime values,
providing a single source of truth for invoice item type categorization.
The previous implementation incorrectly handled consecutive capital letters
by inserting underscores between each letter. For example, 'profileDurationUI'
was converted to 'profile_duration_u_i' instead of 'profile_duration_ui'.

The fix tracks the previous character's case state and looks ahead at the next
character to determine if we're at a word boundary:
- Consecutive capitals (like 'UI') are kept together without underscores
- Transitions from lowercase to uppercase still insert underscores
- Transitions from uppercase to lowercase insert underscores before the capital

This ensures backend type alignment:
- ondemand_profile_duration_ui (was: ondemand_profile_duration_u_i)
- reserved_profile_duration_ui (was: reserved_profile_duration_u_i)
Removed unnecessary type assertions that were flagged by ESLint:
- inputField.tsx: Removed `as any` from e.target.value
- numberField.tsx: Removed `as any` from e.target.value
- onboarding.tsx: Removed `as number` assertions from stepper onClick handler

These assertions were unnecessary because TypeScript already infers the correct
types from the event handlers and component props.
[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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Dynamic type generates incorrect prevent user invoice types

The PREVENT_USER category has isBilledCategory: true which causes the dynamic type system to generate reserved_prevent_users and ondemand_prevent_users invoice types. However, the backend actually uses reserved_seer_users for prevent users (as noted in the comment on line 730). This mismatch means the dynamically generated types won't match actual backend invoice types, and the invoiceItemTypeToAddOn function checking for reserved_prevent_users won't match invoices with type reserved_seer_users.

Fix in Cursor Fix in Web

Removed export keywords from unused constants and types:
- SEER_INVOICE_ITEM_TYPES → _SEER_INVOICE_ITEM_TYPES (only used for type derivation)
- LEGACY_INVOICE_ITEM_TYPES → _LEGACY_INVOICE_ITEM_TYPES (only used for type derivation)
- CreditInvoiceItemType, FeeInvoiceItemType, etc. (only used internally to compose StaticInvoiceItemType)

Only CREDIT_INVOICE_ITEM_TYPES and FEE_INVOICE_ITEM_TYPES remain exported as
they are actively used in billing.tsx for runtime filtering.
dashed and others added 2 commits November 19, 2025 21:11
Root cause: Changing DOMAttributes<T> to DOMAttributes<_T> broke React's
module augmentation, causing all React components to lose standard props.

Fixes:
1. Reverted DOMAttributes<_T> back to DOMAttributes<T> with eslint-disable
   - Module augmentation requires matching generic parameter names
   - Added eslint-disable-next-line for the unused-vars false positive

2. Fixed inputField and numberField type assertions
   - Restored (e.target as HTMLInputElement) for onKeyDown handlers
   - e.target is EventTarget, needs cast to access .value property

3. Fixed Stepper component onClick type conflict
   - Used Omit to exclude onClick from React.HTMLAttributes
   - Prevents intersection type conflict between DOM and custom onClick

4. Removed unused DataCategory import from utils.spec.tsx

All TypeScript checks now pass with exit code 0.
case 'reserved_seer_budget':
return AddOnCategory.SEER;
case InvoiceItemType.RESERVED_PREVENT_USERS:
case 'reserved_prevent_users':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing invoice type mappings for Prevent addon

The invoiceItemTypeToAddOn function only maps 'reserved_prevent_users' to AddOnCategory.PREVENT, but the type definitions include 'reserved_seer_users' and 'activated_seer_users' which are documented as prevent user billing types. If the backend sends these invoice types, the function returns null instead of correctly identifying them as Prevent addon items, potentially breaking addon-related logic.

Fix in Cursor Fix in Web

@dashed dashed requested review from a team and isabellaenriquez November 20, 2025 18:57
Copy link
Member

@isabellaenriquez isabellaenriquez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, love the random type fixes lol

@dashed dashed merged commit ad4eceb into master Nov 21, 2025
47 checks passed
@dashed dashed deleted the billing/BIL-969 branch November 21, 2025 15:06
dashed added a commit that referenced this pull request Nov 21, 2025
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants