From c26b009aad9a47895b2a42a10531f1e8db041ea2 Mon Sep 17 00:00:00 2001 From: Alex Nazaruk Date: Thu, 2 Jul 2026 23:58:28 +0200 Subject: [PATCH] fix(usage): compute credit balance from all-time usage, not a 30-day window /api/usage computed balanceUsd = totalCredits - totalUsage where totalCredits summed ALL confirmed/forwarded credit_deposits (no date filter) but totalUsage summed only the usage_events from the last 30 days (and capped at 1000 rows, since that query also feeds the history/breakdowns). So any spend older than 30 days effectively 'came back' and the remaining balance was overstated (e.g. top up $100, spend $90 in January, return in March -> balance shows ~$100 again). Subtract an all-time usage total for the balance via a DB-side aggregate, falling back to the recent-window sum if PostgREST aggregates are unavailable (so it can never overstate more than before). The 30-day query is kept for history/daily/module breakdowns. --- apps/web/src/app/api/usage/route.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/api/usage/route.ts b/apps/web/src/app/api/usage/route.ts index b529770..a643e20 100644 --- a/apps/web/src/app/api/usage/route.ts +++ b/apps/web/src/app/api/usage/route.ts @@ -100,7 +100,23 @@ export async function GET(request: NextRequest) { console.error("[usage] events query error:", eventsErr); } - const totalUsage = (events || []).reduce((sum, e) => sum + Number(e.cost_usd || 0), 0); + // The balance must subtract ALL-TIME usage, not just the last-30-days + // window fetched above (which is also capped at 1000 rows) for the + // history/breakdowns. Previously balance = all-time credits − 30-day usage, + // so any spend older than 30 days silently "came back" and overstated the + // remaining credit. Prefer a DB-side aggregate; if PostgREST aggregates are + // unavailable, fall back to the recent-window sum (prior behavior) so this + // can never overstate more than before. + let totalUsage = (events || []).reduce((sum, e) => sum + Number(e.cost_usd || 0), 0); + const { data: usageAgg, error: usageAggErr } = await admin + .from("usage_events") + .select("total:cost_usd.sum()") + .eq("user_id", user.id) + .single(); + const allTimeUsage = Number((usageAgg as { total?: number } | null)?.total); + if (!usageAggErr && Number.isFinite(allTimeUsage)) { + totalUsage = allTimeUsage; + } const balanceUsd = +(totalCredits - totalUsage).toFixed(2); // Transform events to the expected format