Skip to content

fix(billing): Render unlimited reserved as ∞ in usage history#113259

Open
dashed wants to merge 2 commits into
masterfrom
aleal/fix-usage-history-unlimited-reserved-percentage
Open

fix(billing): Render unlimited reserved as ∞ in usage history#113259
dashed wants to merge 2 commits into
masterfrom
aleal/fix-usage-history-unlimited-reserved-percentage

Conversation

@dashed
Copy link
Copy Markdown
Member

@dashed dashed commented Apr 16, 2026

Billing admins reviewing past periods in the usage history table see >100% in the "Used (%)" column for any category that had unlimited reserved quota — exactly the row in the UI designed to surface overages.

The bug

usageHistory.tsx renders the per-category "Used (%)" cell with a nested ternary that special-cases RESERVED_BUDGET_QUOTA (-2) but not UNLIMITED_RESERVED (-1):

<td>
  {metricHistory.reserved === RESERVED_BUDGET_QUOTA
    ? 'N/A'
    : usagePercentage(
        convertUsageToReservedUnit(metricHistory.usage, metricHistory.category),
        metricHistory.prepaid
      )}
</td>

Inside usagePercentage:

function usagePercentage(usage: number, prepaid: number | null): string {
  if (prepaid === null || prepaid === 0) { return t('0%'); }
  if (usage > prepaid) { return '>100%'; }   // usage > -1 is always true
  return formatPercentage(usage / prepaid, 0);
}

For prepaid = UNLIMITED_RESERVED = -1, usage > -1 is always true for any non-negative usage, so the function returns '>100%'. Opposite of the truth.

The fix

Both layers, defense in depth:

1. Helper (usagePercentage) — early-return the UNLIMITED (∞) constant for unlimited prepaid, and export the helper so the unit-test layer can pin the contract:

-function usagePercentage(usage: number, prepaid: number | null): string {
+export function usagePercentage(usage: number, prepaid: number | null): string {
+  if (isUnlimitedReserved(prepaid)) {
+    return UNLIMITED;
+  }
   if (prepaid === null || prepaid === 0) {
     return t('0%');
   }

2. Call site — add a parallel isUnlimitedReserved(reserved) → UNLIMITED branch next to the existing RESERVED_BUDGET_QUOTA → 'N/A' guard:

 <td>
   {metricHistory.reserved === RESERVED_BUDGET_QUOTA
     ? 'N/A'
+    : isUnlimitedReserved(metricHistory.reserved)
+      ? UNLIMITED
     : usagePercentage(...)}
 </td>

Why both layers: the call-site guard reads the canonical metricHistory.reserved field and mirrors the neighboring RESERVED_BUDGET_QUOTA pattern. The helper guard protects against any caller — now or future — passing prepaid = -1, and enables pure unit tests of the helper contract.

Who is affected

Tests

  • describe('usagePercentage', ...) — 7 new pure-function unit tests of the exported helper (null, 0, unlimited, overage, under-quota, zero usage, exact match).
  • Inside the existing component describe — two new render-level cases pin the regression: the fully-unlimited row (reserved = prepaid = -1) and the defense-in-depth divergent case (reserved > 0, prepaid = -1). The existing "overage is shown as >100%" test remains green, confirming non-unlimited rows still surface overages correctly.

Relationship to #113142 / #113254

Third and final PR in a set addressing the UNLIMITED_RESERVED = -1 sentinel:

PR Surface
#113142 productIsEnabled + usage-overview URL selector
#113254 checkBudgetUsageFor + profiling billing banner
this PR usagePercentage + usage history table

All three now follow the same "treat -1 as active/unlimited, not as a small negative number" convention.

Refs #113142
Refs #113254

`usagePercentage` in `static/gsApp/views/subscriptionPage/usageHistory.tsx` returned
`>100%` for historical billing-period records where `prepaid` is `UNLIMITED_RESERVED`
(-1), because `usage > -1` is always true for non-negative usage. The call site at
the "Used (%)" cell special-cased `RESERVED_BUDGET_QUOTA` (-2) but not
`UNLIMITED_RESERVED`, so unlimited categories rendered as ">100%" — the opposite of
the truth — on every past-period row in the usage history table.

Apply the fix at both layers:
- Call site: add an `isUnlimitedReserved(metricHistory.reserved) → UNLIMITED`
  branch parallel to the existing `RESERVED_BUDGET_QUOTA → 'N/A'` guard.
- Helper (`usagePercentage`): early-return `UNLIMITED` when
  `isUnlimitedReserved(prepaid)`, as defense in depth. Export the helper so the
  unit-test layer can pin this contract.

Affected cohort: enterprise orgs promoted to unlimited via
`correct_enterprise_reserved()`, and any billing-history row from a past
`am3_t` / `am3_t_ent` trial period. Same cohort as #113142 and #113254.

Add unit tests for the now-exported `usagePercentage` (7 cases: null, 0, unlimited,
overage, under-quota, zero usage, exact match) and component regression tests for
both the fully-unlimited row and the defense-in-depth `reserved > 0, prepaid = -1`
case.

Refs #113142
Refs #113254

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Apr 16, 2026
@dashed dashed self-assigned this Apr 16, 2026
Address code-review follow-ups on #113259:
- Drop the dead top-level `reserved`/`usage` fields from the new BillingHistoryFixture
  calls. `UsageHistoryRow` only reads `history.categories`; these were copy-pasted
  from the adjacent overage test and unused.
- Scope the cell assertions to the Errors row via `within()`, and use `getByRole`
  where exactly-one ∞ is expected (implicitly pinning the count), so the tests are
  self-describing.
- Drop the inline WHAT comments now that the assertions encode the intent.

Co-Authored-By: Claude <noreply@anthropic.com>
@dashed dashed marked this pull request as ready for review April 16, 2026 23:55
@dashed dashed requested a review from a team as a code owner April 16, 2026 23:55
metricHistory.prepaid
)}
: isUnlimitedReserved(metricHistory.reserved)
? UNLIMITED
Copy link
Copy Markdown
Member

@brendanhsentry brendanhsentry Apr 20, 2026

Choose a reason for hiding this comment

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

It makes more sense to make this 'N/A' imo. Infinite percent used could be misleading

@getsantry
Copy link
Copy Markdown
Contributor

getsantry Bot commented May 12, 2026

This pull request has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you add the label WIP, I will leave it alone unless WIP is removed ... forever!


"A weed is but an unloved flower." ― Ella Wheeler Wilcox 🥀

@getsantry getsantry Bot added the Stale label May 12, 2026
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 Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants