fix(#3325): render Internet Banking amounts in the configured currency - #3342
Conversation
Four messages wrote `NZ$` into their text instead of using the club's configured currency: the returned-credit sentence on a cancellation, the same sentence when an Internet Banking hold expires, the orphaned-credit backfill's audit-log detail, and every amount in the hold-clearing audit report. All four now render through `formatCents`, the one home (#3302), and the report's local `formatIbAuditCents` helper is deleted with them. Under the default configuration (en-NZ, NZD) the text moves from `NZ$20.00` to `$20.00`, as the owner accepted; amounts stay integer cents. Docblocks in `utils.ts` no longer describe `NZ$` as pending #3325. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `CENTS_DISPLAY_EXEMPTIONS` block naming #3325 was the worklist for this change, and `cents-display-guard.test.ts` requires the hand-rolled pattern to still exist in every exempted file, so the block goes with its cause. `internet-banking-payment-cron.ts` also leaves `CENTS_DISPLAY_MONEY_DOMAIN_OVERLAP` — it is no longer exempt at all — and the "four" overlaps become three in the comments and the guard test title. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ering Exact strings, not regexes: `$20.00 of applied account credit was returned.` in the cancel and hold-expiry suites; `$0.00`, `$1,234.56` (now grouped, as `formatCents` renders everywhere) and `-$5.00` in the hold-clearing report suite, which also pins that `NZ$` and the ungrouped `$1234.56` do not come back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`booking-cancel.ts` returns to its base length (a comment folded), and two allowances that earlier lanes declared for files this change SHRANK are deleted — `ib-hold-clearing-audit.ts` (813 declared, 795 now) from #3326's file and `public-page-content-tokens.ts` (771 declared, 765 now) from #3338's — because the ratchet refuses an allowance a change no longer needs. CI runs the ratchet against `origin/main` on every pull_request, where this is clean. Against `origin/epic/2680-mep` the same edit is reported as 23 findings, structurally: an edited allowance file already merged into that base makes every other entry in it "an allowance this change did not need". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…says pending Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…two homes
The owner's decision is "the configured currency everywhere", and these are
the same defect the toFixed arm cannot see: `finance-fees-sections.tsx` and
`joining-fee-preview.tsx` each built `Intl.NumberFormat("en-NZ", { currency:
"NZD" })` (both now call `formatCents`; the joining helper is deleted),
`public-page-content-tokens.ts` hard-coded the locale while taking the
configured currency (now `formatCents`), and the finance chart theme held
three literal-locale number formatters (now `formatFinanceNumber`,
`formatFinancePercent` and a new `formatFinanceRatio`, all from
`finance-format.ts`, the dashboard's one home).
Renderings are pinned byte-identical under the default configuration:
`$1,234.56` in the fee page and a new hint test; `1,234,567`, `12.5%`,
`1.35`, `$1,235` in the theme test. The tokens suite mocked an `APP_LOCALE`
the code never read; a fresh-import de-DE/EUR case (`1.234,56 €`) now makes
that mock discriminate, and fails on the literal-locale mutant.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…NFIG-001)
`new Intl.NumberFormat(<string literal>, { style: "currency" })` fails the
build anywhere in src/. Its own group on the mandatory set rather than an
addition to the toFixed arm, so the editable-input exemptions do not lift
it — two of the four sites it would have caught sat on that list. The two
homes build from `APP_LOCALE`, an Identifier, so they need no exemption;
whole-tree lint reports zero sites after the fix commit. Guard test covers
fire / APP_LOCALE negative / non-currency negative / not lifted at an
exempted file; mutation-verified with a scratch file under src/.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`formatCentsPlain` no longer enumerates its callers (the list already omitted one); the #3325 narrative lives in the `formatCents` docblock and the other sites carry a bare citation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`public-page-content-tokens.ts` now imports `@/lib/utils`, whose formatter reads `APP_LOCALE` at module load, so two suites whose `@/config/operational` mock returned only `APP_CURRENCY` died at import — the #2836 class that `test:related` exists to catch, and did. `eslint.config.d.mts` declares the new `CURRENCY_LOCALE_GUARD_ARM` export, as `typecheck-project-coverage` requires. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…onfiguration `formatFinanceAxisTick` spelt `$` by hand for `$10k` / `$1.2m` / `$450`. The number keeps that exact shape — `Intl`'s `notation: "compact"` renders `$10K` / `$1.2M` and was measured and rejected — while the symbol and its position now come from the configured display formatter's parts, in `finance-format.ts` (`formatCompactDollarsDisplay`), the dashboard's one home. The six default outputs were measured BEFORE the change and pinned; a de-DE/EUR case (`10k €`) proves the symbol is no longer a literal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four input labels hard-coded the code: the fees page's Amount, Annual amount
and component Amount, and the booking-requests panel's Total. Each now
renders `(${APP_CURRENCY})`, byte-identical under the default; the fee page
suite keeps its literal "(NZD)" pins as that proof and adds a fresh-import
AUD case. The AI spend-cap "(NZD)" labels are deliberately untouched — that
cap IS denominated in New Zealand dollars (owner, 9 Sep 2026; new issue).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rency-code label Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… and is mirrored Review measured the arm silent on a literal currency code with a configured locale and on a `new`-less `Intl.NumberFormat(...)`. Both arms now match with or without `new`; a second selector fires on `currency: "<literal>"` whatever the locale argument, with its own INV-CONFIG-001 message. Whole-tree lint stays at zero sites. The guard test gains positive and negative cases per arm, states the shapes deliberately not covered (template/array locale, aliased constructor, `Intl["NumberFormat"]`, `toLocaleString`), and mirrors `CURRENCY_LOCALE_GUARD_ARM` structurally — every selector must resolve at an ordinary file and at every toFixed-exempted file — so the export is read. The docblock's "four files" is corrected to the three at the merge base, and `formatCompactDollarsDisplay` records that its k/m number is not localised. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…25-configured-currency
|
Merge ready — head What was built. Owner decisions on #3325 (8–9 Sep): configured currency everywhere; the canonical formatter's plain Reviews and what they found. Correctness/regression lens and the standing single-source-of-truth lens at Stated limits (not residuals). The compact axis tick's k/m number is not localised (a |
Linked Issue
epic/2680-mepbecause SSOT: one formatCents — the copies #3264 found beneath formatSignedCents #3302 (merged there) renamed the local helper, pinned the current strings and registered the lint exemptions this PR removes. Follow-up filed by owner decision: CONFIG: AI spend caps are priced in New Zealand dollars at a fixed conversion, whatever currency the club is configured for #3354 (AI spend caps are priced in NZD at a fixed conversion — a feature, not drift).Summary
NZ$; accept that the canonical formatter renders New Zealand dollars as a plain$(what every other amount already shows) rather than change the formatter product-wide; fix every other hard-coded locale or currency the reviews found in this PR; leave the AI spend-cap copy for CONFIG: AI spend caps are priced in New Zealand dollars at a fixed conversion, whatever currency the club is configured for #3354, because that figure genuinely is NZD-denominated.formatCents.NZ$20.00becomes$20.00; the audit report gains thousands grouping ($1,234.56) and renders negatives-$5.00. The localformatIbAuditCentshelper is deleted (23 call sites).src/outside the AI caps: the admin fees page and the joining-fee preview each defined their ownIntl.NumberFormat("en-NZ", { currency: "NZD" })(nowformatCents); the public page content tokens hard-coded"en-NZ"(nowformatCents); the finance chart theme held threeen-NZnumber/percent/ratio formatters (now fromfinance-format.ts) and built compact axis ticks with a literal$(nowformatCompactDollarsDisplayinfinance-format.ts, symbol from the configured currency, output byte-identical for the default:$10k,$1.2m,$-1.2m); four input labels read(NZD)(now(${APP_CURRENCY})).no-restricted-syntaxarms ineslint.config.mjs, in their own always-on group: a literal locale passed to a currencyIntl.NumberFormat(with or withoutnew), and a literalcurrencycode anywhere outside the two formatter homes (utils.ts,finance-format.ts). Messages nameINV-CONFIG-001/ CONFIG: Internet Banking messages hard-code NZ$ instead of the configured currency #3325. Whole-tree lint with both arms: 0 errors. The CONFIG: Internet Banking messages hard-code NZ$ instead of the configured currency #3325 exemption block is deleted and the cron leaves the money-domain overlap list.Risk Level
Changed Areas
Member-facing text in two messages, officer-facing text in an audit report and on admin screens; no amount changes.
Tests Added Or Updated
booking-cancel.test.ts,internet-banking-payment-cron.test.ts,ib-hold-clearing-audit.test.ts(also assertsnot.toContain("NZ$")and the grouped$1,234.56).de-DE/EURorAUDconfiguration) inpublic-page-content-tokens.test.ts,finance-format.test.ts,finance-chart-theme.test.ts,fee-configuration-page.test.tsx; newjoining-fee-preview-hint.test.tsx.cents-display-guard.test.ts: eleven cases for the two new lint arms (positive with and withoutnew, negatives for a variable currency and a non-currency locale, once-per-arm when both are literal, still fires at atoFixed-exempted file) plus a structural mirror asserting every arm selector is present in the resolved rule; the comment lists the shapes deliberately not covered (template or array locale, aliased constructor,toLocaleString).hut-leader-cms-token.test.tsandtoken-catalogue.test.tswhere the module graph widened (found bytest:related).Validation Commands Run
Commands Not Run And Why
npm testand build: PR CI owns them. A live-Postgres run of the cron and backfill paths was not done; the change is string rendering through the same function every other screen uses.quality:budget --base origin/epic/2680-mepreports 23 findings, all "declares an allowance the check did not need": editing an already-merged allowance file makes every entry that did not grow against that base a finding, so the two bases cannot both be clean once such a file is edited. CI'spull_requestbase isorigin/main, which is clean.Screenshots Or UI Evidence
Security And Privacy Impact
Data Integrity Impact
Concurrency And Lock Impact
cron, or concurrency-sensitive writer changed.
booking-cancel.tsandinternet-banking-payment-cron.tsare sensitive paths, but the diff in each is one template string: the reason text a writer records, rendered throughformatCents. No lock, transaction boundary, guarded transition or side effect changed.formatCentsone-home this follows, merged into the epic), fix(#3220): one chokepoint for a dead payment recovery, and it withdraws the ask it left standing #3308 and feat(#3213): groundwork for a withheld-share row on the money-to-settle-by-hand card #3309 (recent writers on the cron and settlement paths; neither touched these strings), chore(#3351): nodemailer 9.1.1 — closes four advisories and un-reds the Dependency audit gate #3352 (nodemailer bump, merged into the epic and into this branch so the audit gate reads the fixed lockfile).None, or justify the bounded exception fromdocs/CONCURRENCY_AND_LOCKING.md): NoneSingle Source Of Truth
formatFinanceRatioandformatCompactDollarsDisplayadded tosrc/lib/finance-format.ts(the whole-dollar and chart formatter home); five second definitions removed (formatIbAuditCents,formatJoiningFeeDollars, the fees-pagedollarsformatter, the tokens'en-NZmoney formatter, the chart theme's threeen-NZinstances) and routed toformatCentsorfinance-format.ts.Payment Or Accounting Impact
NZ$or$.Migration Or Deployment Impact
Docs Updated
src/lib/utils.ts(rule stated once),finance-format.ts(compact-notation measurement recorded; localisation limit stated),ib-hold-clearing-audit.ts,orphaned-applied-credit-backfill.ts,booking-cancel.ts;changelog.d/3326-one-format-cents.mdtrimmed so it no longer describes theNZ$prefix as pending. No guide quoted the old strings.Changelog Entry
changelog.d/<pr-number>-<slug>.md):changelog.d/3342-configured-currency.md.Residual Risks
de-DEclub reads1.2m €beside1.234.567 €tooltips), deliberate and pinned; thein thede-DEpins depends on the runner's ICU (Node 24 locally matched CI's); the AI spend-cap copy and its two(NZD)labels are CONFIG: AI spend caps are priced in New Zealand dollars at a fixed conversion, whatever currency the club is configured for #3354's by owner decision.Manual Checks Required
Safety Confirmation
backups, live Stripe, live Xero, live SES, live Sentry, or live provider
webhooks for exploratory validation.
AGENTS.md"Completion and Merge" risk gate:eligible Low/Medium-risk PRs may merge (and close their linked issue) once
CI is green; Critical or High-risk changes — security, payments, booking,
membership, Xero/Stripe/SES/Sentry, schema/migrations, deployment, or data
integrity — wait for explicit owner approval. Merge commits only.