From cca7b85fec5542e69b05e9c80e5609ada1caa3de Mon Sep 17 00:00:00 2001 From: Charles Pizzato <311327716+modernitconsultants@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:41:26 +1000 Subject: [PATCH 1/2] fix(dashboard): #327 removed the default currency, twelve call sites still invent one #327 deleted DEFAULT_CURRENCY from money.ts so an absent currency renders plainly instead of as dollars. The formatter is now honest, but twelve call sites in the same app still substitute 'USD' when a record has no code, and seven of those substitutions are sent to the API and written to a ledger. The sharpest ones are guards that CANNOT FIRE: const currencyCode = folio?.currencyCode ?? 'USD'; // Folios.tsx:500 ... requireCurrency(currencyCode); // :514, :535, :554 requireCurrency can never throw there, because the value it checks was made non-null one line above. Three mutations read as guarded and are not, which is worse than no guard: a reviewer sees the call and stops looking. HouseAccounts.tsx:324 has the identical shape in front of its charge and payment mutations. WRITES THAT SENT AN INVENTED CURRENCY Folios.tsx:252 split folio create -- no guard of any kind FrontDesk.tsx:489 walk-in reservation FrontDesk.tsx:542 reservation split Accounting.tsx:285 A/R payment (the case the requireCurrency docblock names: a ledger in USD against a yen property) Reservations.tsx:96 CSV import, per row ReservationPartyPanel currencyCode PROP DEFAULTED to 'USD', so the caller's own ?? 'USD' was the second of two fallbacks HouseAccounts.tsx:68 house-account create was unguarded while createProduct twelve lines below was guarded While wiring the panel prop I found a third instance of the same bug: GuestDetailsModal already receives currencyCode: string | null and renders money with it, but never passed it to ReservationPartyPanel -- so a split started from the front-desk guest modal took the panel's 'USD' default. Now passed through. WHAT EACH CHANGE DOES Writes: drop the ?? 'USD' and guard with requireCurrency, so a missing code fails loudly at the call site instead of quietly becoming dollars. The two defanged derivations become ?? null. Both values are also handed to formatMoney, which #327 already taught to render a null currency plainly, so display stays correct and the guards can now actually fire. CSV import throws naming the row number rather than importing it as USD. A missing column is not a dollar booking, it is a row we cannot price. RatePlans.tsx:544 displayed 'USD' for a plan carrying no currency -- a wrong fact on screen. Shows an em dash, matching every other absent field beside it. NOT INCLUDED, deliberately: Settings.tsx:76/106, which seeds its form state to 'USD'. That is the screen whose job is to SET the property currency, so a default there is a different question -- though note :106 means loading a property with no currency and saving any other field on the form stamps it USD. Happy to follow up if you want that changed too. No behaviour change for any property that has a currency set, which is all of them created through the UI. The change is entirely in what happens when one is missing: it now stops instead of guessing. --- .../src/components/front-desk/GuestDetailsModal.tsx | 1 + .../reservations/ReservationPartyPanel.tsx | 7 ++++--- apps/dashboard/src/pages/Accounting.tsx | 3 ++- apps/dashboard/src/pages/Folios.tsx | 13 ++++++++----- apps/dashboard/src/pages/FrontDesk.tsx | 8 +++++--- apps/dashboard/src/pages/HouseAccounts.tsx | 3 ++- apps/dashboard/src/pages/RatePlans.tsx | 2 +- apps/dashboard/src/pages/Reservations.tsx | 12 +++++++++--- 8 files changed, 32 insertions(+), 17 deletions(-) diff --git a/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx b/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx index cee3433..4d08a90 100644 --- a/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx +++ b/apps/dashboard/src/components/front-desk/GuestDetailsModal.tsx @@ -225,6 +225,7 @@ export default function GuestDetailsModal({ roomTypeId={reservation.roomTypeId} ratePlanId={reservation.ratePlanId} totalAmount={reservation.totalAmount} + currencyCode={currencyCode} /> diff --git a/apps/dashboard/src/components/reservations/ReservationPartyPanel.tsx b/apps/dashboard/src/components/reservations/ReservationPartyPanel.tsx index 48f5972..fe86c17 100644 --- a/apps/dashboard/src/components/reservations/ReservationPartyPanel.tsx +++ b/apps/dashboard/src/components/reservations/ReservationPartyPanel.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { UserPlus, Split, ArrowRightLeft, Trash2 } from 'lucide-react'; import { api } from '../../lib/api'; -import { moneyString, requirePropertyId } from '../../lib/api-helpers'; +import { moneyString, requirePropertyId, requireCurrency } from '../../lib/api-helpers'; import { useToast } from '../ui/Toast'; import FindGuest from '../guests/FindGuest'; import type { Guest } from '../../types/guest'; @@ -54,14 +54,14 @@ export default function ReservationPartyPanel({ roomTypeId, ratePlanId, totalAmount, - currencyCode = 'USD', + currencyCode, }: { reservationId: string; propertyId: string; roomTypeId?: string; ratePlanId?: string; totalAmount?: string; - currencyCode?: string; + currencyCode?: string | null; }) { const { t } = useTranslation(); const { toast } = useToast(); @@ -166,6 +166,7 @@ export default function ReservationPartyPanel({ const splitMutation = useMutation({ mutationFn: () => { requirePropertyId(propertyId); + requireCurrency(currencyCode); return api.post( `/v1/reservations/${reservationId}/split`, { diff --git a/apps/dashboard/src/pages/Accounting.tsx b/apps/dashboard/src/pages/Accounting.tsx index 69c5e70..f23ce80 100644 --- a/apps/dashboard/src/pages/Accounting.tsx +++ b/apps/dashboard/src/pages/Accounting.tsx @@ -279,10 +279,11 @@ function AccountingHome() { const recordArPayment = useMutation({ mutationFn: () => { requirePropertyId(propertyId); + requireCurrency(selectedLedger?.currencyCode ?? null); return api.post(`/v1/ar/ledgers/${selectedLedger!.id}/payments`, { propertyId, amount: moneyString(arPaymentAmount), - currencyCode: selectedLedger?.currencyCode ?? 'USD', + currencyCode: selectedLedger?.currencyCode, }); }, onSuccess: () => { diff --git a/apps/dashboard/src/pages/Folios.tsx b/apps/dashboard/src/pages/Folios.tsx index 81a7b23..0de23eb 100644 --- a/apps/dashboard/src/pages/Folios.tsx +++ b/apps/dashboard/src/pages/Folios.tsx @@ -243,14 +243,17 @@ function SplitFolioPanel({ }); const createSplitFolio = useMutation({ - mutationFn: () => - api.post('/v1/folios', { + mutationFn: () => { + requirePropertyId(propertyId); + requireCurrency(folio.currencyCode ?? null); + return api.post('/v1/folios', { propertyId, reservationId, guestId: folio.guestId, type: 'guest', - currencyCode: folio.currencyCode ?? 'USD', - }), + currencyCode: folio.currencyCode, + }); + }, onSuccess: () => { refetchSiblings(); setSplitOpen(false); @@ -497,7 +500,7 @@ function FolioDetail() { const folio: Folio | null = folioData?.data ?? folioData ?? null; const charges: Charge[] = chargesData?.data ?? chargesData ?? []; const payments: Payment[] = paymentsData?.data ?? paymentsData ?? []; - const currencyCode = folio?.currencyCode ?? 'USD'; + const currencyCode = folio?.currencyCode ?? null; const reversedIds = new Set( charges.filter((c) => c.isReversal && c.originalChargeId).map((c) => c.originalChargeId!), diff --git a/apps/dashboard/src/pages/FrontDesk.tsx b/apps/dashboard/src/pages/FrontDesk.tsx index 7f9e657..36ff45c 100644 --- a/apps/dashboard/src/pages/FrontDesk.tsx +++ b/apps/dashboard/src/pages/FrontDesk.tsx @@ -4,7 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { ConciergeBell, LogIn, Users, LogOut, UserPlus, UsersRound, ArrowRightLeft, StickyNote, UserRound, Plus, X } from 'lucide-react'; import { addDays, differenceInCalendarDays, format, parseISO } from 'date-fns'; import { api } from '../lib/api'; -import { moneyString, requirePropertyId } from '../lib/api-helpers'; +import { moneyString, requirePropertyId, requireCurrency } from '../lib/api-helpers'; import { useProperty } from '../context/PropertyContext'; import StatusBadge from '../components/ui/StatusBadge'; import Modal from '../components/ui/Modal'; @@ -472,6 +472,7 @@ export default function FrontDesk() { const plans: any[] = Array.isArray(ratePlans) ? ratePlans : ratePlans?.data ?? []; const plan = plans.find((p) => p.id === wiRatePlanId); + requireCurrency(plan?.currencyCode ?? null); const nightly = Number(plan?.baseAmount ?? 0); const guestId = wiGuest.id; const resCreate = await api.post( @@ -486,7 +487,7 @@ export default function FrontDesk() { adults: 1 + primaryAdditionalIds.length, source: 'walk_in', totalAmount: moneyString(nightly * wiNights), - currencyCode: plan?.currencyCode ?? 'USD', + currencyCode: plan?.currencyCode, }, { skipErrorToast: true }, ); @@ -518,6 +519,7 @@ export default function FrontDesk() { for (let i = 0; i < wiExtraRooms.length; i++) { const extra = wiExtraRooms[i]; const planExtra = plans.find((p) => p.id === extra.ratePlanId); + requireCurrency(planExtra?.currencyCode ?? null); const nightlyExtra = Number(planExtra?.baseAmount ?? 0); const extraGuestIds = [extra.guest!.id, ...extraAdditionalIds[i]]; // Occupants must be attached to the source (primary) reservation before they @@ -539,7 +541,7 @@ export default function FrontDesk() { roomTypeId: extra.roomTypeId, ratePlanId: extra.ratePlanId, totalAmount: moneyString(nightlyExtra * wiNights), - currencyCode: planExtra?.currencyCode ?? 'USD', + currencyCode: planExtra?.currencyCode, roomId: extra.roomId, adults: extraGuestIds.length, overrideMaxOccupancy: extra.overrideOccupancy, diff --git a/apps/dashboard/src/pages/HouseAccounts.tsx b/apps/dashboard/src/pages/HouseAccounts.tsx index 5bb516d..b521027 100644 --- a/apps/dashboard/src/pages/HouseAccounts.tsx +++ b/apps/dashboard/src/pages/HouseAccounts.tsx @@ -65,6 +65,7 @@ function HouseAccountList() { const createMutation = useMutation({ mutationFn: () => { requirePropertyId(propertyId); + requireCurrency(currencyCode); return api.post('/v1/house-accounts', { propertyId, name, kind, currencyCode }); }, onSuccess: () => { @@ -321,7 +322,7 @@ function HouseAccountDetail() { const account: HouseAccount | null = data?.data ?? data ?? null; const products: Product[] = productsData?.data ?? productsData ?? []; - const currencyCode = account?.currencyCode ?? 'USD'; + const currencyCode = account?.currencyCode ?? null; const postCharge = useMutation({ mutationFn: () => { diff --git a/apps/dashboard/src/pages/RatePlans.tsx b/apps/dashboard/src/pages/RatePlans.tsx index de2a40e..a7335c7 100644 --- a/apps/dashboard/src/pages/RatePlans.tsx +++ b/apps/dashboard/src/pages/RatePlans.tsx @@ -541,7 +541,7 @@ function RatePlanDetail() {
{t('ratePlans.code')}
{plan.code}
{t('ratePlans.baseAmount')}
{plan.baseAmount != null ? formatMoney(plan.baseAmount, currencyCode) : '—'}
{t('ratePlans.roomType')}
{plan.roomTypeName ?? '—'}
{t('ratePlans.currency')}
{plan.currency ?? plan.currencyCode ?? 'USD'}
{t('ratePlans.currency')}
{plan.currency ?? plan.currencyCode ?? '—'}
{t('ratePlans.parentRatePlan')}
{plan.parentRatePlanId ?? '—'}