feat(billing): include prepaid credits in credit balance (#45177)

### Summary

This PR updates the logic to include `prepaid_credits_balance` while
showing the existing customer balance.

This changes the credit balance shown in:

- Billing Settings > Credit Balance
- Credit code redemption modal

The displayed amount now reflects the total credit balance across
prepaid credits and the customer balance.

### Testing

- Open an org billing page with prepaid credits and verify Credit
Balance includes both sources.
- Open the credit redemption modal and verify Current Balance matches
the combined credit amount.
- Verify an org with only customer balance still shows the same credit
amount as before.
- Verify an org with only prepaid credits balance and no customer
balance now shows credits correctly.
- Verify an org with no credits shows 0.00 and does not show /credits.
- Verify an org where net balance is debt still shows a negative amount
without the /credits suffix.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Credit balance display now includes purchased and prorated credits for
a complete account view.
* Credit redemption and current-balance screens now show combined credit
totals (prepaid + existing) for clearer availability.
* UI descriptive text clarified to explain how credits are applied and
how charges occur once credits are exhausted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Kanishk Dudeja
2026-04-24 15:24:24 +05:30
committed by GitHub
parent 9743da0167
commit 9d2807e19b
4 changed files with 37 additions and 13 deletions
@@ -3,6 +3,7 @@ import { useParams } from 'common'
import { CreditCodeRedemption } from './CreditCodeRedemption'
import { CreditTopUp } from './CreditTopUp'
import { getTotalCreditBalanceCents } from './helpers'
import {
ScaffoldSection,
ScaffoldSectionContent,
@@ -31,13 +32,14 @@ const CreditBalance = () => {
isSuccess,
} = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canReadSubscriptions })
const customerBalance = (subscription?.customer_balance ?? 0) / 100
const isCredit = customerBalance < 0
const isDebt = customerBalance > 0
const balance =
isCredit && customerBalance !== 0
? customerBalance.toFixed(2).toString().replace('-', '')
: customerBalance.toFixed(2)
const combinedCreditBalanceCents = getTotalCreditBalanceCents({
customerBalance: subscription?.customer_balance,
prepaidCreditsBalance: subscription?.prepaid_credits_balance,
})
const combinedCreditBalance = combinedCreditBalanceCents / 100
const hasCredits = combinedCreditBalanceCents > 0
const hasDebt = combinedCreditBalanceCents < 0
const balance = Math.abs(combinedCreditBalance).toFixed(2)
return (
<ScaffoldSection>
@@ -47,8 +49,11 @@ const CreditBalance = () => {
<p className="text-foreground text-base m-0">Credit Balance</p>
</div>
<p className="text-sm text-foreground-light m-0">
Credits will be applied to future invoices, before charging your payment method. If your
credit balance runs out, your default payment method will be charged.
Credits will be applied to future invoices, before charging your payment method. This
balance includes purchased credits and any prorated credits from plan changes.
</p>
<p className="text-sm text-foreground-light m-0">
If your credits run out, your default payment method will be charged.
</p>
</div>
</ScaffoldSectionDetail>
@@ -79,10 +84,10 @@ const CreditBalance = () => {
<div className="flex w-full justify-between items-center">
<span>Balance</span>
<div className="flex items-center space-x-1">
{isDebt && <h4 className="opacity-50">-</h4>}
{hasDebt && <h4 className="opacity-50">-</h4>}
<h4 className="opacity-50">$</h4>
<h1 className="relative">{balance}</h1>
{isCredit && <h4 className="opacity-50">/credits</h4>}
{hasCredits && <h4 className="opacity-50">/credits</h4>}
</div>
</div>
)}
@@ -26,6 +26,7 @@ import { Admonition, ShimmeringLoader, TimestampInfo } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
import { getTotalCreditBalanceCents } from './helpers'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
import { useOrganizationCreditCodeRedemptionMutation } from '@/data/organizations/organization-credit-code-redemption-mutation'
@@ -59,6 +60,12 @@ export const CreditCodeRedemption = ({
const { data: org, isLoading: isOrgLoading } = useOrganizationQuery({ slug })
const { data: customerProfile, isLoading: isCustomerProfileLoading } =
useOrganizationCustomerProfileQuery({ slug })
const combinedCreditBalanceCents = customerProfile
? getTotalCreditBalanceCents({
customerBalance: customerProfile.balance,
prepaidCreditsBalance: customerProfile.prepaid_credits_balance,
})
: undefined
const { can: canRedeemCode, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
@@ -282,12 +289,12 @@ export const CreditCodeRedemption = ({
)}
/>
{customerProfile && customerProfile.balance < 0 && (
{combinedCreditBalanceCents !== undefined && combinedCreditBalanceCents > 0 && (
<div className="flex w-full justify-between items-center">
<span className="text-sm">Current Balance</span>
<div className="flex items-center gap-x-1">
<p className="opacity-50 text-sm">$</p>
<p className="text-2xl">{customerProfile.balance / -100}</p>
<p className="text-2xl">{combinedCreditBalanceCents / 100}</p>
<p className="opacity-50 text-sm">/credits</p>
</div>
</div>
@@ -94,3 +94,13 @@ export const generateUpgradeReasons = (originalPlan?: string, upgradedPlan?: str
return reasons
}
// For `customerBalance`, negative sign means credit.
// Negate it first so both sources contribute as positive credit amounts before combining.
export const getTotalCreditBalanceCents = ({
customerBalance = 0,
prepaidCreditsBalance = 0,
}: {
customerBalance?: number
prepaidCreditsBalance?: number
}) => -customerBalance + prepaidCreditsBalance
+2
View File
@@ -6071,6 +6071,7 @@ export interface components {
billing_name?: string
billing_via_partner: boolean
email: string
prepaid_credits_balance?: number
tax_id: {
country: string
type: string
@@ -6767,6 +6768,7 @@ export interface components {
id: 'free' | 'pro' | 'team' | 'enterprise' | 'platform'
name: string
}
prepaid_credits_balance?: number
project_addons: {
addons: {
/** @enum {string} */