Skip to content

fix(dashboard): render money in the property's currency, not hardcoded USD - #301

Merged
telivity-otaip merged 5 commits into
TelivityAI:mainfrom
modernitconsultants:fix/currency-display
Aug 12, 2026
Merged

fix(dashboard): render money in the property's currency, not hardcoded USD#301
telivity-otaip merged 5 commits into
TelivityAI:mainfrom
modernitconsultants:fix/currency-display

Conversation

@modernitconsultants

Copy link
Copy Markdown
Contributor

Running a property with currencyCode: 'JPY', every money value in the dashboard renders as US dollars — a ¥511,275 reservation shows as $511275.00.

Two defects, the second worse than the first

Display (18 sites). `$${Number(amount).toFixed(2)}` — a hardcoded symbol and hardcoded two decimal places, ignoring the currencyCode that is already on every record. For yen the decimals are wrong too: JPY has no minor unit, so the correct render is ¥511,275.

Writes (7 sites). currencyCode: 'USD' is hardcoded when creating rate plans, reservations, house accounts and ledger entries. On a non-USD property that writes incorrect data silently — a rate plan created through the UI on a yen property is stored as USD.

The root of both: PropertySummary doesn't carry currencyCode, so the dashboard has no way to know the property's currency even though the API returns it.

The change

  • lib/money.tsformatMoney / formatMoneyPlain via Intl.NumberFormat, so symbol, separators and fraction digits all derive from the ISO 4217 code. Unknown codes degrade to 1234.00 XYZ rather than a wrong symbol.
  • currencyCode added to PropertySummary and exposed from PropertyContext. Portfolio mode falls back to the default deliberately — properties spanning currencies have no single answer, and asserting one property's code over others would be wrong.
  • All 18 render sites and all 7 create sites routed through it.
  • NightAudit's revenue helper is module-level so it takes the currency as a parameter rather than reaching for a hook.

Notes

Removed a local formatMoney shadow in Channels.tsx that duplicated the new shared helper.

You ship German, Portuguese, Spanish, French, Croatian, Italian and Serbian dashboard locales — this is the money half of that same internationalisation, and it currently blocks any non-USD operator.

Found running HAIP self-hosted for a property trading in yen.

@telivity-otaip telivity-otaip left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Direction is right — thank you. Hardcoded $ + toFixed(2) and create paths forcing USD are real bugs for non-USD properties; formatMoney + PropertyContext.currencyCode is the right approach.

Requesting changes: description says all render sites are covered, but Folios, Cashier, FrontDesk, Reports table bodies, Dashboard portfolio table rows, and HouseAccounts balances still use $\u2026toFixed(2). Please either finish those in this PR or narrow the description / split a follow-up — happy either way, and happy to help if you want a second pair of hands.

@telivity-otaip

Copy link
Copy Markdown
Collaborator

Clarifying the request-changes note: Folios, Cashier, FrontDesk, Reports table bodies, Dashboard portfolio table rows, and HouseAccounts balances still use $…toFixed(2) after this diff. Please either finish those call sites in this PR or narrow the description / split a follow-up — happy either way, and happy to help if you want a second pair of hands.

@modernitconsultants

Copy link
Copy Markdown
Contributor Author

You're right, and the real number is worse than your list — thanks for catching it.

I grepped a single pattern ($${) and reported 18 sites. Enumerating properly across every form (${Number(...)}, bare toFixed(2), component files as well as pages) gives 63 renders across 15 files:

GuestDetailsModal, RatePlanCalendar, Accounting, Cashier, Channels,
Commercial, Dashboard, Folios, FrontDesk, Groups, HouseAccounts,
NightAudit, RatePlans, Reports, Reservations

So my description overstated the diff — that's on me, and it's the same class of mistake as the bug itself: trusting one pattern to represent all occurrences.

Plan, unless you'd prefer otherwise: finish all 63 in this PR rather than split, since a half-converted dashboard is arguably worse than none — a user seeing ¥ on one screen and $ on the next reasonably concludes one of them is lying about the amount.

The mechanical part is easy; the fiddly part is that each render needs the property's currencyCode in scope in the owning component, and a few of these are module-level helpers that can't call a hook (NightAudit's revenue formatter was one — it takes the currency as a parameter now). I'll work through them and push to this branch.

Taking you up on the offer if it stalls — but no need yet.

…y by hand

Completes the review: 63 renders across 15 files, not the 18 my first pass
claimed — I had grepped a single pattern, which is the same mistake as the
bug itself.

The design changed to make that safe. Rather than thread currencyCode into
every component that happens to show an amount — including module-level
helpers that cannot call a hook — lib/money keeps the active property's
currency in a module value that PropertyContext pushes on change. That is
the pattern lib/api.ts already uses for propertyId, so it follows the
codebase rather than inventing a mechanism.

Call sites are now formatMoney(amount), with an explicit second argument only
where a record carries its own currency. Nothing needs a hook in scope, which
is what made the earlier partial attempts fragile.
@modernitconsultants

Copy link
Copy Markdown
Contributor Author

Done — all 63 across the 15 files, zero toFixed(2)/$ renders left in the dashboard.

One design change worth flagging, because it's what made finishing it safe: rather than thread currencyCode into every component that shows an amount — several are module-level helpers that can't call a hook — lib/money now keeps the active property's currency in a module value that PropertyContext pushes on change. That's the pattern lib/api.ts already uses for propertyId, so it follows your codebase rather than inventing a mechanism.

Call sites are plain formatMoney(amount), with an explicit currency only where a record carries its own (folios, house-account products).

Portfolio mode still falls back to the default deliberately — properties spanning currencies have no single right answer, and asserting one property's code over the others would be wrong.

@modernitconsultants

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: when I posted it the push had actually been rejected (non-fast-forward) and I didn't check before saying it was done. Apologies — it's pushed now, verified as f556ac4 on the branch.

The rejection was because you'd merged main into the branch. I merged that in rather than force-pushing, so your merge is intact and the branch now carries both.

Worth noting your postgres-options.ts refactor came through in that merge — it generalises the pooler options from my earlier PR nicely, and my changes sit alongside it cleanly.

Maintainer polish on the currency-display work: four leftover bare-$
renders in the cashier session report summary.

Co-authored-by: telivity-otaip <telivity-otaip@users.noreply.github.com>

@telivity-otaip telivity-otaip left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for finishing the full pass, Charles — and for the honest correction on the push. The activeCurrency module value (same pattern as setPropertyId in lib/api.ts) is the right call; it made the remaining call sites safe without threading hooks everywhere.

Merging with full credit to you. Small maintainer polish on top of your PR: four leftover bare-$ amounts in the Cashier session-report summary now go through formatMoney as well.

@telivity-otaip
telivity-otaip merged commit a11408d into TelivityAI:main Aug 12, 2026
5 checks passed
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.

3 participants