Summary
Replaces the placeholder Treasury & Custody admin page with a Funds overview ("Nguồn tiền") section on /dashboard. Ahad's three funds are shown side by side, each with the provider balance next to our book balance, the period flows and the latest reconciliation verdict.
| Fund | Provider balance | Book | Period |
|---|---|---|---|
| Crypto | Fireblocks central vault, per token | crypto ledger book per token | deposits in / withdrawals out |
| Fiat (USD) | Airwallex USD balance | USD accounts (balance + locked) | user wallets, Convert USDC→USD / USD→USDC |
| Card | KeySecure reserve | card balances + reserve + fee revenue − pending (CARD check internal side) | spent, user fees, provider cost, margin |
Backend
- New
GET /admin/dashboard/funds?range=7d|30d|month(default30d),treasury:read, tagAdmin - Dashboard. -
FundsProviderReader: provider reads are cached in Redis for 60 s. A failed or malformed read is not cached and returnsnullfor that fund only. -
ReconciliationServiceexposes its book readers (cryptoBookTotals,fiatBookTotal,latestRuns), so the dashboard and the reconciliation check share one definition. - Period sums run in SQL
numeric, and money stays a decimal string end to end. Card-era convert orders are excluded. - The response carries a
clients[]array that holds only AHAD for now. MUSU can be added later as another entry.
Admin
-
FundsSectionsits between the stat cards and the work queues. It has a client Select (Ahad; MUSU disabled, "coming soon"), a 7d/30d/month range switch and three fund cards with a match/drift/unreadable state. - Removed the
/treasurynav item and its locale keys. - The service calls
clientdirectly because the staging swagger used by orval does not include the endpoint yet. Regenerate after deploy.
Out of scope
MUSU data and its ingest API, a clients table, CSV export, charts, USD totals across tokens, and the lending vault. The existing /admin/treasury endpoints are unchanged.
Test plan
-
BE unit: 131 passed -
BE DB specs on moanapay_test: 166 passed -
yarn buildclean -
Admin: 604 tests passed, type-check clean, lint 0 errors -
Local smoke test: 7d/month return 200, invalid range returns 400, no auth returns 401; UI checked at desktop width and 400px -
Opus review gate: PASS after 2 rounds (3 NITs left)
Known
-
ranAtis a naive timestamp, a pre-existing pattern in the codebase. It shows 7 h early on a +07 dev machine; staging and prod run UTC.