mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
+9
-2
@@ -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
@@ -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} */
|
||||
|
||||
Reference in New Issue
Block a user