From 429032b5834dda4cd66f7b0d007e08c35a04c604 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 10 Aug 2026 23:20:00 +0530 Subject: [PATCH 1/2] Refactor: migrate convertToDisplayString to useCurrencyListActions hook (part 19) --- src/components/KYCWall/BaseKYCWall.tsx | 3 +- .../MoneyRequestViewReportFields.tsx | 6 +- .../ReportActionItem/MoneyReportView.tsx | 4 +- src/hooks/useSearchBulkActions.ts | 3 +- src/libs/CurrencyUtils.ts | 30 +++++++ src/libs/Formula.ts | 30 ++++--- src/libs/ReportUtils.ts | 16 ++-- src/libs/actions/IOU/BulkEdit.ts | 1 + src/libs/actions/IOU/DeleteMoneyRequest.ts | 2 +- src/libs/actions/IOU/MoneyRequestBuilder.ts | 11 ++- src/libs/actions/IOU/UpdateMoneyRequest.ts | 1 + src/libs/actions/Policy/Policy.ts | 2 +- src/libs/actions/Report/index.ts | 10 ++- .../DynamicReportChangeWorkspacePage.tsx | 3 +- tests/actions/ReportTest.ts | 22 ++--- tests/unit/FormulaTest.ts | 81 ++++++++++++++----- tests/unit/NextStepUtilsTest.ts | 1 + tests/unit/ReportUtilsTest.ts | 25 +++++- 18 files changed, 181 insertions(+), 70 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index f5ee4bfe8c29..40f9804a91d6 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -180,6 +180,7 @@ function KYCWall({ currentUserAccountID, employeeLogin, doesSubmitterPersonalDetailExist ?? false, + getCurrencyDecimals, reportTransactions, ); if (inviteResult?.policyExpenseChatReportID) { @@ -191,7 +192,7 @@ function KYCWall({ Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: adminPolicy.id})); }); } else { - const moveResult = moveIOUReportToPolicy(iouReport, adminPolicy, reportPreviewAction, true, reportTransactions); + const moveResult = moveIOUReportToPolicy(iouReport, adminPolicy, reportPreviewAction, getCurrencyDecimals, true, reportTransactions); savePreferredPaymentMethod(iouReport.policyID, adminPolicy.id, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[adminPolicy.id]); if (moveResult?.policyExpenseChatReportID && !moveResult.useTemporaryOptimisticExpenseChatReportID) { diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 8cd677b2c596..c58bb07a5e6b 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -1,6 +1,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -87,6 +88,7 @@ function ReportFieldView(reportField: EnrichedPolicyReportField, report: OnyxEnt function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequestViewReportFieldsProps) { const styles = useThemeStyles(); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const {getCurrencyDecimals} = useCurrencyListActions(); const sortedPolicyReportFields = useMemo((): EnrichedPolicyReportField[] => { const {fieldValues, fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {}); @@ -97,7 +99,7 @@ function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequ .filter((reportField) => !shouldHideSingleReportField(reportField)) .sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight) .map((field): EnrichedPolicyReportField => { - const fieldValue = resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName); + const fieldValue = resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName, getCurrencyDecimals); const isFieldDisabled = isReportFieldDisabledForUser(report, field, policy, currentUserAccountID); const isDeletedFormulaField = field.type === CONST.REPORT_FIELD_TYPES.FORMULA && field.deletable; const fieldKey = getReportFieldKey(field.fieldID); @@ -114,7 +116,7 @@ function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequ violationTranslation, }; }); - }, [policy, report, currentUserAccountID]); + }, [policy, report, currentUserAccountID, getCurrencyDecimals]); const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report, policy?.type); const isInvoiceReport = isInvoiceReportUtils(report); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4e9105e4de60..d9233fc1be15 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -100,7 +100,7 @@ function MoneyReportView({ const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const {convertToDisplayString} = useCurrencyListActions(); + const {convertToDisplayString, getCurrencyDecimals} = useCurrencyListActions(); const {isOffline} = useNetwork(); const isSettled = isSettledReportUtils(report?.reportID); const isTotalUpdated = hasUpdatedTotal(report, policy) && !isTotalPending; @@ -184,7 +184,7 @@ function MoneyReportView({ return null; } - const fieldValue = resolveReportFieldValue(reportField, report, policy, fieldValues, fieldsByName); + const fieldValue = resolveReportFieldValue(reportField, report, policy, fieldValues, fieldsByName, getCurrencyDecimals); const isFieldDisabled = isReportFieldDisabledForUser(report, reportField, policy, currentUserAccountID); const fieldKey = getReportFieldKey(reportField.fieldID); diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index ca915e7755a1..ce7e4213a061 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1260,10 +1260,11 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { accountID, getLoginByAccountID(itemReport?.ownerAccountID, personalDetails), doesPersonalDetailExistSelector(itemReport?.ownerAccountID)(personalDetails), + getCurrencyDecimals, reportTransactions, ); if (!invite?.policyExpenseChatReportID) { - moveIOUReportToPolicy(itemReport, adminPolicy, reportPreviewAction, false, reportTransactions); + moveIOUReportToPolicy(itemReport, adminPolicy, reportPreviewAction, getCurrencyDecimals, false, reportTransactions); } } } diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index cfd35319ba84..67c402bf13ac 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -202,6 +202,35 @@ function convertToDisplayStringEnLocale(amountInCents: number, currency: string }); } +/** + * Same as convertToDisplayStringWithoutCurrency but always formats with the `en` locale, with decimals + * injected. Used alongside convertToDisplayStringEnLocale for stored values (e.g. formula-computed + * report titles) that must not depend on the viewer's locale or this module's Onyx fallback. + */ +function convertToDisplayStringWithoutCurrencyEnLocale( + amountInCents: number, + currency: string | undefined, + getCurrencyDecimalsImpl: CurrencyListActionsContextType['getCurrencyDecimals'], +): string { + const sanitizedCurrency = sanitizeCurrencyCode(currency); + const decimals = getCurrencyDecimalsImpl(sanitizedCurrency); + const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, decimals); + return formatToParts(CONST.LOCALES.EN, convertedAmount, { + style: 'currency', + currency: sanitizedCurrency, + + // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies + // See: https://github.com/Expensify/PHP-Libs/pull/834 + minimumFractionDigits: decimals, + // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places. + maximumFractionDigits: 2, + }) + .filter((x) => x.type !== 'currency') + .filter((x) => x.type !== 'literal' || x.value.trim().length !== 0) + .map((x) => x.value) + .join(''); +} + /** Same intended use as convertToDisplayString, but purposely omit currency symbol if not provided */ function convertToDisplayStringWithExplicitCurrency(amountInCents: number, currency: string | undefined, currencies?: CurrencyList): string { if (!currency) { @@ -285,6 +314,7 @@ export { convertToDisplayStringEnLocale, convertAmountToDisplayString, convertToDisplayStringWithoutCurrency, + convertToDisplayStringWithoutCurrencyEnLocale, convertToDisplayStringWithExplicitCurrency, convertToShortDisplayString, }; diff --git a/src/libs/Formula.ts b/src/libs/Formula.ts index a7d928cb17df..d214500907c4 100644 --- a/src/libs/Formula.ts +++ b/src/libs/Formula.ts @@ -1,5 +1,7 @@ +import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList'; + import CONST from '@src/CONST'; -import type {CurrencyList, PersonalDetails, Policy, PolicyReportField, Report, Transaction} from '@src/types/onyx'; +import type {PersonalDetails, Policy, PolicyReportField, Report, Transaction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxEntry} from 'react-native-onyx'; @@ -7,7 +9,7 @@ import type {ValueOf} from 'type-fest'; import {endOfDay, endOfMonth, endOfWeek, getDay, lastDayOfMonth, set, startOfMonth, startOfWeek, subDays} from 'date-fns'; -import {convertToDisplayString, convertToDisplayStringWithoutCurrency, isValidCurrencyCode} from './CurrencyUtils'; +import {convertToDisplayStringEnLocale, convertToDisplayStringWithoutCurrencyEnLocale, isValidCurrencyCode} from './CurrencyUtils'; import {getCurrentUserEmail} from './CurrentUserStore'; import formatDate from './FormulaDatetime'; import getBase62ReportID from './getBase62ReportID'; @@ -36,7 +38,7 @@ type MinimalTransaction = Pick; - currencyList?: CurrencyList; + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals']; transaction?: Transaction; submitterPersonalDetails?: PersonalDetails; managerPersonalDetails?: PersonalDetails; @@ -387,12 +389,12 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string { case 'enddate': return formatDate(getNewestTransactionDate(report.reportID, context) ?? new Date().toISOString(), format); case 'total': { - const formattedAmount = formatAmount(report.total, report.currency, format, context.currencyList); + const formattedAmount = formatAmount(report.total, report.currency, format, context.getCurrencyDecimals); // Return empty string when conversion needed (formatAmount returns null for unavailable conversions) return formattedAmount ?? ''; } case 'reimbursable': { - const formattedAmount = formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, report.currency, format, context.currencyList); + const formattedAmount = formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, report.currency, format, context.getCurrencyDecimals); return formattedAmount ?? ''; } case 'currency': @@ -580,7 +582,12 @@ function getSubstring(value: string, args: string[]): string { * Format an amount value * @returns The formatted amount string, or null if currency conversion is needed (unavailable on frontend) */ -function formatAmount(amount: number | undefined, currency: string | undefined, displayCurrency?: string, currencyList?: CurrencyList): string | null { +function formatAmount( + amount: number | undefined, + currency: string | undefined, + displayCurrency: string | undefined, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], +): string | null { if (amount === undefined) { return ''; } @@ -592,7 +599,7 @@ function formatAmount(amount: number | undefined, currency: string | undefined, const trimmedDisplayCurrency = displayCurrency?.trim().toUpperCase(); if (trimmedDisplayCurrency) { if (trimmedDisplayCurrency === 'NOSYMBOL') { - return convertToDisplayStringWithoutCurrency(absoluteAmount, trimmedCurrency, currencyList); + return convertToDisplayStringWithoutCurrencyEnLocale(absoluteAmount, trimmedCurrency, getCurrencyDecimals); } // If a currency conversion is needed (displayCurrency differs from the source), @@ -607,7 +614,7 @@ function formatAmount(amount: number | undefined, currency: string | undefined, return ''; } - return convertToDisplayString(absoluteAmount, trimmedDisplayCurrency, false, currencyList); + return convertToDisplayStringEnLocale(absoluteAmount, trimmedDisplayCurrency, getCurrencyDecimals); } if (trimmedCurrency) { @@ -615,10 +622,10 @@ function formatAmount(amount: number | undefined, currency: string | undefined, if (!isValidCurrencyCode(trimmedCurrency)) { return ''; } - return convertToDisplayString(absoluteAmount, trimmedCurrency, true, currencyList); + return convertToDisplayStringEnLocale(absoluteAmount, trimmedCurrency, getCurrencyDecimals); } - return convertToDisplayStringWithoutCurrency(absoluteAmount, currency, currencyList); + return convertToDisplayStringWithoutCurrencyEnLocale(absoluteAmount, currency, getCurrencyDecimals); } catch (error) { Log.hmmm('[Formula] formatAmount failed', {error, amount, currency, displayCurrency}); return ''; @@ -1010,6 +1017,7 @@ function resolveReportFieldValue( policy: OnyxEntry, fieldValues: Record, fieldsByName: Record, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], ): string { const fieldValue = field.value ?? field.defaultValue ?? ''; @@ -1017,7 +1025,7 @@ function resolveReportFieldValue( return fieldValue; } - return compute(field.defaultValue, {report, policy, fieldValues, fieldsByName}); + return compute(field.defaultValue, {report, policy, fieldValues, fieldsByName, getCurrencyDecimals}); } export {FORMULA_PART_TYPES, compute, computeWithMetadata, parse, hasCircularReferences, resolveReportFieldValue}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5c7261e1de77..fc786b7bdbe0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -996,7 +996,6 @@ type BuildOptimisticExpenseReportParams = { optimisticIOUReportID?: string; reportTransactions?: Record; createdTimestamp?: string; - currencyList?: CurrencyList; getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals']; }; @@ -7005,9 +7004,9 @@ function computeOptimisticReportName( policy: OnyxEntry, policyID: string | undefined, reportTransactions: Record, - currencyList?: CurrencyList, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], ): string | null { - const result = computeOptimisticReportNameWithMetadata(report, policy, policyID, reportTransactions, currencyList); + const result = computeOptimisticReportNameWithMetadata(report, policy, policyID, reportTransactions, getCurrencyDecimals); if (!result || result.hasUnresolvedTokens) { return null; } @@ -7023,7 +7022,7 @@ function computeOptimisticReportNameWithMetadata( policy: OnyxEntry, policyID: string | undefined, reportTransactions: Record, - currencyList?: CurrencyList, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], ): {value: string; hasUnresolvedTokens: boolean} | null { if (!isGroupPolicyPolicyUtils(policy)) { return null; @@ -7033,7 +7032,7 @@ function computeOptimisticReportNameWithMetadata( const formulaContext: FormulaContext = { report, policy, - currencyList, + getCurrencyDecimals, allTransactions: reportTransactions, }; @@ -7105,7 +7104,6 @@ function buildOptimisticExpenseReport({ optimisticIOUReportID, reportTransactions, createdTimestamp, - currencyList, getCurrencyDecimals, }: BuildOptimisticExpenseReportParams): OptimisticExpenseReport { // The amount for Expense reports are stored as negative value in the database @@ -7158,7 +7156,7 @@ function buildOptimisticExpenseReport({ } // Compute optimistic report name if applicable - const computedName = computeOptimisticReportName(expenseReport, policy, policyID, reportTransactions ?? {}, currencyList); + const computedName = computeOptimisticReportName(expenseReport, policy, policyID, reportTransactions ?? {}, getCurrencyDecimals); if (computedName !== null) { expenseReport.reportName = computedName; } @@ -7178,7 +7176,7 @@ function buildOptimisticEmptyReport( policy: OnyxEntry, timeOfCreation: string, betas: OnyxEntry, - currencyList?: CurrencyList, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], ) { const {stateNum, statusNum} = getExpenseReportStateAndStatus(policy, betas, true); const optimisticEmptyReport: OptimisticNewReport = { @@ -7205,7 +7203,7 @@ function buildOptimisticEmptyReport( }; // Compute optimistic report name if applicable - const optimisticReportName = computeOptimisticReportName(optimisticEmptyReport as Report, policy, policy?.id, {}, currencyList); + const optimisticReportName = computeOptimisticReportName(optimisticEmptyReport as Report, policy, policy?.id, {}, getCurrencyDecimals); if (optimisticReportName !== null) { optimisticEmptyReport.reportName = optimisticReportName; } diff --git a/src/libs/actions/IOU/BulkEdit.ts b/src/libs/actions/IOU/BulkEdit.ts index 283381398d43..8cdcaf73c19f 100644 --- a/src/libs/actions/IOU/BulkEdit.ts +++ b/src/libs/actions/IOU/BulkEdit.ts @@ -502,6 +502,7 @@ function updateMultipleMoneyRequests({ transaction, isTransactionOnHold, transactionPolicy, + getCurrencyDecimals, optimisticReportAction?.actorAccountID, transactionChanges, additionalTransactionsForFormula, diff --git a/src/libs/actions/IOU/DeleteMoneyRequest.ts b/src/libs/actions/IOU/DeleteMoneyRequest.ts index ad7ff504f275..5a03749f9c1a 100644 --- a/src/libs/actions/IOU/DeleteMoneyRequest.ts +++ b/src/libs/actions/IOU/DeleteMoneyRequest.ts @@ -278,7 +278,7 @@ function prepareToCleanUpMoneyRequest({ overlay[priorTxn.transactionID] = {...priorTxn, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}; } } - updatedIOUReport = maybeUpdateReportNameForFormulaTitle(updatedIOUReport, policy, overlay); + updatedIOUReport = maybeUpdateReportNameForFormulaTitle(updatedIOUReport, policy, getCurrencyDecimals, overlay); } } diff --git a/src/libs/actions/IOU/MoneyRequestBuilder.ts b/src/libs/actions/IOU/MoneyRequestBuilder.ts index 019cf6211251..4fe1995d0cca 100644 --- a/src/libs/actions/IOU/MoneyRequestBuilder.ts +++ b/src/libs/actions/IOU/MoneyRequestBuilder.ts @@ -1200,6 +1200,7 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR function recalculateOptimisticReportName( iouReport: OnyxTypes.Report, policy: OnyxEntry, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], optimisticTransactions: Record = {}, ): string | undefined { if (!policy?.fieldList?.[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]) { @@ -1220,7 +1221,7 @@ function recalculateOptimisticReportName( transactionsRecord[id] = transaction; } - const result = computeOptimisticReportNameWithMetadata(iouReport, policy, iouReport.policyID, transactionsRecord); + const result = computeOptimisticReportNameWithMetadata(iouReport, policy, iouReport.policyID, transactionsRecord, getCurrencyDecimals); if (!result || result.hasUnresolvedTokens) { return undefined; } @@ -1230,6 +1231,7 @@ function recalculateOptimisticReportName( function maybeUpdateReportNameForFormulaTitle( iouReport: OnyxTypes.Report, policy: OnyxEntry, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], optimisticTransactions: Record = {}, ): OnyxTypes.Report { const allReportNameValuePairs = getAllReportNameValuePairs(); @@ -1241,7 +1243,7 @@ function maybeUpdateReportNameForFormulaTitle( return iouReport; } - const updatedReportName = recalculateOptimisticReportName(iouReport, policy, optimisticTransactions); + const updatedReportName = recalculateOptimisticReportName(iouReport, policy, getCurrencyDecimals, optimisticTransactions); if (!updatedReportName) { return iouReport; } @@ -1586,7 +1588,7 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma // Runs after STEP 3 so the optimistic transaction is in the formula context; the gate skips // total-stale cases where the formula would bake in a wrong `{report:total}`. if (!shouldCreateNewMoneyRequestReport && isPolicyExpenseChat && didUpdateOptimisticTotal) { - iouReport = maybeUpdateReportNameForFormulaTitle(iouReport, policy, {[optimisticTransaction.transactionID]: optimisticTransaction}); + iouReport = maybeUpdateReportNameForFormulaTitle(iouReport, policy, getCurrencyDecimals, {[optimisticTransaction.transactionID]: optimisticTransaction}); } // STEP 4: Build optimistic reportActions. We need: @@ -1789,6 +1791,7 @@ function getUpdatedMoneyRequestReportData( transaction: OnyxEntry, isTransactionOnHold: boolean, policy: OnyxEntry, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], actorAccountID?: number, transactionChanges?: TransactionChanges, // Overlaid on Onyx in the formula context — search snapshots, prior bulk-edit iterations, etc. @@ -1853,7 +1856,7 @@ function getUpdatedMoneyRequestReportData( if (updatedTransaction?.transactionID) { optimisticTransactions[updatedTransaction.transactionID] = updatedTransaction; } - updatedMoneyRequestReport = maybeUpdateReportNameForFormulaTitle(updatedMoneyRequestReport, policy, optimisticTransactions); + updatedMoneyRequestReport = maybeUpdateReportNameForFormulaTitle(updatedMoneyRequestReport, policy, getCurrencyDecimals, optimisticTransactions); } } else { updatedMoneyRequestReport = updateIOUOwnerAndTotal(iouReport, actorAccountID ?? CONST.DEFAULT_NUMBER_ID, diff, getCurrency(transaction), false, true, isTransactionOnHold); diff --git a/src/libs/actions/IOU/UpdateMoneyRequest.ts b/src/libs/actions/IOU/UpdateMoneyRequest.ts index 42f9dab94998..92cd05bfaf78 100644 --- a/src/libs/actions/IOU/UpdateMoneyRequest.ts +++ b/src/libs/actions/IOU/UpdateMoneyRequest.ts @@ -1782,6 +1782,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U transaction, isTransactionOnHold, policy, + getCurrencyDecimals, updatedReportAction?.actorAccountID, transactionChanges, ); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 5c8552783f45..ff14f77dedd9 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4629,7 +4629,7 @@ function createWorkspaceFromIOUPayment({ transactionsRecord[transaction.transactionID] = transaction; } } - const computedExpenseReportName = ReportUtils.computeOptimisticReportName(expenseReport, newWorkspace as Policy, policyID, transactionsRecord); + const computedExpenseReportName = ReportUtils.computeOptimisticReportName(expenseReport, newWorkspace as Policy, policyID, transactionsRecord, getCurrencyDecimals); if (computedExpenseReportName !== null) { expenseReport.reportName = computedExpenseReportName; } diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 6ecef773055c..cb5ed0ed15dd 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4178,7 +4178,7 @@ function buildNewReportOptimisticData({ const {accountID, login, email} = ownerPersonalDetails; const timeOfCreation = DateUtils.getDBTime(); const parentReport = getPolicyExpenseChat(accountID, policy?.id); - const optimisticReportData = buildOptimisticEmptyReport(reportID, accountID, login, parentReport, reportPreviewReportActionID, policy, timeOfCreation, betas); + const optimisticReportData = buildOptimisticEmptyReport(reportID, accountID, login, parentReport, reportPreviewReportActionID, policy, timeOfCreation, betas, getCurrencyDecimals); if (reportName) { optimisticReportData.reportName = reportName; @@ -6984,6 +6984,7 @@ function moveIOUReportToPolicy( iouReport: OnyxEntry, policy: Policy, reportPreviewAction: OnyxEntry, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], isFromSettlementButton?: boolean, reportTransactions: Transaction[] = [], ): {policyExpenseChatReportID?: string; useTemporaryOptimisticExpenseChatReportID: boolean} | undefined { @@ -7011,6 +7012,7 @@ function moveIOUReportToPolicy( policyID, optimisticExpenseChatReportID, reportPreviewAction, + getCurrencyDecimals, reportTransactions, ); @@ -7037,6 +7039,7 @@ function moveIOUReportToPolicyAndInviteSubmitter( currentUserAccountID: number, submitterLogin: string | undefined, doesSubmitterPersonalDetailExist: boolean, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], reportTransactions: Transaction[] = [], ): {policyExpenseChatReportID?: string} | undefined { if (!policy || !iouReport) { @@ -7169,7 +7172,7 @@ function moveIOUReportToPolicyAndInviteSubmitter( failureData: convertedFailureData, movedExpenseReportAction, movedReportAction, - } = convertIOUReportToExpenseReport(iouReport, policy, policyID, optimisticPolicyExpenseChatReportID, reportPreviewAction, reportTransactions); + } = convertIOUReportToExpenseReport(iouReport, policy, policyID, optimisticPolicyExpenseChatReportID, reportPreviewAction, getCurrencyDecimals, reportTransactions); optimisticData.push(...convertedOptimisticData); successData.push(...convertedSuccessData); @@ -7194,6 +7197,7 @@ function convertIOUReportToExpenseReport( policyID: string, optimisticPolicyExpenseChatReportID: string, reportPreviewAction: OnyxEntry, + getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'], reportTransactions: Transaction[] = [], ) { const optimisticData: Array> = []; @@ -7226,7 +7230,7 @@ function convertIOUReportToExpenseReport( } // Compute optimistic report name if applicable - const computedName = computeOptimisticReportName(expenseReport, policy, policyID, transactionsRecord); + const computedName = computeOptimisticReportName(expenseReport, policy, policyID, transactionsRecord, getCurrencyDecimals); if (computedName !== null) { expenseReport.reportName = computedName; } diff --git a/src/pages/DynamicReportChangeWorkspacePage.tsx b/src/pages/DynamicReportChangeWorkspacePage.tsx index c7a44be93414..a9666643729a 100644 --- a/src/pages/DynamicReportChangeWorkspacePage.tsx +++ b/src/pages/DynamicReportChangeWorkspacePage.tsx @@ -134,10 +134,11 @@ function DynamicReportChangeWorkspacePage({report}: DynamicReportChangeWorkspace session?.accountID ?? CONST.DEFAULT_NUMBER_ID, submitterLogin, doesSubmitterPersonalDetailExist ?? false, + getCurrencyDecimals, reportTransactions, ); if (!invite?.policyExpenseChatReportID) { - moveIOUReportToPolicy(report, policy, reportPreviewAction, false, reportTransactions); + moveIOUReportToPolicy(report, policy, reportPreviewAction, getCurrencyDecimals, false, reportTransactions); } return; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 560bd5d90021..8b1a473d79d3 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -4360,7 +4360,7 @@ describe('actions/Report', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); // When moving iou to a workspace - Report.moveIOUReportToPolicy(iouReport, policy, undefined); + Report.moveIOUReportToPolicy(iouReport, policy, undefined, TestHelper.getCurrencyDecimalsLocal); await waitForBatchedUpdates(); // Then MOVED report action should be added to the expense report @@ -4382,7 +4382,7 @@ describe('actions/Report', () => { type: CONST.REPORT.TYPE.EXPENSE, }; const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN}; - const result = Report.moveIOUReportToPolicy(expenseReport, policy, undefined); + const result = Report.moveIOUReportToPolicy(expenseReport, policy, undefined, TestHelper.getCurrencyDecimalsLocal); expect(result).toBeUndefined(); }); @@ -4407,7 +4407,7 @@ describe('actions/Report', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, {[iouReportAction.reportActionID]: iouReportAction}); await waitForBatchedUpdates(); - const result = Report.moveIOUReportToPolicy(iouReport, policy, undefined, false); + const result = Report.moveIOUReportToPolicy(iouReport, policy, undefined, TestHelper.getCurrencyDecimalsLocal, false); expect(result).toBeUndefined(); }); @@ -4444,7 +4444,7 @@ describe('actions/Report', () => { await waitForBatchedUpdates(); // When isFromSettlementButton is true, it should proceed despite hasRequestFromCurrentAccount being true - const result = Report.moveIOUReportToPolicy(iouReport, policy, undefined, true); + const result = Report.moveIOUReportToPolicy(iouReport, policy, undefined, TestHelper.getCurrencyDecimalsLocal, true); expect(result).toBeDefined(); expect(result?.policyExpenseChatReportID).toBeDefined(); }); @@ -4474,7 +4474,7 @@ describe('actions/Report', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policyWithEmptyFieldList); // When converting IOU report to expense report - const result = Report.convertIOUReportToExpenseReport(iouReport, policyWithEmptyFieldList, policyID, 'expenseChat123', undefined, []); + const result = Report.convertIOUReportToExpenseReport(iouReport, policyWithEmptyFieldList, policyID, 'expenseChat123', undefined, TestHelper.getCurrencyDecimalsLocal, []); // Then the report name should be set to the default formula result ("New Report") const reportUpdate = result.optimisticData.find((update) => update.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`); @@ -4506,7 +4506,7 @@ describe('actions/Report', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policyWithEmptyFieldList); // When converting IOU report to expense report - const result = Report.convertIOUReportToExpenseReport(iouReport, policyWithEmptyFieldList, policyID, 'expenseChat124', undefined, []); + const result = Report.convertIOUReportToExpenseReport(iouReport, policyWithEmptyFieldList, policyID, 'expenseChat124', undefined, TestHelper.getCurrencyDecimalsLocal, []); // Then the report name should be set to the default formula result ("New Report") const reportUpdate = result.optimisticData.find((update) => update.key === `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`); @@ -4538,7 +4538,7 @@ describe('actions/Report', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); // When moving iou to a workspace and invite the submitter - Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true); + Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true, TestHelper.getCurrencyDecimalsLocal); await waitForBatchedUpdates(); // Then MOVED report action should be added to the expense report @@ -4604,7 +4604,7 @@ describe('actions/Report', () => { await waitForBatchedUpdates(); // Call moveIOUReportToPolicyAndInviteSubmitter - Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true); + Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true, TestHelper.getCurrencyDecimalsLocal); await waitForBatchedUpdates(); // Simulate network failure @@ -4662,7 +4662,7 @@ describe('actions/Report', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction); // When moving IOU to a workspace with reportTransactions - Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true, [transaction]); + Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true, TestHelper.getCurrencyDecimalsLocal, [transaction]); await waitForBatchedUpdates(); // Then the transaction amounts should be negated optimistically @@ -4708,7 +4708,7 @@ describe('actions/Report', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction); // When moving IOU to a workspace with transactions - Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true, [transaction]); + Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, policy, {}, undefined, TEST_USER_ACCOUNT_ID, ownerEmail, true, TestHelper.getCurrencyDecimalsLocal, [transaction]); await waitForBatchedUpdates(); // Then the report should be converted to an expense report with the new policyID @@ -4736,7 +4736,7 @@ describe('actions/Report', () => { it('should return undefined when iouReport is missing', () => { const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN}; - const result = Report.moveIOUReportToPolicyAndInviteSubmitter(undefined, policy, {}, undefined, TEST_USER_ACCOUNT_ID, '', false); + const result = Report.moveIOUReportToPolicyAndInviteSubmitter(undefined, policy, {}, undefined, TEST_USER_ACCOUNT_ID, '', false, TestHelper.getCurrencyDecimalsLocal); expect(result).toBeUndefined(); }); }); diff --git a/tests/unit/FormulaTest.ts b/tests/unit/FormulaTest.ts index 2d7edcca6aa4..9b2fc42285a4 100644 --- a/tests/unit/FormulaTest.ts +++ b/tests/unit/FormulaTest.ts @@ -7,6 +7,7 @@ import CONST from '@src/CONST'; import type {PersonalDetails, Policy, PolicyReportField, Report, ReportActions, Transaction} from '@src/types/onyx'; import createMock from '../utils/createMock'; +import {getCurrencyDecimalsLocal} from '../utils/TestHelper'; jest.mock('@libs/ReportActionsUtils', () => ({ getAllReportActions: jest.fn(), @@ -76,6 +77,7 @@ describe('CustomFormula', () => { describe('compute()', () => { const mockContext: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({ reportID: '123', reportName: '', @@ -261,6 +263,7 @@ describe('CustomFormula', () => { test('should handle missing report data gracefully', () => { const contextWithMissingData: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({}), policy: null as unknown as Policy, }; @@ -371,6 +374,7 @@ describe('CustomFormula', () => { describe('Reimbursable Amount', () => { const reimbursableContext: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: { reportID: '123', reportName: '', @@ -443,6 +447,7 @@ describe('CustomFormula', () => { describe('Currency Formatting & Conversion', () => { const currencyContext: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({ reportID: '123', total: -10000, @@ -534,6 +539,7 @@ describe('CustomFormula', () => { describe('Function Modifiers', () => { const mockContext: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({ reportID: 'report123456789', reportName: '', @@ -560,6 +566,7 @@ describe('CustomFormula', () => { test('should handle empty strings', () => { const contextWithEmpty: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({}), policy: createMock({name: ''}), }; @@ -583,6 +590,7 @@ describe('CustomFormula', () => { test('should handle empty strings', () => { const contextWithEmpty: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({}), policy: createMock({name: ''}), }; @@ -621,7 +629,7 @@ describe('CustomFormula', () => { describe('Auto-reporting Frequency', () => { const mockReport = createMock({reportID: '123'}); - const createMockContext = (policy: Policy): FormulaContext => ({report: mockReport, policy}); + const createMockContext = (policy: Policy): FormulaContext => ({getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy}); beforeEach(() => { jest.clearAllMocks(); @@ -698,6 +706,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, transaction: createMock({transactionID: 'optimistic1', reportID: '123', created: '2025-01-14T16:00:00Z', merchant: 'Restaurant', amount: 3000}), @@ -712,6 +721,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, allTransactions: { @@ -730,6 +740,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, allTransactions: { @@ -757,6 +768,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, allTransactions: { @@ -775,6 +787,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, allTransactions: { @@ -793,6 +806,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, allTransactions: { @@ -809,6 +823,7 @@ describe('CustomFormula', () => { const policy = createMock({autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy, allTransactions: { @@ -830,13 +845,13 @@ describe('CustomFormula', () => { }); test('should return formula definition when policy or frequency is missing', () => { - expect(compute('{report:autoreporting:start}', {report: mockReport, policy: undefined})).toBe('{report:autoreporting:start}'); + expect(compute('{report:autoreporting:start}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: undefined})).toBe('{report:autoreporting:start}'); expect(compute('{report:autoreporting:end}', createMockContext(createMock({})))).toBe('{report:autoreporting:end}'); }); }); describe('computeWithMetadata()', () => { - const mockCtx: FormulaContext = {report: createMock({reportID: '1'}), policy: undefined}; + const mockCtx: FormulaContext = {getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '1'}), policy: undefined}; test('flags REPORT parts that fall back to their raw definition as unresolved', () => { const result = computeWithMetadata('{report:autoreporting:end}', mockCtx); @@ -875,7 +890,7 @@ describe('CustomFormula', () => { test('returns null when the formula leaves any tokenized part unresolved', () => { // `{report:total:EUR}` on a USD report with no conversion falls back to the raw token → wrapper must return null. - expect(ReportUtils.computeOptimisticReportName(usdReport, groupPolicy, 'p-1', {})).toBeNull(); + expect(ReportUtils.computeOptimisticReportName(usdReport, groupPolicy, 'p-1', {}, getCurrencyDecimalsLocal)).toBeNull(); }); test('resolves TRIP autoreporting formula to today on empty-report create (Option B fallback, buildOptimisticEmptyReport path)', () => { @@ -887,14 +902,14 @@ describe('CustomFormula', () => { autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, fieldList: {[CONST.POLICY.FIELDS.FIELD_LIST_TITLE]: tripField}, }); - const result = ReportUtils.computeOptimisticReportName(usdReport, tripPolicy, 'p-1', {}); + const result = ReportUtils.computeOptimisticReportName(usdReport, tripPolicy, 'p-1', {}, getCurrencyDecimalsLocal); expect(result).not.toBeNull(); expect(result).toMatch(/^Trip from \w{3} \d{2} to \w{3} \d{2}, \d{4}$/); }); }); describe('User formula parts', () => { - const mockUserContext: FormulaContext = {report: createMock({reportID: '1'}), policy: undefined}; + const mockUserContext: FormulaContext = {getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '1'}), policy: undefined}; test('should resolve {user:email} to the current user email', () => { expect(compute('{user:email}', mockUserContext)).toBe('jane@example.com'); @@ -917,6 +932,7 @@ describe('CustomFormula', () => { test('should handle undefined amounts', () => { const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({total: undefined}), policy: null as unknown as Policy, }; @@ -927,6 +943,7 @@ describe('CustomFormula', () => { test('should handle missing report actions for created', () => { mockReportActionsUtils.getAllReportActions.mockReturnValue({}); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, }; @@ -938,6 +955,7 @@ describe('CustomFormula', () => { test('should handle missing transactions for startdate', () => { mockReportUtils.getReportTransactions.mockReturnValue([]); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, }; @@ -950,6 +968,7 @@ describe('CustomFormula', () => { test('should handle missing transactions for enddate', () => { mockReportUtils.getReportTransactions.mockReturnValue([]); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, }; @@ -961,6 +980,7 @@ describe('CustomFormula', () => { test('should call getReportTransactions with correct reportID for startdate', () => { const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: 'test-report-123'}), policy: null as unknown as Policy, }; @@ -971,6 +991,7 @@ describe('CustomFormula', () => { test('should call getAllReportActions with correct reportID for created', () => { const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: 'test-report-456'}), policy: null as unknown as Policy, }; @@ -1003,6 +1024,7 @@ describe('CustomFormula', () => { mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: 'test-report-123'}), policy: null as unknown as Policy, }; @@ -1029,6 +1051,7 @@ describe('CustomFormula', () => { mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: 'test-report-123'}), policy: undefined, }; @@ -1064,6 +1087,7 @@ describe('CustomFormula', () => { mockReportUtils.getReportTransactions.mockReturnValue(mockTransactions); const context: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: 'test-report-123'}), policy: null as unknown as Policy, }; @@ -1082,6 +1106,7 @@ describe('CustomFormula', () => { const morningDate = '2025-01-08T09:05:02.123Z'; // 9:05:02 AM for leading zero tests const mockContextWithDate: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, }; @@ -1252,6 +1277,7 @@ describe('CustomFormula', () => { }; const mockContextWithSubmissionInfo: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({ reportID: '123', reportName: '', @@ -1340,6 +1366,7 @@ describe('CustomFormula', () => { test('name fields fall back to email when name missing', () => { const contextWithPartialDetails: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1355,6 +1382,7 @@ describe('CustomFormula', () => { test('customfield1 - return empty when employeeList missing', () => { const contextWithoutEmployeeList: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: createMock({ name: 'Test Policy', @@ -1367,6 +1395,7 @@ describe('CustomFormula', () => { test('customfield2 - return empty when employeeList missing', () => { const contextWithoutEmployeeList: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: createMock({ name: 'Test Policy', @@ -1379,6 +1408,7 @@ describe('CustomFormula', () => { test('customfield1 - return empty when user not in employeeList', () => { const contextWithDifferentEmployee: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: createMock({ name: 'Test Policy', @@ -1399,6 +1429,7 @@ describe('CustomFormula', () => { test('customfield1/customfield2 - return empty when glCodes disabled', () => { const contextWithGlCodesDisabled: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: createMock({ name: 'Test Policy', @@ -1455,6 +1486,7 @@ describe('CustomFormula', () => { test('firstname - fall back to email when manager name missing', () => { const contextWithPartialManagerDetails: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, managerPersonalDetails: createMock({ @@ -1468,6 +1500,7 @@ describe('CustomFormula', () => { test('fullname - fall back to email when manager displayName missing', () => { const contextWithPartialManagerDetails: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, managerPersonalDetails: createMock({ @@ -1536,6 +1569,7 @@ describe('CustomFormula', () => { describe('Edge cases', () => { test('empty email - return empty when email empty', () => { const contextWithEmptyEmail: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1549,6 +1583,7 @@ describe('CustomFormula', () => { test('empty email with name - return empty when name also empty', () => { const contextWithEmptyEmail: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1562,6 +1597,7 @@ describe('CustomFormula', () => { test('empty firstname - fallback to email when firstname is empty string', () => { const contextWithEmptyFirstName: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1576,6 +1612,7 @@ describe('CustomFormula', () => { test('empty lastname - fallback to email when lastname is empty string', () => { const contextWithEmptyLastName: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1590,6 +1627,7 @@ describe('CustomFormula', () => { test('empty displayName - fallback to email when displayName is empty string', () => { const contextWithEmptyDisplayName: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1604,6 +1642,7 @@ describe('CustomFormula', () => { test('empty email with frontpart - return empty for empty email modifier', () => { const contextWithEmptyEmail: FormulaContext = { + getCurrencyDecimals: getCurrencyDecimalsLocal, report: createMock({reportID: '123'}), policy: null as unknown as Policy, submitterPersonalDetails: createMock({ @@ -1677,7 +1716,7 @@ describe('CustomFormula', () => { }; const fieldValues = {b: 'value_from_b'}; - const result = compute('{field:B}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('{field:B}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('value_from_b'); }); @@ -1699,7 +1738,7 @@ describe('CustomFormula', () => { const fieldValues = {a: 'stale_value', b: 'current_value_b'}; // When computing {field:A}, it should recursively resolve {field:B} from A's defaultValue - const result = compute('{field:A}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('{field:A}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('current_value_b'); }); @@ -1727,7 +1766,7 @@ describe('CustomFormula', () => { const fieldValues = {a: 'stale_a', b: 'fresh_value_b', c: 'stale_c'}; // C references A, A references B - should get B's current value - const result = compute('{field:C}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('{field:C}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('fresh_value_b'); }); @@ -1742,7 +1781,7 @@ describe('CustomFormula', () => { }; const fieldValues = {name: 'John'}; - const result = compute('Hello {field:Name}!', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('Hello {field:Name}!', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('Hello John!'); }); @@ -1763,7 +1802,7 @@ describe('CustomFormula', () => { }; const fieldValues = {first: 'Hello', second: 'World'}; - const result = compute('{field:First} {field:Second}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('{field:First} {field:Second}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('Hello World'); }); @@ -1771,7 +1810,7 @@ describe('CustomFormula', () => { const fieldsByName = {}; const fieldValues = {}; - const result = compute('{field:Unknown}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('{field:Unknown}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('{field:Unknown}'); }); @@ -1786,7 +1825,7 @@ describe('CustomFormula', () => { }; const fieldValues = {simple: 'current_value'}; - const result = compute('{field:Simple}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); + const result = compute('{field:Simple}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues}); expect(result).toBe('current_value'); }); @@ -1801,9 +1840,9 @@ describe('CustomFormula', () => { }; const fieldValues = {myfield: 'test_value'}; - expect(compute('{field:MyField}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues})).toBe('test_value'); - expect(compute('{field:MYFIELD}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues})).toBe('test_value'); - expect(compute('{field:myfield}', {report: mockReport, policy: mockPolicy, fieldsByName, fieldValues})).toBe('test_value'); + expect(compute('{field:MyField}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues})).toBe('test_value'); + expect(compute('{field:MYFIELD}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues})).toBe('test_value'); + expect(compute('{field:myfield}', {getCurrencyDecimals: getCurrencyDecimalsLocal, report: mockReport, policy: mockPolicy, fieldsByName, fieldValues})).toBe('test_value'); }); }); @@ -1819,7 +1858,7 @@ describe('CustomFormula', () => { value: 'current_value', }); - const result = resolveReportFieldValue(field, mockReport, mockPolicy, {}, {}); + const result = resolveReportFieldValue(field, mockReport, mockPolicy, {}, {}, getCurrencyDecimalsLocal); expect(result).toBe('current_value'); }); @@ -1831,7 +1870,7 @@ describe('CustomFormula', () => { value: undefined, }); - const result = resolveReportFieldValue(field, mockReport, mockPolicy, {}, {}); + const result = resolveReportFieldValue(field, mockReport, mockPolicy, {}, {}, getCurrencyDecimalsLocal); expect(result).toBe('fallback_value'); }); @@ -1853,7 +1892,7 @@ describe('CustomFormula', () => { }; const fieldValues = {b: 'resolved_value'}; - const result = resolveReportFieldValue(field, mockReport, mockPolicy, fieldValues, fieldsByName); + const result = resolveReportFieldValue(field, mockReport, mockPolicy, fieldValues, fieldsByName, getCurrencyDecimalsLocal); expect(result).toBe('resolved_value'); }); @@ -1894,7 +1933,7 @@ describe('CustomFormula', () => { }; const fieldValues = {a: '', b: 'final_value'}; - const result = resolveReportFieldValue(field, mockReport, mockPolicy, fieldValues, fieldsByName); + const result = resolveReportFieldValue(field, mockReport, mockPolicy, fieldValues, fieldsByName, getCurrencyDecimalsLocal); expect(result).toBe('final_value'); }); @@ -1904,7 +1943,7 @@ describe('CustomFormula', () => { name: 'Empty', }); - const result = resolveReportFieldValue(field, mockReport, mockPolicy, {}, {}); + const result = resolveReportFieldValue(field, mockReport, mockPolicy, {}, {}, getCurrencyDecimalsLocal); expect(result).toBe(''); }); }); diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index d371c05eedce..db828f614634 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -104,6 +104,7 @@ describe('libs/NextStepUtils', () => { policy, '2025-03-31 13:23:11', [CONST.BETAS.ALL], + getCurrencyDecimalsLocal, ); const expectedResult: ReportNextStep = { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 779e6f91da5f..6771b6c3447e 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -12978,6 +12978,7 @@ describe('ReportUtils', () => { }; const createFormulaContext = (reportParam: Report, policyParam: Policy, reportTransactions: Record = {}): FormulaContext => ({ + getCurrencyDecimals: getCurrencyDecimalsLocal, report: reportParam, policy: policyParam, allTransactions: reportTransactions, @@ -18442,7 +18443,17 @@ describe('ReportUtils', () => { const timeOfCreation = DateUtils.getDBTime(); // Then the report name should be "New Report" - const optimisticReport = buildOptimisticEmptyReport(reportID, accountID, currentUserEmail, parentReport, parentReportActionID, policyWithEmptyFieldList, timeOfCreation, betas); + const optimisticReport = buildOptimisticEmptyReport( + reportID, + accountID, + currentUserEmail, + parentReport, + parentReportActionID, + policyWithEmptyFieldList, + timeOfCreation, + betas, + getCurrencyDecimalsLocal, + ); expect(optimisticReport.reportName).toBe(CONST.REPORT.DEFAULT_EXPENSE_REPORT_NAME); }); @@ -18471,7 +18482,17 @@ describe('ReportUtils', () => { const timeOfCreation = DateUtils.getDBTime(); // Then the report name should be "New Report" - const optimisticReport = buildOptimisticEmptyReport(reportID, accountID, currentUserEmail, parentReport, parentReportActionID, policyWithEmptyFieldList, timeOfCreation, betas); + const optimisticReport = buildOptimisticEmptyReport( + reportID, + accountID, + currentUserEmail, + parentReport, + parentReportActionID, + policyWithEmptyFieldList, + timeOfCreation, + betas, + getCurrencyDecimalsLocal, + ); expect(optimisticReport.reportName).toBe(CONST.REPORT.DEFAULT_EXPENSE_REPORT_NAME); }); }); From eec889e61dde99ed9d3f609abad72017e9a7d8d0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 10 Aug 2026 23:46:06 +0530 Subject: [PATCH 2/2] Fix ESLint --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fc786b7bdbe0..ee83fdd8b42c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -28,7 +28,6 @@ import type { BankAccountList, Beta, BillingGraceEndPeriod, - CurrencyList, IntroSelected, OnyxInputOrEntry, OutstandingReportsByPolicyIDDerivedValue,