Skip to content

feat(045): admin UI for credit top-ups and the opening balance - #129

Merged
studert merged 2 commits into
mainfrom
045-credit-entry-ui
Oct 5, 2026
Merged

studert merged 2 commits into
mainfrom
045-credit-entry-ui

Conversation

@studert

@studert studert commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Why

Spec 045 added the server actions for prepaid Claude Console credits (recordCreditPurchase, deleteCreditPurchase, setCreditOpeningBalance), but nothing in the UI called them. A Console top-up could not be recorded, and with no opening balance on file the derived credit balance always read "unavailable".

What

Both are now on the Monthly Billing Budget card on /claude (the page is already admin-only):

  • Opening balance: the amount read in the Console and an "as of end of" date (not in the future), with Edit and Clear. The dialog explains contract C4: only top-ups and usage dated after that day move the balance.

  • Credit top-ups: record an amount, a date (defaults to today), an optional invoice and a note. The list asks for confirmation before deleting. Top-ups dated on or before the opening date are flagged as already included in it, so they don't silently fail to count.

  • getCreditPurchaseOverview (in src/actions/credits.ts) loads the recorded purchases and the vendor invoices from the last 180 days that aren't already linked. Invoices are offered for linking but never chosen automatically (D9), because seat invoices and top-ups both come from "Anthropic, PBC".

  • parseCreditAmountCents and parseOpeningBalanceCents turn typed dollars into integer cents. A top-up must be more than 0; an opening balance may be 0.

  • Link invoice on a top-up recorded before its invoice arrived (linkCreditPurchaseInvoice). This updates the existing row, so the cash is never counted twice.

  • Migration 0033: credit_purchases.invoice_id is now a unique index (credit_purchases_invoice_id_unique), so two admins can't link one invoice to two top-ups. setCreditOpeningBalance also rejects negative balances and future dates on the server.

No new packages.

Verification

  • pnpm typecheck passes, and ESLint is clean on the changed files.
  • tests/unit/credits.test.ts passes (13 tests, including the new parser cases).
  • Tested in a browser against the preview/045-usage-based-cost-model Neon branch, never production:
    • Set the opening balance to $44.79 as of 2026-10-04, then recorded a $600 top-up on 2026-10-05. The card showed $644.79 = $44.79 + $600.00 − $0.00 consumed.
    • Moved the opening date to 2026-10-05 and the same-day warning appeared on the top-up.
    • Recorded a top-up without an invoice, then linked one with Link invoice: the row showed the invoice, the balance stayed $644.79, and the button disappeared. Migration 0033 was applied to the preview branch first.
    • Deleted the top-up and cleared the opening balance; the card went back to "not recorded". No console errors.

After merge

  1. Apply migration 0033 to production (pnpm db:migrate; the build doesn't run migrations). credit_purchases is empty there, so the unique index can't fail.
  2. Enter the real figures on production: opening balance $44.79 as of end of 2026-10-04, then a top-up of $600.00 on 2026-10-05. Use Link invoice on the top-up once its invoice arrives.

🤖 Generated with Claude Code

Spec 045 shipped recordCreditPurchase, deleteCreditPurchase and
setCreditOpeningBalance, but no surface called them, so a Console top-up
could not be recorded and the derived credit balance stayed unavailable.

Adds both to the Monthly Billing Budget card on /claude:
- Opening balance: amount + "as of end of" date, edit and clear. The
  dialog spells out that only events dated after that day move the
  balance (C4).
- Credit top-ups: record amount, date, optional invoice link and note;
  list with confirm-before-delete. Rows on or before the opening date
  are flagged as already included in it.
- getCreditPurchaseOverview loads the purchases and the recent vendor
  invoices not yet recorded as a purchase, offered for linking, never
  picked automatically (D9).
- parseCreditAmountCents / parseOpeningBalanceCents turn typed dollars
  into integer cents; unit-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 08:44
@vercel

vercel Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ai-developer-hub Ready Ready Preview Oct 5, 2026 9:00am UTC

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Invoice relinking can duplicate balances, server validation is incomplete, and the controls remain inaccessible before the first usage sync.

Review effort: Balanced
Findings: 3 High severity · 1 Medium severity · 1 Low severity

Open (5)
What changed in this PR

Adds admin controls for managing Claude prepaid-credit opening balances and top-ups.

Changes:

  • Adds opening-balance and top-up dialogs.
  • Loads purchases and eligible invoices for linking.
  • Adds dollar-to-cents parsers and unit tests.
File Description
src/​actions/​credits.ts Loads purchases and linkable invoices.
src/​app/​claude/​page.tsx Fetches credit-purchase data.
src/​components/​claude/​credit-opening-balance.tsx Adds opening-balance controls.
src/​components/​claude/​credit-purchases.tsx Adds top-up recording and deletion UI.
src/​components/​claude/​org-credits-panel.tsx Integrates the new controls.
src/​lib/​credits.ts Adds amount parsers.
tests/​unit/​credits.test.ts Tests amount parsing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/actions/credits.ts Resolved
Comment thread src/app/claude/page.tsx
Comment on lines +86 to +88
const creditPurchasesResult = creditBalance
? await getCreditPurchaseOverview(creditBalance.toolId)
: null;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changed in this PR. The /claude empty state shows before any Anthropic cost data has been synced, and the whole dashboard, including the existing billing-budget card, was already hidden in that case. Production has synced data. Showing the billing section alongside the empty state would be a separate change to the page; leaving that to the reviewer.

🤖 Addressed by Claude Code

… checks

Addresses review on #129:
- Unique index on credit_purchases.invoice_id (migration 0033) so two
  admins can't link the same invoice and double-count the balance; the
  violation maps to the same friendly error as the pre-check.
- linkCreditPurchaseInvoice attaches an invoice to an existing top-up.
  The old advice to re-record once the invoice arrived would have left
  two rows and doubled the cash. The list now offers "Link invoice" on
  unlinked rows.
- setCreditOpeningBalance rejects negative balances and future as-of
  dates on the server, not only in the form (one day of slack for the
  admin's timezone running ahead of UTC).
- The delete confirmation for a top-up on or before the opening date
  says the balance won't change, instead of claiming it will.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@studert
studert merged commit 71ae80f into main Oct 5, 2026
7 checks passed

This branch was successfully deployed

1 active deployment
Preview — 42673141 Deployed Oct 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants