Skip to content

fix(#3325): render Internet Banking amounts in the configured currency - #3342

Merged
thatskiff33 merged 15 commits into
epic/2680-mepfrom
fix/issue-3325-configured-currency
Sep 12, 2026
Merged

thatskiff33 merged 15 commits into
epic/2680-mepfrom
fix/issue-3325-configured-currency

Conversation

@thatskiff33-agents

@thatskiff33-agents thatskiff33-agents commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Linked Issue

Summary

  • Owner decisions on CONFIG: Internet Banking messages hard-code NZ$ instead of the configured currency #3325 (8–9 September 2026, all in session, recorded on the issue): use the configured currency everywhere and drop the hard-coded 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.
  • The four Internet Banking money strings (booking cancel note, IB payment cron reason, orphaned-credit backfill audit detail, IB hold-clearing audit report) render through formatCents. NZ$20.00 becomes $20.00; the audit report gains thousands grouping ($1,234.56) and renders negatives -$5.00. The local formatIbAuditCents helper is deleted (23 call sites).
  • Every other hard-coded locale or currency in src/ outside the AI caps: the admin fees page and the joining-fee preview each defined their own Intl.NumberFormat("en-NZ", { currency: "NZD" }) (now formatCents); the public page content tokens hard-coded "en-NZ" (now formatCents); the finance chart theme held three en-NZ number/percent/ratio formatters (now from finance-format.ts) and built compact axis ticks with a literal $ (now formatCompactDollarsDisplay in finance-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})).
  • The lint guard now covers the shape that let this drift in. Two new no-restricted-syntax arms in eslint.config.mjs, in their own always-on group: a literal locale passed to a currency Intl.NumberFormat (with or without new), and a literal currency code anywhere outside the two formatter homes (utils.ts, finance-format.ts). Messages name INV-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.
  • Money stays integer cents throughout; only rendering changed. No reader or writer of amounts changed.

Risk Level

  • Critical
  • High
  • Medium
  • Low
  • Informational/docs only

Changed Areas

  • Admin/finance/lodge UI
  • Public UI/UX/accessibility
  • Docs/agent workflow only
  • Booking/capacity
  • Payment/refund/credits
  • Membership/family lifecycle
  • Xero/Stripe/SES/Sentry integration
  • Auth/security/privacy
  • Database schema/migrations
  • Deployment/operations

Member-facing text in two messages, officer-facing text in an audit report and on admin screens; no amount changes.

Tests Added Or Updated

  • Pinned suites moved to the exact new strings: booking-cancel.test.ts, internet-banking-payment-cron.test.ts, ib-hold-clearing-audit.test.ts (also asserts not.toContain("NZ$") and the grouped $1,234.56).
  • Byte-identical pins for the default configuration plus a discriminating second-locale case (fresh import with a mocked de-DE/EUR or AUD configuration) in public-page-content-tokens.test.ts, finance-format.test.ts, finance-chart-theme.test.ts, fee-configuration-page.test.tsx; new joining-fee-preview-hint.test.tsx.
  • cents-display-guard.test.ts: eleven cases for the two new lint arms (positive with and without new, negatives for a variable currency and a non-currency locale, once-per-arm when both are literal, still fires at a toFixed-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).
  • Mocks completed in hut-leader-cms-token.test.ts and token-catalogue.test.ts where the module graph widened (found by test:related).

Validation Commands Run

npm run db:generate && npm run lint && npm run typecheck && npm run knip        # all exit 0; lint 0 errors with both new arms
npm run quality:budget -- --base origin/main                                   # exit 0 (CI's base); two stale allowance entries removed, one +1-line allowance added
npm run docs:linkcheck && npm run docs:indexcheck                              # exit 0
npm run test:named -- <15 touched suites>                                      # all pass
npm run test:related -- $(git diff --name-only origin/epic/2680-mep...HEAD)    # 1,119 files (utils.ts is under nearly everything): 1,117 passed, 2 skipped, 0 failed
# mutations: hand-rolled toFixed at the report -> ib-hold-clearing-audit pin fails; literal en-NZ in money() -> de-DE case fails; literal $ in the compact helper -> de-DE case fails; scratch file with a literal currency code -> lint exit 1 with the INV-CONFIG-001 message; all restored, tree clean
npm run pr:check -- <this body> --base origin/epic/2680-mep

Commands Not Run And Why

  • Full npm test and 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-mep reports 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's pull_request base is origin/main, which is clean.

Screenshots Or UI Evidence

  • N/A — text only; every before/after string is pinned in the tests named above and the default rendering is byte-identical where the symbol did not change.

Security And Privacy Impact

  • None.

Data Integrity Impact

  • None. Integer cents throughout; the change is the rendering of a number into text.

Concurrency And Lock Impact

Single Source Of Truth

  • New or moved definitions, and the one module each now lives in: formatFinanceRatio and formatCompactDollarsDisplay added to src/lib/finance-format.ts (the whole-dollar and chart formatter home); five second definitions removed (formatIbAuditCents, formatJoiningFeeDollars, the fees-page dollars formatter, the tokens' en-NZ money formatter, the chart theme's three en-NZ instances) and routed to formatCents or finance-format.ts.
  • If anything was copied rather than imported, or a guard was added instead of making the wrong thing unrepresentable — which structural option was rejected and why: nothing copied. Two lint arms were added because a literal locale or currency string cannot be made unrepresentable in TypeScript; the arms are in their own group so no existing exemption lifts them, and the test states their known gaps.

Payment Or Accounting Impact

  • None to amounts. Member notes, an officer audit report and admin screens show the configured currency's rendering instead of a hard-coded NZ$ or $.

Migration Or Deployment Impact

  • None.

Docs Updated

  • Docblocks in 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.md trimmed so it no longer describes the NZ$ prefix as pending. No guide quoted the old strings.

Changelog Entry

  • Fragment added (changelog.d/<pr-number>-<slug>.md): changelog.d/3342-configured-currency.md.

Residual Risks

Manual Checks Required

  • None.

Safety Confirmation

  • I did not use production credentials, production databases, production
    backups, live Stripe, live Xero, live SES, live Sentry, or live provider
    webhooks for exploratory validation.
  • Merge handling follows the 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.

thatskiff33-agents and others added 4 commits September 8, 2026 22:58
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>
thatskiff33-agents and others added 6 commits September 8, 2026 23:45
`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>
thatskiff33-agents and others added 3 commits September 9, 2026 23:13
…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>
thatskiff33-agents and others added 2 commits September 10, 2026 03:57
… 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>
@thatskiff33-agents
thatskiff33-agents marked this pull request as ready for review September 9, 2026 16:23
@thatskiff33-agents

Copy link
Copy Markdown
Collaborator Author

Merge ready — head 6b77aa96c, every check green (verify, migration drift, data-migration verification, static analysis, gitleaks, Trivy, dependency audit, both E2E suites, CodeQL, Semgrep). Risk Low, targeting epic/2680-mep as a child; the epic orchestrator sequences the merge (LANE-SYNC posted on #2680).

What was built. Owner decisions on #3325 (8–9 Sep): configured currency everywhere; the canonical formatter's plain $ accepted for New Zealand dollars; every other hard-coded locale or currency found by review fixed here; the AI spend caps left for #3354 because that figure genuinely is NZD-denominated. The four Internet Banking money strings render through formatCents (NZ$20.00$20.00; the audit report gains grouping and -$5.00). Five second definitions of a currency formatter were removed and routed to formatCents or finance-format.ts (admin fees page, joining-fee preview, public content tokens, and the finance chart theme's three formatters plus its compact axis ticks, byte-identical for the default). Four (NZD) input labels read the configured currency code. Two new lint arms catch a literal locale or a literal currency code in a currency formatter, with and without new, in their own always-on group; the #3325 exemption block is gone.

Reviews and what they found. Correctness/regression lens and the standing single-source-of-truth lens at 1d614bee6; a scoped lens over the fix round's new code at f0e62fdee. Findings and fixes: the file-size ratchet would have failed CI (a comment grew the cancel module by one line; a stale allowance for the shrunk audit module) — folded and removed; the #3302 changelog fragment still described the prefix as pending — trimmed; the fragment was misnamed — renamed to the PR number; three more hard-coded en-NZ/NZD formatter homes — retired (owner: fix in this PR); the formatCentsPlain docblock enumerated callers incompletely — now states purpose and points at the lint rule; the decision narrative was repeated four times — stated once; the lint arm's message promised currency coverage it lacked and its export was unread — second arm added, both mirrored structurally in the guard test with the uncovered shapes listed; a docblock count was off by one — corrected. test:related (1,117 files) found two mock factories the widened module graph broke — completed.

Stated limits (not residuals). The compact axis tick's k/m number is not localised (a de-DE club reads 1.2m € beside 1.234.567 € tooltips), deliberate and pinned; the de-DE pins depend on the runner's ICU, which matched CI's Node 24 locally; the AI cap copy and its two (NZD) labels are #3354's by owner decision.

@thatskiff33
thatskiff33 merged commit 366b30e into epic/2680-mep Sep 12, 2026
16 checks passed
@thatskiff33
thatskiff33 deleted the fix/issue-3325-configured-currency branch September 12, 2026 06:20
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