diff --git a/components/console/PlansPanel.tsx b/components/console/PlansPanel.tsx index 8445a27..96adf71 100644 --- a/components/console/PlansPanel.tsx +++ b/components/console/PlansPanel.tsx @@ -21,10 +21,13 @@ import { } from "@/lib/console/useBillingPlans"; import { redirectToCheckout } from "@/lib/console/checkout-redirect"; import { useAuth } from "@/components/console/AuthContext"; -import { useWalletBillingState } from "@/lib/console/useOwnerWallet"; +import { + SESSION_USAGE_OPTIONS, + useAccountUsage, +} from "@/lib/console/useAccountUsage"; import { includedUsageRemainingLabel, - includedUsageSummary, + includedUsageSummaryFromBalance, } from "@/lib/console/wallet-settlement-display"; function isUsagePlan( @@ -82,10 +85,18 @@ function clearCheckoutQueryParam(): void { export default function PlansPanel() { const { isConnected } = useAuth(); const { state, reload, subscribe, changePlan } = useBillingPlans(isConnected); - const wallet = useWalletBillingState(isConnected); + const usage = useAccountUsage(isConnected, SESSION_USAGE_OPTIONS); const included = - wallet.state.status === "ready" - ? includedUsageSummary(wallet.state.wallet.billingState) + usage.status === "ready" + ? includedUsageSummaryFromBalance( + usage.data.balance, + state.status === "ready" && state.subscription + ? { + id: state.subscription.planId, + name: state.subscription.planName, + } + : undefined + ) : null; const [busyPlanId, setBusyPlanId] = useState(null); const [error, setError] = useState(null); diff --git a/components/console/SidebarUsageCard.tsx b/components/console/SidebarUsageCard.tsx index c9aad3e..206cf61 100644 --- a/components/console/SidebarUsageCard.tsx +++ b/components/console/SidebarUsageCard.tsx @@ -2,9 +2,12 @@ import Link from "next/link"; import { useAuth } from "@/components/console/AuthContext"; -import { useWalletBillingState } from "@/lib/console/useOwnerWallet"; +import { + SESSION_USAGE_OPTIONS, + useAccountUsage, +} from "@/lib/console/useAccountUsage"; import { microsToUsd } from "@/lib/console/usage-capability-display"; -import { includedUsageSummary } from "@/lib/console/wallet-settlement-display"; +import { includedUsageSummaryFromBalance } from "@/lib/console/wallet-settlement-display"; /** * Sidebar balance meter. It shows the remaining balance against the amount @@ -12,16 +15,15 @@ import { includedUsageSummary } from "@/lib/console/wallet-settlement-display"; */ export default function SidebarUsageCard() { const { isConnected } = useAuth(); - const wallet = useWalletBillingState(isConnected); + const usage = useAccountUsage(isConnected, SESSION_USAGE_OPTIONS); const included = - wallet.state.status === "ready" - ? includedUsageSummary(wallet.state.wallet.billingState) + usage.status === "ready" + ? includedUsageSummaryFromBalance(usage.data.balance) : null; - if ( - isConnected && - (wallet.state.status === "loading" || wallet.state.status === "idle") - ) { + if (!isConnected) return null; + + if (usage.status === "loading" || usage.status === "idle") { return (
0 ? Math.min(100, (remainingUsd / issuedUsd) * 100) : 0; - const canSpend = wallet.state.wallet.billingState.canSpend; + const canSpend = usage.data.balance?.hasAccess ?? remainingUsd > 0; return ( { - if (walletState.state.status !== "ready" || !included) return null; - const funding = walletState.state.wallet.billingState.funding; - const overage = funding.overage; + if (usage.status !== "ready") return null; return { - includedTotal: microsToUsd(included.totalUsdMicros), - consumed: microsToUsd(included.consumedUsdMicros), - credits: microsToUsd(walletState.state.wallet.balance?.usdMicros ?? "0"), - overage: - overage.eligible && overage.remaining - ? microsToUsd(overage.remaining.usdMicros) - : null, + includedTotal: included ? microsToUsd(included.totalUsdMicros) : 0, + consumed: included ? microsToUsd(included.consumedUsdMicros) : 0, + credits: 0, + overage: null, }; - }, [walletState.state, included]); + }, [usage.status, included]); - const loading = - walletState.state.status === "loading" || - walletState.state.status === "idle"; + const loading = usage.status === "loading" || usage.status === "idle"; return (
@@ -221,12 +216,9 @@ export default function UsageView() { />
- {walletState.state.status === "error" && ( + {usage.status === "error" && (
- +
)} diff --git a/components/console/settings/BillingSection.tsx b/components/console/settings/BillingSection.tsx index e05c738..db80366 100644 --- a/components/console/settings/BillingSection.tsx +++ b/components/console/settings/BillingSection.tsx @@ -32,9 +32,13 @@ import { startWalletTopUp, useWalletBillingState, } from "@/lib/console/useOwnerWallet"; +import { + SESSION_USAGE_OPTIONS, + useAccountUsage, +} from "@/lib/console/useAccountUsage"; import { includedUsageRemainingLabel, - includedUsageSummary, + includedUsageSummaryFromBalance, formatWalletUsd, overageLimitNote, } from "@/lib/console/wallet-settlement-display"; @@ -195,10 +199,7 @@ export default function BillingSection() { removePaymentMethod, } = useBillingAccount(isConnected); const wallet = useWalletBillingState(isConnected); - const included = - wallet.state.status === "ready" - ? includedUsageSummary(wallet.state.wallet.billingState) - : null; + const usage = useAccountUsage(isConnected, SESSION_USAGE_OPTIONS); const [busyPlanId, setBusyPlanId] = useState(null); const [pmBusy, setPmBusy] = useState(false); @@ -658,6 +659,16 @@ export default function BillingSection() { const cancelingEndsAt = resolveCancelingEffectiveAt(subscription); const cancelingEndsLabel = formatPendingCancelDate(cancelingEndsAt); + const included = + usage.status === "ready" + ? includedUsageSummaryFromBalance( + usage.data.balance, + subscription + ? { id: subscription.planId, name: subscription.planName } + : undefined + ) + : null; + const planSub = subscriptionUiState.kind === "canceling" ? `${cancelingPlanName} ends ${cancelingEndsLabel}` diff --git a/lib/console/useAccountUsage.ts b/lib/console/useAccountUsage.ts index bdcb891..44faaa3 100644 --- a/lib/console/useAccountUsage.ts +++ b/lib/console/useAccountUsage.ts @@ -43,6 +43,11 @@ type UseAccountUsageOptions = { includePrior?: boolean; }; +/** Session meters only need the live allowance, not the prior usage window. */ +export const SESSION_USAGE_OPTIONS: UseAccountUsageOptions = { + includePrior: false, +}; + function normalizeOptions( periodDaysOrOptions: number | UseAccountUsageOptions = 30 ): Required< diff --git a/lib/console/wallet-settlement-display.test.ts b/lib/console/wallet-settlement-display.test.ts index 7e588b0..d53b72f 100644 --- a/lib/console/wallet-settlement-display.test.ts +++ b/lib/console/wallet-settlement-display.test.ts @@ -8,6 +8,7 @@ import { collectionSchedule, includedUsageRemainingLabel, includedUsageSummary, + includedUsageSummaryFromBalance, overageLimitNote, spendPostureBadge, } from "./wallet-settlement-display"; @@ -257,6 +258,42 @@ describe("includedUsageSummary", () => { }); }); +describe("includedUsageSummaryFromBalance", () => { + it("returns null when the user has no granted allowance", () => { + const summary = includedUsageSummaryFromBalance({ + externalUserId: "eu_1", + balanceUsdMicros: "0", + consumedUsdMicros: "0", + lifetimeGrantedUsdMicros: "0", + hasAccess: false, + }); + assert.equal(summary, null); + }); + + it("maps remaining and granted from the user usage balance", () => { + const summary = includedUsageSummaryFromBalance( + { + externalUserId: "eu_1", + balanceUsdMicros: "4982000", + consumedUsdMicros: "18000", + lifetimeGrantedUsdMicros: "5000000", + hasAccess: true, + }, + { id: "plan_1", name: "Starter" }, + ); + assert.ok(summary); + assert.equal(summary.remainingUsd, "4.98"); + assert.equal(summary.totalUsd, "5.00"); + assert.equal(summary.consumedUsd, "0.02"); + assert.equal(summary.planName, "Starter"); + assert.equal(summary.planId, "plan_1"); + assert.equal( + includedUsageRemainingLabel(summary), + "Starter ยท $4.98 of $5.00 included left", + ); + }); +}); + describe("overageLimitNote", () => { it("shows ceiling and remaining headroom while spendable", () => { const note = overageLimitNote(makeState({})); diff --git a/lib/console/wallet-settlement-display.ts b/lib/console/wallet-settlement-display.ts index db95d08..cfeef9d 100644 --- a/lib/console/wallet-settlement-display.ts +++ b/lib/console/wallet-settlement-display.ts @@ -1,4 +1,5 @@ import type { BillingState, BillingStatus } from "@pymthouse/builder-sdk"; +import type { AccountUsageBalance } from "./account-usage"; import { microsToUsd } from "./usage-capability-display"; type IncludedUsageFunding = { @@ -191,6 +192,37 @@ export function includedUsageSummary( }; } +/** + * Session-user included allowance from `me/usage/balance`. + * Null when the user has no granted cycle (prepaid / invoice only). + */ +export function includedUsageSummaryFromBalance( + balance: AccountUsageBalance | null | undefined, + sourcePlan?: { id?: string | null; name?: string | null }, +): IncludedUsageSummary | null { + if (!balance) return null; + const remainingUsdMicros = balance.balanceUsdMicros; + const grantedUsdMicros = balance.lifetimeGrantedUsdMicros; + const consumedUsdMicros = balance.consumedUsdMicros; + const totalUsdMicros = + parseUsdMicros(grantedUsdMicros) > BigInt(0) + ? grantedUsdMicros + : remainingUsdMicros; + if (parseUsdMicros(totalUsdMicros) <= BigInt(0)) return null; + + return { + remainingUsdMicros, + totalUsdMicros, + consumedUsdMicros, + remainingUsd: formatWalletUsd(remainingUsdMicros), + totalUsd: formatWalletUsd(totalUsdMicros), + consumedUsd: formatWalletUsd(consumedUsdMicros), + planId: sourcePlan?.id?.trim() || null, + planName: sourcePlan?.name?.trim() || null, + resetsAt: null, + }; +} + export function includedUsageRemainingLabel( summary: IncludedUsageSummary, ): string {