From a13d2d2d531729cecbb22d879f5767451c618ef7 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 5 Jul 2026 13:39:07 +0530 Subject: [PATCH 1/4] Refactor: migrate getRate to use usePersonalPolicy hook (part 4) --- src/hooks/useTransactionInlineEdit.ts | 4 +- src/libs/TransactionUtils/index.ts | 28 +- src/libs/actions/IOU/BulkEdit.ts | 3 + src/libs/actions/IOU/Split.ts | 2 + src/libs/actions/IOU/UpdateMoneyRequest.ts | 17 + src/libs/actions/TransactionInlineEdit.ts | 3 +- .../SearchEditMultiplePage.tsx | 3 + .../iou/request/step/IOURequestStepDate.tsx | 1 + .../request/step/IOURequestStepDistance.tsx | 7 +- .../step/IOURequestStepDistanceManual.tsx | 3 +- .../step/IOURequestStepDistanceMap.tsx | 4 +- .../step/IOURequestStepDistanceOdometer.tsx | 2 + .../step/IOURequestStepDistanceRate.tsx | 5 +- .../actions/IOUTest/UpdateMoneyRequestTest.ts | 11 + tests/unit/TransactionUtilsTest.ts | 607 +++++++++++++++--- 15 files changed, 597 insertions(+), 103 deletions(-) diff --git a/src/hooks/useTransactionInlineEdit.ts b/src/hooks/useTransactionInlineEdit.ts index 6369c63fd50e..2df084bfa3e3 100644 --- a/src/hooks/useTransactionInlineEdit.ts +++ b/src/hooks/useTransactionInlineEdit.ts @@ -35,6 +35,7 @@ import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import useDistanceRateOriginalPolicy from './useDistanceRateOriginalPolicy'; import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; +import usePersonalPolicy from './usePersonalPolicy'; import usePolicyForMovingExpenses from './usePolicyForMovingExpenses'; import usePolicyForTransaction from './usePolicyForTransaction'; import useSelfDMReport from './useSelfDMReport'; @@ -146,6 +147,7 @@ function useTransactionInlineEdit({transactionID, hash, linkedReportAction}: Use const distanceOriginalPolicy = useDistanceRateOriginalPolicy(customUnitRateID, shouldLookupDistancePolicy); const {isOffline} = useNetwork(); + const personalPolicy = usePersonalPolicy(); const permissions = getTransactionEditPermissions({ transaction, @@ -186,7 +188,7 @@ function useTransactionInlineEdit({transactionID, hash, linkedReportAction}: Use }; const onEditDate = (newDate: string) => { - editTransactionDateInline(getEditParams(), newDate); + editTransactionDateInline(getEditParams(), newDate, personalPolicy?.outputCurrency); }; const onEditMerchant = (newMerchant: string) => { diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 89cd20e19bcb..ecce8299376c 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -609,6 +609,7 @@ function getUpdatedTransaction({ shouldUpdateReceiptState = true, policy = undefined, isSplitTransaction = false, + personalPolicyOutputCurrency, }: { transaction: Transaction; transactionChanges: TransactionChanges; @@ -616,6 +617,7 @@ function getUpdatedTransaction({ shouldUpdateReceiptState?: boolean; policy?: OnyxEntry; isSplitTransaction?: boolean; + personalPolicyOutputCurrency: string | undefined; }): Transaction { const isUnReportedExpense = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; @@ -666,7 +668,11 @@ function getUpdatedTransaction({ updatedTransaction.modifiedAmount = CONST.IOU.DEFAULT_AMOUNT; updatedTransaction.modifiedMerchant = translateLocal('iou.fieldPending'); } else if (transactionChanges.routes?.route0?.geometry?.coordinates) { - const mileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy}); + const mileageRate = DistanceRequestUtils.getRate({ + transaction: updatedTransaction, + policy, + personalPolicyOutputCurrency, + }); const {unit, rate} = mileageRate; // Use route distance directly since waypoints changed and the route was recalculated. @@ -715,7 +721,11 @@ function getUpdatedTransaction({ const existingDistanceUnit = transaction?.comment?.customUnit?.distanceUnit; // Get the new distance unit from the rate's unit - const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({transaction: updatedTransaction, policy}); + const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({ + transaction: updatedTransaction, + policy, + personalPolicyOutputCurrency, + }); lodashSet(updatedTransaction, 'comment.customUnit.distanceUnit', newDistanceUnit); // If the distanceUnit is set and the rate is changed to one that has a different unit, convert the distance to the new unit. @@ -730,7 +740,12 @@ function getUpdatedTransaction({ // When the waypoints are being fetched from the server, we have no information about the distance, and cannot recalculate the updated amount. // Otherwise, recalculate the fields based on the new rate. - const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false}); + const updatedMileageRate = DistanceRequestUtils.getRate({ + transaction: updatedTransaction, + policy, + useTransactionDistanceUnit: false, + personalPolicyOutputCurrency, + }); const {unit, rate} = updatedMileageRate; const distanceInMeters = getDistanceInMeters(updatedTransaction, unit); @@ -818,7 +833,12 @@ function getUpdatedTransaction({ lodashSet(updatedTransaction, 'routes.route0.distance', null); shouldStopSmartscan = true; - const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false}); + const updatedMileageRate = DistanceRequestUtils.getRate({ + transaction: updatedTransaction, + policy, + useTransactionDistanceUnit: false, + personalPolicyOutputCurrency, + }); const {unit, rate} = updatedMileageRate; // Sync the stored distanceUnit to the policy's current unit. The user's input on the Manual diff --git a/src/libs/actions/IOU/BulkEdit.ts b/src/libs/actions/IOU/BulkEdit.ts index 814d3974c70e..1705c6bd7d44 100644 --- a/src/libs/actions/IOU/BulkEdit.ts +++ b/src/libs/actions/IOU/BulkEdit.ts @@ -96,6 +96,7 @@ type UpdateMultipleMoneyRequestsParams = { allPolicies?: OnyxCollection; currentUserAccountID: number; delegateAccountID: number | undefined; + personalPolicyOutputCurrency?: string; }; function updateMultipleMoneyRequests({ @@ -112,6 +113,7 @@ function updateMultipleMoneyRequests({ allPolicies, currentUserAccountID, delegateAccountID, + personalPolicyOutputCurrency, }: UpdateMultipleMoneyRequestsParams) { // Track running totals per report so multiple edits in the same report compound correctly. const optimisticReportsByID: Record = {}; @@ -347,6 +349,7 @@ function updateMultipleMoneyRequests({ transactionChanges, isFromExpenseReport, policy: transactionPolicy, + personalPolicyOutputCurrency, }); const isTransactionOnHold = isOnHold(transaction); diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index 6114823272b8..66d623dbfb67 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -1247,6 +1247,7 @@ function setDraftSplitTransaction( splitTransactionDraft: OnyxEntry, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry, + personalPolicyOutputCurrency?: string, ) { if (!transactionID) { return undefined; @@ -1265,6 +1266,7 @@ function setDraftSplitTransaction( shouldUpdateReceiptState: false, policy, isSplitTransaction: true, + personalPolicyOutputCurrency, }) : null; diff --git a/src/libs/actions/IOU/UpdateMoneyRequest.ts b/src/libs/actions/IOU/UpdateMoneyRequest.ts index 4f1fc1e8a701..160ee875d6f0 100644 --- a/src/libs/actions/IOU/UpdateMoneyRequest.ts +++ b/src/libs/actions/IOU/UpdateMoneyRequest.ts @@ -83,6 +83,7 @@ type UpdateMoneyRequestDateParams = { delegateAccountID: number | undefined; policyForTrackExpense?: OnyxEntry; distanceOriginalPolicy?: OnyxEntry; + personalPolicyOutputCurrency: string | undefined; }; type SearchSnapshotOnyxData = { @@ -228,6 +229,7 @@ function updateMoneyRequestDate({ delegateAccountID, policyForTrackExpense, distanceOriginalPolicy, + personalPolicyOutputCurrency, }: UpdateMoneyRequestDateParams) { const transaction = getAllTransactions()[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const isTrackExpense = isTrackExpenseReport(transactionThreadReport) && isSelfDM(parentReport); @@ -270,6 +272,7 @@ function updateMoneyRequestDate({ updatedTaxValue: distanceRateTaxUpdates?.taxValue, distanceOriginalPolicy, currentTransactionViolations, + personalPolicyOutputCurrency, }); return; } @@ -913,6 +916,7 @@ type UpdateMoneyRequestDistanceParams = { parentReportNextStep: OnyxEntry; distanceOriginalPolicy?: OnyxEntry; delegateAccountID: number | undefined; + personalPolicyOutputCurrency: string | undefined; }; /** Updates the waypoints of a distance expense */ @@ -936,6 +940,7 @@ function updateMoneyRequestDistance({ parentReportNextStep, distanceOriginalPolicy, delegateAccountID, + personalPolicyOutputCurrency, }: UpdateMoneyRequestDistanceParams) { const transactionChanges: TransactionChanges = { // Don't sanitize waypoints here - keep all fields for Onyx optimistic data (e.g., keyForList) @@ -961,6 +966,8 @@ function updateMoneyRequestDistance({ undefined, undefined, distanceOriginalPolicy, + undefined, + personalPolicyOutputCurrency, ); } else { data = getUpdateMoneyRequestParams({ @@ -978,6 +985,7 @@ function updateMoneyRequestDistance({ isASAPSubmitBetaEnabled, iouReportNextStep: parentReportNextStep, delegateAccountID, + personalPolicyOutputCurrency, }); } const {params, onyxData} = data; @@ -1176,6 +1184,7 @@ function updateMoneyRequestDistanceRate({ shouldBuildOptimisticModifiedExpenseReportAction = true, distanceOriginalPolicy, currentTransactionViolations: currentTransactionViolationsParam, + personalPolicyOutputCurrency, }: { transaction: OnyxEntry; transactionThreadReport: OnyxEntry; @@ -1200,6 +1209,7 @@ function updateMoneyRequestDistanceRate({ shouldBuildOptimisticModifiedExpenseReportAction?: boolean; distanceOriginalPolicy?: OnyxEntry; currentTransactionViolations?: OnyxEntry; + personalPolicyOutputCurrency: string | undefined; }) { const transactionChanges: TransactionChanges = { customUnitRateID: rateID, @@ -1238,6 +1248,7 @@ function updateMoneyRequestDistanceRate({ shouldBuildOptimisticModifiedExpenseReportAction, distanceOriginalPolicy, currentTransactionViolations, + personalPolicyOutputCurrency, ); } else { data = getUpdateMoneyRequestParams({ @@ -1260,6 +1271,7 @@ function updateMoneyRequestDistanceRate({ shouldBuildOptimisticModifiedExpenseReportAction, distanceOriginalPolicy, violations: currentTransactionViolations, + personalPolicyOutputCurrency, }); if (created && transaction?.transactionID && transactions && transactionViolations) { removeTransactionFromDuplicateTransactionViolation(data.onyxData, transaction.transactionID, transactions, transactionViolations); @@ -1385,6 +1397,7 @@ type GetUpdateMoneyRequestParamsType = { isOffline?: boolean; delegateAccountID: number | undefined; distanceOriginalPolicy?: OnyxEntry; + personalPolicyOutputCurrency?: string; }; type UpdateMoneyRequestDataKeys = @@ -1427,6 +1440,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U isOffline, delegateAccountID, distanceOriginalPolicy, + personalPolicyOutputCurrency, } = params; const optimisticData: Array< OnyxUpdate< @@ -1513,6 +1527,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U isFromExpenseReport, isSplitTransaction, policy, + personalPolicyOutputCurrency, }) : undefined; @@ -2081,6 +2096,7 @@ function getUpdateTrackExpenseParams( shouldBuildOptimisticModifiedExpenseReportAction = true, distanceOriginalPolicy?: OnyxEntry, currentTransactionViolations?: OnyxEntry, + personalPolicyOutputCurrency?: string, ): UpdateMoneyRequestData< | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.TRANSACTION @@ -2127,6 +2143,7 @@ function getUpdateTrackExpenseParams( transactionChanges, isFromExpenseReport: false, policy: policyForTransaction, + personalPolicyOutputCurrency, }) : null; const transactionDetails = getTransactionDetails(updatedTransaction); diff --git a/src/libs/actions/TransactionInlineEdit.ts b/src/libs/actions/TransactionInlineEdit.ts index 6d0761bd14be..f71ad2619604 100644 --- a/src/libs/actions/TransactionInlineEdit.ts +++ b/src/libs/actions/TransactionInlineEdit.ts @@ -290,7 +290,7 @@ function getIouParamsForTransaction({ } /** Updates the date of an expense from the Search results table or the Expense Report page. */ -function editTransactionDateInline(params: TransactionInlineEditParams, newDate: string) { +function editTransactionDateInline(params: TransactionInlineEditParams, newDate: string, personalPolicyOutputCurrency: string | undefined) { const iouParams = getIouParamsForTransaction(params); updateMoneyRequestDate({ ...iouParams, @@ -302,6 +302,7 @@ function editTransactionDateInline(params: TransactionInlineEditParams, newDate: isOffline: params.isOffline, hash: params.hash, distanceOriginalPolicy: params.distanceOriginalPolicy, + personalPolicyOutputCurrency, }); } diff --git a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx index 73e25cf9f8d2..c4559f309c01 100644 --- a/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx +++ b/src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx @@ -11,6 +11,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearBulkEditDraftTransaction, updateMultipleMoneyRequests} from '@libs/actions/IOU/BulkEdit'; @@ -51,6 +52,7 @@ function SearchEditMultiplePage() { const {currentSearchResults} = useSearchResultsContext(); const {clearSelectedTransactions} = useSearchSelectionActions(); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const personalPolicy = usePersonalPolicy(); const delegateAccountID = useDelegateAccountID(); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); @@ -192,6 +194,7 @@ function SearchEditMultiplePage() { allPolicies: policies, currentUserAccountID, delegateAccountID, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); // Bulk edit can start from report (ID-based selection) or search (map-based selection), // so clear both stores to keep deselection behavior consistent. diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/IOURequestStepDate.tsx index d12a4841dc40..b81708c74a14 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDate.tsx @@ -136,6 +136,7 @@ function IOURequestStepDate({ isOffline, delegateAccountID, distanceOriginalPolicy, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); } else { setMoneyRequestCreated(transactionID, newCreated, isTransactionDraft, hasReceipt(transaction)); diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 077c493a9494..b4dd1ba760ef 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -462,6 +462,7 @@ function IOURequestStepDistance({ originalSplitTransactionDraft, {waypoints: currentTransaction?.comment?.waypoints, routes: currentTransaction?.routes}, policy, + personalPolicy?.outputCurrency, ); navigateBackAfterSave(); return; @@ -499,6 +500,7 @@ function IOURequestStepDistance({ parentReportNextStep, delegateAccountID, distanceOriginalPolicy, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); } transactionWasSaved.current = true; @@ -543,6 +545,7 @@ function IOURequestStepDistance({ parentReportNextStep, delegateAccountID, distanceOriginalPolicy, + personalPolicy?.outputCurrency, ]); const submitManualDistance = useCallback(() => { @@ -572,7 +575,7 @@ function IOURequestStepDistance({ if (isEditingSplit && transaction) { setMoneyRequestDistance(transactionID, distanceAsFloat, shouldUseTransactionDraft(action, iouType), distanceUnit); - setDraftSplitTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID, splitDraftTransaction, {distance: distanceAsFloat}, policy); + setDraftSplitTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID, splitDraftTransaction, {distance: distanceAsFloat}, policy, personalPolicy?.outputCurrency); navigateBackAfterSave(); return; } @@ -610,6 +613,7 @@ function IOURequestStepDistance({ delegateAccountID, recentWaypoints, distanceOriginalPolicy, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); transactionWasSaved.current = true; // Remove the backup eagerly so the parent report view reads the optimistic transaction @@ -646,6 +650,7 @@ function IOURequestStepDistance({ hasRouteError, delegateAccountID, distanceOriginalPolicy, + personalPolicy?.outputCurrency, ]); const renderItem = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx b/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx index c6f5c9012585..1605a46e8768 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx @@ -204,7 +204,7 @@ function IOURequestStepDistanceManual({ if (action === CONST.IOU.ACTION.EDIT) { // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplit && transaction) { - setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {distance: distanceAsFloat}, policy); + setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {distance: distanceAsFloat}, policy, personalPolicy?.outputCurrency); Navigation.goBack(backTo); return; } @@ -232,6 +232,7 @@ function IOURequestStepDistanceManual({ parentReportNextStep, recentWaypoints, delegateAccountID, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); } Navigation.goBack(backTo); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx b/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx index b5fa2b08dc3a..9104e9591373 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx @@ -336,7 +336,7 @@ function IOURequestStepDistanceMap({ if (isEditing) { // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplit && transaction) { - setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {waypoints}, policy); + setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {waypoints}, policy, personalPolicy?.outputCurrency); navigateBack(); return; } @@ -366,6 +366,7 @@ function IOURequestStepDistanceMap({ isASAPSubmitBetaEnabled, parentReportNextStep, delegateAccountID, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); } transactionWasSaved.current = true; @@ -403,6 +404,7 @@ function IOURequestStepDistanceMap({ recentWaypoints, distanceOriginalPolicy, delegateAccountID, + personalPolicy?.outputCurrency, ]); const renderItem = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx index e258ae103a5f..1fd3a9e7fd8d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx @@ -377,6 +377,7 @@ function IOURequestStepDistanceOdometer({ odometerEnd: end, }, policy, + personalPolicy?.outputCurrency, ); Navigation.goBack(); return; @@ -409,6 +410,7 @@ function IOURequestStepDistanceOdometer({ parentReportNextStep, recentWaypoints, delegateAccountID, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); } Navigation.goBack(); diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 8fd4b0c483bf..6f98462ca53b 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -10,6 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -72,6 +73,7 @@ function IOURequestStepDistanceRate({ const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const personalPolicy = usePersonalPolicy(); const [currentTransactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getNonEmptyStringOnyxID(transaction?.transactionID)}`); const {policy: policyForTransaction} = usePolicyForTransaction({transaction, reportPolicyID: report?.policyID, action, iouType, policyDraft}); @@ -197,7 +199,7 @@ function IOURequestStepDistanceRate({ if (currentRateID !== customUnitRateID || (isMovingTransactionFromTrackExpense && transactionUnit !== selectedRateUnit)) { // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplit && transaction) { - setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {customUnitRateID}, policy); + setDraftSplitTransaction(transaction.transactionID, splitDraftTransaction, {customUnitRateID}, policy, personalPolicy?.outputCurrency); navigateBack(); return; } @@ -223,6 +225,7 @@ function IOURequestStepDistanceRate({ delegateAccountID, isOffline, currentTransactionViolations, + personalPolicyOutputCurrency: personalPolicy?.outputCurrency, }); } else { setMoneyRequestDistanceRate(transaction, customUnitRateID, policy, shouldUseTransactionDraft(action)); diff --git a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts index d6a49b3c5d9c..297b29fb1a42 100644 --- a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts +++ b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts @@ -903,6 +903,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { // When updating the date updateMoneyRequestDate({ + personalPolicyOutputCurrency: undefined, transactionID, transactionThreadReport, parentReport, @@ -1082,6 +1083,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { // When updating the money request with distance and waypoints updateMoneyRequestDistance({ + personalPolicyOutputCurrency: undefined, transaction: fakeTransaction, transactionThreadReport, parentReport, @@ -1174,6 +1176,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { // When updating the money request WITHOUT distance (only waypoints) updateMoneyRequestDistance({ + personalPolicyOutputCurrency: undefined, transaction: fakeTransaction, transactionThreadReport, parentReport, @@ -1290,6 +1293,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { // First update: Add more waypoints to the expense updateMoneyRequestDistance({ + personalPolicyOutputCurrency: undefined, transaction: fakeTransaction, transactionThreadReport, parentReport, @@ -1364,6 +1368,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { // Call with empty waypoints - should not crash updateMoneyRequestDistance({ + personalPolicyOutputCurrency: undefined, transaction: fakeTransaction, transactionThreadReport, parentReport, @@ -1409,6 +1414,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID2}`, fakeTransaction2); updateMoneyRequestDistance({ + personalPolicyOutputCurrency: undefined, transaction: fakeTransaction2, transactionThreadReport, parentReport, @@ -1990,6 +1996,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await waitForBatchedUpdates(); updateMoneyRequestDate({ + personalPolicyOutputCurrency: undefined, transactionID, transactionThreadReport: transactionThread, parentReport: expenseReport, @@ -2116,6 +2123,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await waitForBatchedUpdates(); updateMoneyRequestDate({ + personalPolicyOutputCurrency: undefined, transactionID, transactionThreadReport: transactionThread, parentReport: expenseReport, @@ -2211,6 +2219,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await waitForBatchedUpdates(); updateMoneyRequestDate({ + personalPolicyOutputCurrency: undefined, transactionID, transactionThreadReport: transactionThread, parentReport: expenseReport, @@ -2327,6 +2336,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await waitForBatchedUpdates(); updateMoneyRequestDate({ + personalPolicyOutputCurrency: undefined, transactionID, transactionThreadReport: transactionThread, parentReport: expenseReport, @@ -2444,6 +2454,7 @@ describe('actions/IOU/UpdateMoneyRequest', () => { await waitForBatchedUpdates(); updateMoneyRequestDate({ + personalPolicyOutputCurrency: undefined, transactionID, transactionThreadReport: transactionThread, parentReport: selfDMReport, diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index dd3beb2ef126..b5f91e0968eb 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -100,7 +100,10 @@ describe('TransactionUtils', () => { Onyx.init({ keys: ONYXKEYS, initialKeyStates: { - [ONYXKEYS.SESSION]: {accountID: CURRENT_USER_ID, email: CURRENT_USER_EMAIL}, + [ONYXKEYS.SESSION]: { + accountID: CURRENT_USER_ID, + email: CURRENT_USER_EMAIL, + }, [ONYXKEYS.PERSONAL_DETAILS_LIST]: { [CURRENT_USER_ID]: { accountID: CURRENT_USER_ID, @@ -261,15 +264,28 @@ describe('TransactionUtils', () => { describe('getIsFromGlobalCreate', () => { it('returns true when isFromFloatingActionButton is true', () => { - expect(TransactionUtils.getIsFromGlobalCreate({isFromFloatingActionButton: true} as Transaction)).toBe(true); + expect( + TransactionUtils.getIsFromGlobalCreate({ + isFromFloatingActionButton: true, + } as Transaction), + ).toBe(true); }); it('returns false when isFromFloatingActionButton is explicitly false (FAB takes precedence over isFromGlobalCreate via ?? semantics)', () => { - expect(TransactionUtils.getIsFromGlobalCreate({isFromFloatingActionButton: false, isFromGlobalCreate: true} as Transaction)).toBe(false); + expect( + TransactionUtils.getIsFromGlobalCreate({ + isFromFloatingActionButton: false, + isFromGlobalCreate: true, + } as Transaction), + ).toBe(false); }); it('falls back to isFromGlobalCreate when isFromFloatingActionButton is undefined', () => { - expect(TransactionUtils.getIsFromGlobalCreate({isFromGlobalCreate: true} as Transaction)).toBe(true); + expect( + TransactionUtils.getIsFromGlobalCreate({ + isFromGlobalCreate: true, + } as Transaction), + ).toBe(true); }); it('returns undefined when both flags are absent', () => { @@ -288,7 +304,9 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, + rules: { + expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1'), + }, }; const transaction = generateTransaction(); @@ -308,7 +326,9 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, + rules: { + expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1'), + }, }; const transaction = generateTransaction(); @@ -340,7 +360,9 @@ describe('TransactionUtils', () => { }, }, outputCurrency: 'IDR', - rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, + rules: { + expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1'), + }, }; const transaction = generateTransaction(); @@ -360,7 +382,9 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, + rules: { + expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1'), + }, }; const transaction: Transaction = { ...generateTransaction(), @@ -405,7 +429,9 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: {expenseRules: createCategoryTaxExpenseRules(category, taxCode)}, + rules: { + expenseRules: createCategoryTaxExpenseRules(category, taxCode), + }, }; const transaction = generateTransaction(); @@ -415,6 +441,7 @@ describe('TransactionUtils', () => { isFromExpenseReport: true, policy: fakePolicy, transactionChanges: {category}, + personalPolicyOutputCurrency: undefined, }); // Then the updated transaction should contain the tax from the matched rule @@ -463,6 +490,7 @@ describe('TransactionUtils', () => { isFromExpenseReport: false, policy: fakePolicy, transactionChanges: {distance: newDistance}, + personalPolicyOutputCurrency: undefined, }); // Then: quantity should be updated @@ -485,6 +513,50 @@ describe('TransactionUtils', () => { }); }); + it('threads personalPolicyOutputCurrency into the recalculated rate for a P2P distance expense with no policy', async () => { + // A P2P distance expense (FAKE_P2P_ID) has no policy rate, so the mileage currency comes from the + // resolved personal-policy currency that getUpdatedTransaction forwards to getRate. getRateForP2P only + // returns that resolved currency when the default P2P rate is loaded, so seed it here. + await Onyx.merge(ONYXKEYS.DEFAULT_P2P_MILEAGE_RATE, { + rate: 30, + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + }); + await waitForBatchedUpdates(); + + try { + // transaction.currency is deliberately USD so a passing result proves the EUR came from + // personalPolicyOutputCurrency, not from the transaction itself. + const transaction = generateTransaction({ + currency: CONST.CURRENCY.USD, + comment: { + customUnit: { + customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, + distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + quantity: 10, + }, + }, + }); + + const updatedTransaction = TransactionUtils.getUpdatedTransaction({ + transaction, + isFromExpenseReport: false, + policy: undefined, + transactionChanges: {distance: 20}, + personalPolicyOutputCurrency: 'EUR', + }); + + // Currency + merchant follow the threaded personal currency (EUR), and amount = 20 mi × 30¢ = 600. + // (The merchant shows the "EUR" code rather than the € glyph because the currency list isn't loaded in this unit test.) + expect(updatedTransaction.modifiedCurrency).toBe('EUR'); + expect(updatedTransaction.modifiedMerchant).toContain('EUR'); + expect(updatedTransaction.modifiedMerchant).toContain('0.30'); + expect(updatedTransaction.modifiedAmount).toBe(600); + } finally { + await Onyx.merge(ONYXKEYS.DEFAULT_P2P_MILEAGE_RATE, null); + await waitForBatchedUpdates(); + } + }); + it('should negate modifiedAmount when isFromExpenseReport is true', () => { const transaction = generateTransaction(); const newAmount = 500; @@ -493,6 +565,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: true, transactionChanges: {amount: newAmount}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.modifiedAmount).toBe(-newAmount); @@ -506,6 +579,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: false, transactionChanges: {amount: newAmount}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.modifiedAmount).toBe(newAmount); @@ -517,7 +591,12 @@ describe('TransactionUtils', () => { const updatedTransaction = TransactionUtils.getUpdatedTransaction({ transaction, isFromExpenseReport: true, - transactionChanges: {taxCode: 'id_TAX_RATE_1', taxAmount: 50, taxValue: '5%'}, + transactionChanges: { + taxCode: 'id_TAX_RATE_1', + taxAmount: 50, + taxValue: '5%', + }, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.taxValue).toBe('5%'); @@ -532,6 +611,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: false, transactionChanges: {taxValue: '5%'}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.taxValue).toBe('10%'); @@ -647,32 +727,47 @@ describe('TransactionUtils', () => { }); it('returns false for a non-distance expense', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns false for an odometer distance expense (no map), even if it somehow carries waypoints', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, comment: {waypoints}}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, + comment: {waypoints}, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns false for a pure manual distance expense with no waypoints', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, comment: {}}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, + comment: {}, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns true for a manual distance expense that carries waypoints (e.g. merged from a map expense)', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, comment: {waypoints}}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, + comment: {waypoints}, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a map distance expense', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a GPS distance expense', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); }); @@ -722,7 +817,12 @@ describe('TransactionUtils', () => { describe('shouldShowBrokenConnectionViolation', () => { it('should return false when no broken connection violations are found for the provided transaction', () => { - const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}]; + const transactionViolations = [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }, + ]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(undefined, undefined, transactionViolations); expect(showBrokenConnectionViolation).toBe(false); @@ -730,7 +830,13 @@ describe('TransactionUtils', () => { it('should return true when a broken connection violation exists for one transaction and the user is the policy member', () => { const policy = {role: CONST.POLICY.ROLE.USER} as Policy; - const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; + const transactionViolations = [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.RTER, + data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, + }, + ]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(undefined, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); @@ -749,7 +855,9 @@ describe('TransactionUtils', () => { { type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, - data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, + data: { + rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION, + }, }, ], }; @@ -768,7 +876,13 @@ describe('TransactionUtils', () => { it('should return true when a broken connection violation exists and the user is the policy admin and the expense submitter', () => { const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; const report = processingReport; - const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; + const transactionViolations = [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.RTER, + data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, + }, + ]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); @@ -777,16 +891,32 @@ describe('TransactionUtils', () => { it('should return true when a broken connection violation exists, the user is the policy admin and the expense report is in the open state', () => { const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; const report = secondUserOpenReport; - const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; + const transactionViolations = [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.RTER, + data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, + }, + ]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); }); it('should return true when a broken connection violation exists, the user is the policy admin, the expense report is in the processing state and instant submit is enabled', () => { - const policy = {role: CONST.POLICY.ROLE.ADMIN, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT} as Policy; + const policy = { + role: CONST.POLICY.ROLE.ADMIN, + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + } as Policy; const report = processingReport; - const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; + const transactionViolations = [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.RTER, + data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, + }, + ]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); @@ -795,7 +925,13 @@ describe('TransactionUtils', () => { it('should return false when a broken connection violation exists, the user is the policy admin but the expense report is in the approved state', () => { const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; const report = approvedReport; - const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; + const transactionViolations = [ + { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.RTER, + data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, + }, + ]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(false); @@ -806,7 +942,13 @@ describe('TransactionUtils', () => { it('returns true for empty, default, and partial merchant values', () => { expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: ''}))).toBe(true); expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: CONST.TRANSACTION.DEFAULT_MERCHANT}))).toBe(true); - expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}))).toBe(true); + expect( + TransactionUtils.isMerchantMissing( + generateTransaction({ + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + }), + ), + ).toBe(true); }); it('returns false for a valid merchant', () => { @@ -886,24 +1028,34 @@ describe('TransactionUtils', () => { }); it('should normalize the DEFAULT_MERCHANT ("Expense") sentinel to an empty string', () => { - const transaction = generateTransaction({merchant: CONST.TRANSACTION.DEFAULT_MERCHANT}); + const transaction = generateTransaction({ + merchant: CONST.TRANSACTION.DEFAULT_MERCHANT, + }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe(''); }); it('should normalize the PARTIAL_TRANSACTION_MERCHANT ("(none)") sentinel to an empty string', () => { - const transaction = generateTransaction({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}); + const transaction = generateTransaction({ + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe(''); }); it('should prefer modifiedMerchant over merchant', () => { - const transaction = generateTransaction({merchant: 'Original', modifiedMerchant: 'Modified'}); + const transaction = generateTransaction({ + merchant: 'Original', + modifiedMerchant: 'Modified', + }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe('Modified'); }); it('should return the localized scanning label while a receipt is scanning', () => { const transaction = generateTransaction({ merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'receipt.jpg'}, + receipt: { + state: CONST.IOU.RECEIPT_STATE.SCANNING, + source: 'receipt.jpg', + }, }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe('iou.receiptStatusTitle'); }); @@ -911,7 +1063,10 @@ describe('TransactionUtils', () => { it('should return an empty string for a receipt whose scan failed', () => { const transaction = generateTransaction({ merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, source: 'receipt.jpg'}, + receipt: { + state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, + source: 'receipt.jpg', + }, }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe(''); }); @@ -1041,7 +1196,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When checking if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); @@ -1055,7 +1213,10 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ comment: {}, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When checking if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); @@ -1075,7 +1236,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When checking if violation is dismissed for current user const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); @@ -1104,7 +1268,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When current user (admin, not the owner) checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, iouReport, undefined); @@ -1131,7 +1298,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When current user (admin, not the owner) checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, iouReport, undefined); @@ -1158,7 +1328,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When current user (the submitter) checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, iouReport, undefined); @@ -1187,7 +1360,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; + const violation = { + type: CONST.VIOLATION_TYPES.WARNING, + name: CONST.VIOLATIONS.RTER, + }; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1214,7 +1390,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; + const violation = { + type: CONST.VIOLATION_TYPES.WARNING, + name: CONST.VIOLATIONS.RTER, + }; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1241,7 +1420,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.WARNING, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1269,7 +1451,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; + const violation = { + type: CONST.VIOLATION_TYPES.WARNING, + name: CONST.VIOLATIONS.RTER, + }; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1281,7 +1466,10 @@ describe('TransactionUtils', () => { describe('Edge cases and data validation', () => { it('should return false when transaction is null', () => { - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; const result = TransactionUtils.isViolationDismissed(undefined, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); expect(result).toBe(false); }); @@ -1302,7 +1490,10 @@ describe('TransactionUtils', () => { }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const violation = { + type: CONST.VIOLATION_TYPES.VIOLATION, + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + }; const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); expect(result).toBe(false); }); @@ -1330,13 +1521,19 @@ describe('TransactionUtils', () => { describe('getReportOwnerAsAttendee', () => { it('should return undefined when details has no login and display name', () => { - const result = TransactionUtils.getReportOwnerAsAttendee({accountID: 1}); + const result = TransactionUtils.getReportOwnerAsAttendee({ + accountID: 1, + }); expect(result).toBeUndefined(); }); it('should return report owner as attendee for reported expense', () => { const avatar = 'test'; - const result = TransactionUtils.getReportOwnerAsAttendee({accountID: SECOND_USER_ID, login: OTHER_USER_EMAIL, avatar}); + const result = TransactionUtils.getReportOwnerAsAttendee({ + accountID: SECOND_USER_ID, + login: OTHER_USER_EMAIL, + avatar, + }); expect(result).toBeDefined(); expect(result?.email).toBe(OTHER_USER_EMAIL); @@ -1450,7 +1647,12 @@ describe('TransactionUtils', () => { }, }); - const result = TransactionUtils.getOriginalAttendees(transaction, {accountID: CURRENT_USER_ID, displayName: '', avatarUrl: '', selected: true}); + const result = TransactionUtils.getOriginalAttendees(transaction, { + accountID: CURRENT_USER_ID, + displayName: '', + avatarUrl: '', + selected: true, + }); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1461,19 +1663,45 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { - attendees: [{displayName: ' ', login: ' login-only@example.com ', avatarUrl: ''}], + attendees: [ + { + displayName: ' ', + login: ' login-only@example.com ', + avatarUrl: '', + }, + ], }, }); const result = TransactionUtils.getOriginalAttendees(transaction, undefined); - expect(result).toEqual([{displayName: 'login-only@example.com', login: 'login-only@example.com', avatarUrl: ''}]); + expect(result).toEqual([ + { + displayName: 'login-only@example.com', + login: 'login-only@example.com', + avatarUrl: '', + }, + ]); }); it('should handle attendees stored as a plain object', () => { const attendeesArray: Attendee[] = [ - {email: 'attendee1@example.com', login: 'attendee1@example.com', displayName: 'Attendee One', avatarUrl: '', accountID: 3, selected: true}, - {email: 'attendee2@example.com', login: 'attendee2@example.com', displayName: 'Attendee Two', avatarUrl: '', accountID: 4, selected: false}, + { + email: 'attendee1@example.com', + login: 'attendee1@example.com', + displayName: 'Attendee One', + avatarUrl: '', + accountID: 3, + selected: true, + }, + { + email: 'attendee2@example.com', + login: 'attendee2@example.com', + displayName: 'Attendee Two', + avatarUrl: '', + accountID: 4, + selected: false, + }, ]; const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, @@ -1558,7 +1786,12 @@ describe('TransactionUtils', () => { }, }); - const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: '', selected: true}); + const result = TransactionUtils.getAttendees(transaction, { + accountID: CURRENT_USER_ID, + avatarUrl: '', + displayName: '', + selected: true, + }); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1598,7 +1831,11 @@ describe('TransactionUtils', () => { modifiedAttendees: [], }); - const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: ''}); + const result = TransactionUtils.getAttendees(transaction, { + accountID: CURRENT_USER_ID, + avatarUrl: '', + displayName: '', + }); // When modifiedAttendees is empty array and no report owner fallback applies expect(result.length).toBe(1); @@ -1611,16 +1848,37 @@ describe('TransactionUtils', () => { comment: { attendees: [], }, - modifiedAttendees: [{displayName: ' ', login: ' edited@example.com ', avatarUrl: ''}], + modifiedAttendees: [ + { + displayName: ' ', + login: ' edited@example.com ', + avatarUrl: '', + }, + ], }); const result = TransactionUtils.getAttendees(transaction); - expect(result).toEqual([{displayName: 'edited@example.com', login: 'edited@example.com', avatarUrl: ''}]); + expect(result).toEqual([ + { + displayName: 'edited@example.com', + login: 'edited@example.com', + avatarUrl: '', + }, + ]); }); it('should handle comment attendees stored as a plain object', () => { - const attendeesArray: Attendee[] = [{email: 'attendee@example.com', login: 'attendee@example.com', displayName: 'Attendee', avatarUrl: '', accountID: 7, selected: true}]; + const attendeesArray: Attendee[] = [ + { + email: 'attendee@example.com', + login: 'attendee@example.com', + displayName: 'Attendee', + avatarUrl: '', + accountID: 7, + selected: true, + }, + ]; const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { @@ -1636,7 +1894,14 @@ describe('TransactionUtils', () => { it('should handle modifiedAttendees stored as a plain object', () => { const modifiedAttendeesArray: Attendee[] = [ - {email: 'modified@example.com', login: 'modified@example.com', displayName: 'Modified Attendee', avatarUrl: '', accountID: 6, selected: true}, + { + email: 'modified@example.com', + login: 'modified@example.com', + displayName: 'Modified Attendee', + avatarUrl: '', + accountID: 6, + selected: true, + }, ]; const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, @@ -1660,7 +1925,11 @@ describe('TransactionUtils', () => { }, }); - const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: ''}); + const result = TransactionUtils.getAttendees(transaction, { + accountID: CURRENT_USER_ID, + avatarUrl: '', + displayName: '', + }); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1675,7 +1944,11 @@ describe('TransactionUtils', () => { modifiedAttendees: {} as unknown as Attendee[], }); - const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: ''}); + const result = TransactionUtils.getAttendees(transaction, { + accountID: CURRENT_USER_ID, + avatarUrl: '', + displayName: '', + }); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1683,43 +1956,92 @@ describe('TransactionUtils', () => { }); describe('getAttendeesListDisplayString', () => { - const localeCompare = (a: string, b: string) => a.localeCompare(b, undefined, {numeric: true, sensitivity: 'variant', caseFirst: 'upper'}); + const localeCompare = (a: string, b: string) => + a.localeCompare(b, undefined, { + numeric: true, + sensitivity: 'variant', + caseFirst: 'upper', + }); it('preserves insertion order when no localeCompare is provided', () => { const attendees: Attendee[] = [ - {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, - {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + { + email: 'b@x.com', + displayName: 'banana', + avatarUrl: '', + login: 'b@x.com', + }, + { + email: 'a@x.com', + displayName: 'apple', + avatarUrl: '', + login: 'a@x.com', + }, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('banana, apple'); }); it('returns attendees alphabetically regardless of insertion order (deploy blocker #89130)', () => { const attendees: Attendee[] = [ - {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, - {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + { + email: 'b@x.com', + displayName: 'banana', + avatarUrl: '', + login: 'b@x.com', + }, + { + email: 'a@x.com', + displayName: 'apple', + avatarUrl: '', + login: 'a@x.com', + }, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('apple, banana'); }); it('uses numeric-aware sort so "User 9" comes before "User 10"', () => { const attendees: Attendee[] = [ - {email: '10@x.com', displayName: 'User 10', avatarUrl: '', login: '10@x.com'}, - {email: '9@x.com', displayName: 'User 9', avatarUrl: '', login: '9@x.com'}, + { + email: '10@x.com', + displayName: 'User 10', + avatarUrl: '', + login: '10@x.com', + }, + { + email: '9@x.com', + displayName: 'User 9', + avatarUrl: '', + login: '9@x.com', + }, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('User 9, User 10'); }); it('compares case-insensitively so the joined string matches pill order', () => { const attendees: Attendee[] = [ - {email: 'b@x.com', displayName: 'Bob', avatarUrl: '', login: 'b@x.com'}, - {email: 'a@x.com', displayName: 'alice', avatarUrl: '', login: 'a@x.com'}, + { + email: 'b@x.com', + displayName: 'Bob', + avatarUrl: '', + login: 'b@x.com', + }, + { + email: 'a@x.com', + displayName: 'alice', + avatarUrl: '', + login: 'a@x.com', + }, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('alice, Bob'); }); it('strips the @expensify.sms domain so phone-login attendees render the same as in pills', () => { const attendees: Attendee[] = [ - {displayName: '+15551234567@expensify.sms', avatarUrl: '', login: '+15551234567@expensify.sms'}, + { + displayName: '+15551234567@expensify.sms', + avatarUrl: '', + login: '+15551234567@expensify.sms', + }, {displayName: 'Alice', avatarUrl: '', login: 'alice@x.com'}, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('+15551234567, Alice'); @@ -1731,8 +2053,18 @@ describe('TransactionUtils', () => { it('does not mutate the input array', () => { const attendees: Attendee[] = [ - {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, - {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, + { + email: 'b@x.com', + displayName: 'banana', + avatarUrl: '', + login: 'b@x.com', + }, + { + email: 'a@x.com', + displayName: 'apple', + avatarUrl: '', + login: 'a@x.com', + }, ]; const snapshot = [...attendees]; TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare); @@ -2336,7 +2668,11 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; + const fakePolicy = { + ...createRandomPolicy(0), + id: 'fakePolicyID', + areTagsEnabled: true, + }; const result = TransactionUtils.compareDuplicateTransactionFields(singleLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); expect(result.change.tag).toContain('Engineering'); @@ -2367,7 +2703,11 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; + const fakePolicy = { + ...createRandomPolicy(0), + id: 'fakePolicyID', + areTagsEnabled: true, + }; const result = TransactionUtils.compareDuplicateTransactionFields(singleLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); // Since only one enabled tag is available and empty is not included, tag should not be in change @@ -2739,7 +3079,11 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; + const fakePolicy = { + ...createRandomPolicy(0), + id: 'fakePolicyID', + areTagsEnabled: true, + }; const result = TransactionUtils.compareDuplicateTransactionFields(multiLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); expect(result.change.tag).toContain('Engineering:Project A'); @@ -2806,7 +3150,11 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; + const fakePolicy = { + ...createRandomPolicy(0), + id: 'fakePolicyID', + areTagsEnabled: true, + }; const result = TransactionUtils.compareDuplicateTransactionFields(singleLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); expect(result.change.tag).toContain('Engineering'); @@ -3612,14 +3960,19 @@ describe('TransactionUtils', () => { describe('when iouRequestType is a non-distance type', () => { it('returns false', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + }); expect(TransactionUtils.isDistanceRequest(transaction)).toBe(false); }); }); describe('when iouRequestType is null', () => { it('returns false', () => { - const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; + const transaction = { + ...generateTransaction(), + iouRequestType: null, + } as unknown as Transaction; expect(TransactionUtils.isDistanceRequest(transaction)).toBe(false); }); }); @@ -3639,14 +3992,46 @@ describe('TransactionUtils', () => { }); describe.each([ - {fn: 'isDistanceTypeRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, - {fn: 'isMapDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, other: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}, - {fn: 'isGPSDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, - {fn: 'isManualDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, - {fn: 'isOdometerDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, - {fn: 'isScanRequest', match: CONST.IOU.REQUEST_TYPE.SCAN, other: CONST.IOU.REQUEST_TYPE.MANUAL}, - {fn: 'isPerDiemRequest', match: CONST.IOU.REQUEST_TYPE.PER_DIEM, other: CONST.IOU.REQUEST_TYPE.TIME}, - {fn: 'isTimeRequest', match: CONST.IOU.REQUEST_TYPE.TIME, other: CONST.IOU.REQUEST_TYPE.PER_DIEM}, + { + fn: 'isDistanceTypeRequest', + match: CONST.IOU.REQUEST_TYPE.DISTANCE, + other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + }, + { + fn: 'isMapDistanceRequest', + match: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + other: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, + }, + { + fn: 'isGPSDistanceRequest', + match: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, + other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + }, + { + fn: 'isManualDistanceRequest', + match: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, + other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + }, + { + fn: 'isOdometerDistanceRequest', + match: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, + other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + }, + { + fn: 'isScanRequest', + match: CONST.IOU.REQUEST_TYPE.SCAN, + other: CONST.IOU.REQUEST_TYPE.MANUAL, + }, + { + fn: 'isPerDiemRequest', + match: CONST.IOU.REQUEST_TYPE.PER_DIEM, + other: CONST.IOU.REQUEST_TYPE.TIME, + }, + { + fn: 'isTimeRequest', + match: CONST.IOU.REQUEST_TYPE.TIME, + other: CONST.IOU.REQUEST_TYPE.PER_DIEM, + }, ] as const)('$fn', ({fn, match, other}) => { describe(`when iouRequestType matches`, () => { it('returns true', () => { @@ -3664,7 +4049,10 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns false', () => { - const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; + const transaction = { + ...generateTransaction(), + iouRequestType: null, + } as unknown as Transaction; expect(TransactionUtils[fn](transaction)).toBe(false); }); }); @@ -3697,7 +4085,10 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns manual as the fallback', () => { - const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; + const transaction = { + ...generateTransaction(), + iouRequestType: null, + } as unknown as Transaction; expect(TransactionUtils.getRequestType(transaction)).toBe(CONST.IOU.REQUEST_TYPE.MANUAL); }); }); @@ -3722,29 +4113,42 @@ describe('TransactionUtils', () => { }); it('returns false for a non-distance (scan) request', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns false for an odometer distance request', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns true for a map distance request', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a GPS distance request', () => { - const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}); + const transaction = generateTransaction({ + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, + }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a manual distance request that carries waypoints', () => { const transaction = generateTransaction({ iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, - comment: {waypoints: {waypoint0: {lat: 0, lng: 0}, waypoint1: {lat: 1, lng: 1}}}, + comment: { + waypoints: { + waypoint0: {lat: 0, lng: 0}, + waypoint1: {lat: 1, lng: 1}, + }, + }, }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); @@ -3799,7 +4203,11 @@ describe('getSupersededPendingCardTransactionIDs', () => { it('does not supersede an unrelated non-card pending transaction', () => { const cardPending = buildCardTransaction('auth1', CONST.TRANSACTION.STATUS.PENDING); const cardPosted = buildCardTransaction('clear1', CONST.TRANSACTION.STATUS.POSTED, 'auth1'); - const manualPending = generateTransaction({transactionID: 'manual1', bank: '', status: CONST.TRANSACTION.STATUS.PENDING}); + const manualPending = generateTransaction({ + transactionID: 'manual1', + bank: '', + status: CONST.TRANSACTION.STATUS.PENDING, + }); const result = TransactionUtils.getSupersededPendingCardTransactionIDs([cardPending, cardPosted, manualPending]); @@ -3843,12 +4251,25 @@ describe('doesMoneyRequestDraftHaveUserInput', () => { }); it('returns false when the draft only has empty waypoint placeholders', () => { - const transaction = generateTransaction({comment: {waypoints: {waypoint0: {}, waypoint1: {}}}}); + const transaction = generateTransaction({ + comment: {waypoints: {waypoint0: {}, waypoint1: {}}}, + }); expect(doesMoneyRequestDraftHaveUserInput(transaction)).toBe(false); }); it('returns true when the user entered a waypoint', () => { - const transaction = generateTransaction({comment: {waypoints: {waypoint0: {address: '350 5th Ave, New York', lat: 40.7484, lng: -73.9857}, waypoint1: {}}}}); + const transaction = generateTransaction({ + comment: { + waypoints: { + waypoint0: { + address: '350 5th Ave, New York', + lat: 40.7484, + lng: -73.9857, + }, + waypoint1: {}, + }, + }, + }); expect(doesMoneyRequestDraftHaveUserInput(transaction)).toBe(true); }); }); From 0282a5e4aee30f36d70dd77376131f8805f27988 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 5 Jul 2026 13:51:21 +0530 Subject: [PATCH 2/4] Fix commit --- src/libs/TransactionUtils/index.ts | 26 +- tests/unit/TransactionUtilsTest.ts | 562 +++++------------------------ 2 files changed, 98 insertions(+), 490 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index ecce8299376c..4516f44e8778 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -668,11 +668,7 @@ function getUpdatedTransaction({ updatedTransaction.modifiedAmount = CONST.IOU.DEFAULT_AMOUNT; updatedTransaction.modifiedMerchant = translateLocal('iou.fieldPending'); } else if (transactionChanges.routes?.route0?.geometry?.coordinates) { - const mileageRate = DistanceRequestUtils.getRate({ - transaction: updatedTransaction, - policy, - personalPolicyOutputCurrency, - }); + const mileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, personalPolicyOutputCurrency}); const {unit, rate} = mileageRate; // Use route distance directly since waypoints changed and the route was recalculated. @@ -721,11 +717,7 @@ function getUpdatedTransaction({ const existingDistanceUnit = transaction?.comment?.customUnit?.distanceUnit; // Get the new distance unit from the rate's unit - const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({ - transaction: updatedTransaction, - policy, - personalPolicyOutputCurrency, - }); + const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({transaction: updatedTransaction, policy, personalPolicyOutputCurrency}); lodashSet(updatedTransaction, 'comment.customUnit.distanceUnit', newDistanceUnit); // If the distanceUnit is set and the rate is changed to one that has a different unit, convert the distance to the new unit. @@ -740,12 +732,7 @@ function getUpdatedTransaction({ // When the waypoints are being fetched from the server, we have no information about the distance, and cannot recalculate the updated amount. // Otherwise, recalculate the fields based on the new rate. - const updatedMileageRate = DistanceRequestUtils.getRate({ - transaction: updatedTransaction, - policy, - useTransactionDistanceUnit: false, - personalPolicyOutputCurrency, - }); + const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false, personalPolicyOutputCurrency}); const {unit, rate} = updatedMileageRate; const distanceInMeters = getDistanceInMeters(updatedTransaction, unit); @@ -833,12 +820,7 @@ function getUpdatedTransaction({ lodashSet(updatedTransaction, 'routes.route0.distance', null); shouldStopSmartscan = true; - const updatedMileageRate = DistanceRequestUtils.getRate({ - transaction: updatedTransaction, - policy, - useTransactionDistanceUnit: false, - personalPolicyOutputCurrency, - }); + const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false, personalPolicyOutputCurrency}); const {unit, rate} = updatedMileageRate; // Sync the stored distanceUnit to the policy's current unit. The user's input on the Manual diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index b5f91e0968eb..1affc2360be4 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -100,10 +100,7 @@ describe('TransactionUtils', () => { Onyx.init({ keys: ONYXKEYS, initialKeyStates: { - [ONYXKEYS.SESSION]: { - accountID: CURRENT_USER_ID, - email: CURRENT_USER_EMAIL, - }, + [ONYXKEYS.SESSION]: {accountID: CURRENT_USER_ID, email: CURRENT_USER_EMAIL}, [ONYXKEYS.PERSONAL_DETAILS_LIST]: { [CURRENT_USER_ID]: { accountID: CURRENT_USER_ID, @@ -264,28 +261,15 @@ describe('TransactionUtils', () => { describe('getIsFromGlobalCreate', () => { it('returns true when isFromFloatingActionButton is true', () => { - expect( - TransactionUtils.getIsFromGlobalCreate({ - isFromFloatingActionButton: true, - } as Transaction), - ).toBe(true); + expect(TransactionUtils.getIsFromGlobalCreate({isFromFloatingActionButton: true} as Transaction)).toBe(true); }); it('returns false when isFromFloatingActionButton is explicitly false (FAB takes precedence over isFromGlobalCreate via ?? semantics)', () => { - expect( - TransactionUtils.getIsFromGlobalCreate({ - isFromFloatingActionButton: false, - isFromGlobalCreate: true, - } as Transaction), - ).toBe(false); + expect(TransactionUtils.getIsFromGlobalCreate({isFromFloatingActionButton: false, isFromGlobalCreate: true} as Transaction)).toBe(false); }); it('falls back to isFromGlobalCreate when isFromFloatingActionButton is undefined', () => { - expect( - TransactionUtils.getIsFromGlobalCreate({ - isFromGlobalCreate: true, - } as Transaction), - ).toBe(true); + expect(TransactionUtils.getIsFromGlobalCreate({isFromGlobalCreate: true} as Transaction)).toBe(true); }); it('returns undefined when both flags are absent', () => { @@ -304,9 +288,7 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: { - expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1'), - }, + rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, }; const transaction = generateTransaction(); @@ -326,9 +308,7 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: { - expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1'), - }, + rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, }; const transaction = generateTransaction(); @@ -360,9 +340,7 @@ describe('TransactionUtils', () => { }, }, outputCurrency: 'IDR', - rules: { - expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1'), - }, + rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, }; const transaction = generateTransaction(); @@ -382,9 +360,7 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: { - expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1'), - }, + rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, }; const transaction: Transaction = { ...generateTransaction(), @@ -429,9 +405,7 @@ describe('TransactionUtils', () => { const fakePolicy: Policy = { ...createRandomPolicy(0), taxRates: CONST.DEFAULT_TAX, - rules: { - expenseRules: createCategoryTaxExpenseRules(category, taxCode), - }, + rules: {expenseRules: createCategoryTaxExpenseRules(category, taxCode)}, }; const transaction = generateTransaction(); @@ -517,10 +491,7 @@ describe('TransactionUtils', () => { // A P2P distance expense (FAKE_P2P_ID) has no policy rate, so the mileage currency comes from the // resolved personal-policy currency that getUpdatedTransaction forwards to getRate. getRateForP2P only // returns that resolved currency when the default P2P rate is loaded, so seed it here. - await Onyx.merge(ONYXKEYS.DEFAULT_P2P_MILEAGE_RATE, { - rate: 30, - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - }); + await Onyx.merge(ONYXKEYS.DEFAULT_P2P_MILEAGE_RATE, {rate: 30, unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}); await waitForBatchedUpdates(); try { @@ -591,11 +562,7 @@ describe('TransactionUtils', () => { const updatedTransaction = TransactionUtils.getUpdatedTransaction({ transaction, isFromExpenseReport: true, - transactionChanges: { - taxCode: 'id_TAX_RATE_1', - taxAmount: 50, - taxValue: '5%', - }, + transactionChanges: {taxCode: 'id_TAX_RATE_1', taxAmount: 50, taxValue: '5%'}, personalPolicyOutputCurrency: undefined, }); @@ -727,47 +694,32 @@ describe('TransactionUtils', () => { }); it('returns false for a non-distance expense', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns false for an odometer distance expense (no map), even if it somehow carries waypoints', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, - comment: {waypoints}, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, comment: {waypoints}}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns false for a pure manual distance expense with no waypoints', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, - comment: {}, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, comment: {}}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns true for a manual distance expense that carries waypoints (e.g. merged from a map expense)', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, - comment: {waypoints}, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, comment: {waypoints}}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a map distance expense', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a GPS distance expense', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); }); @@ -817,12 +769,7 @@ describe('TransactionUtils', () => { describe('shouldShowBrokenConnectionViolation', () => { it('should return false when no broken connection violations are found for the provided transaction', () => { - const transactionViolations = [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }, - ]; + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(undefined, undefined, transactionViolations); expect(showBrokenConnectionViolation).toBe(false); @@ -830,13 +777,7 @@ describe('TransactionUtils', () => { it('should return true when a broken connection violation exists for one transaction and the user is the policy member', () => { const policy = {role: CONST.POLICY.ROLE.USER} as Policy; - const transactionViolations = [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.RTER, - data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, - }, - ]; + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(undefined, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); @@ -855,9 +796,7 @@ describe('TransactionUtils', () => { { type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, - data: { - rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION, - }, + data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, }, ], }; @@ -876,13 +815,7 @@ describe('TransactionUtils', () => { it('should return true when a broken connection violation exists and the user is the policy admin and the expense submitter', () => { const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; const report = processingReport; - const transactionViolations = [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.RTER, - data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, - }, - ]; + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); @@ -891,32 +824,16 @@ describe('TransactionUtils', () => { it('should return true when a broken connection violation exists, the user is the policy admin and the expense report is in the open state', () => { const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; const report = secondUserOpenReport; - const transactionViolations = [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.RTER, - data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, - }, - ]; + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); }); it('should return true when a broken connection violation exists, the user is the policy admin, the expense report is in the processing state and instant submit is enabled', () => { - const policy = { - role: CONST.POLICY.ROLE.ADMIN, - autoReporting: true, - autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, - } as Policy; + const policy = {role: CONST.POLICY.ROLE.ADMIN, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT} as Policy; const report = processingReport; - const transactionViolations = [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.RTER, - data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, - }, - ]; + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(true); @@ -925,13 +842,7 @@ describe('TransactionUtils', () => { it('should return false when a broken connection violation exists, the user is the policy admin but the expense report is in the approved state', () => { const policy = {role: CONST.POLICY.ROLE.ADMIN} as Policy; const report = approvedReport; - const transactionViolations = [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.RTER, - data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}, - }, - ]; + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION}}]; const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(report, policy, transactionViolations); expect(showBrokenConnectionViolation).toBe(false); @@ -942,13 +853,7 @@ describe('TransactionUtils', () => { it('returns true for empty, default, and partial merchant values', () => { expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: ''}))).toBe(true); expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: CONST.TRANSACTION.DEFAULT_MERCHANT}))).toBe(true); - expect( - TransactionUtils.isMerchantMissing( - generateTransaction({ - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - }), - ), - ).toBe(true); + expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}))).toBe(true); }); it('returns false for a valid merchant', () => { @@ -1028,34 +933,24 @@ describe('TransactionUtils', () => { }); it('should normalize the DEFAULT_MERCHANT ("Expense") sentinel to an empty string', () => { - const transaction = generateTransaction({ - merchant: CONST.TRANSACTION.DEFAULT_MERCHANT, - }); + const transaction = generateTransaction({merchant: CONST.TRANSACTION.DEFAULT_MERCHANT}); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe(''); }); it('should normalize the PARTIAL_TRANSACTION_MERCHANT ("(none)") sentinel to an empty string', () => { - const transaction = generateTransaction({ - merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - }); + const transaction = generateTransaction({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe(''); }); it('should prefer modifiedMerchant over merchant', () => { - const transaction = generateTransaction({ - merchant: 'Original', - modifiedMerchant: 'Modified', - }); + const transaction = generateTransaction({merchant: 'Original', modifiedMerchant: 'Modified'}); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe('Modified'); }); it('should return the localized scanning label while a receipt is scanning', () => { const transaction = generateTransaction({ merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - receipt: { - state: CONST.IOU.RECEIPT_STATE.SCANNING, - source: 'receipt.jpg', - }, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCANNING, source: 'receipt.jpg'}, }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe('iou.receiptStatusTitle'); }); @@ -1063,10 +958,7 @@ describe('TransactionUtils', () => { it('should return an empty string for a receipt whose scan failed', () => { const transaction = generateTransaction({ merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - receipt: { - state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, - source: 'receipt.jpg', - }, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED, source: 'receipt.jpg'}, }); expect(TransactionUtils.getMerchantName(transaction, translate)).toBe(''); }); @@ -1196,10 +1088,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When checking if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); @@ -1213,10 +1102,7 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ comment: {}, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When checking if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); @@ -1236,10 +1122,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When checking if violation is dismissed for current user const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); @@ -1268,10 +1151,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When current user (admin, not the owner) checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, iouReport, undefined); @@ -1298,10 +1178,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When current user (admin, not the owner) checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, iouReport, undefined); @@ -1328,10 +1205,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When current user (the submitter) checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, iouReport, undefined); @@ -1360,10 +1234,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.WARNING, - name: CONST.VIOLATIONS.RTER, - }; + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1390,10 +1261,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.WARNING, - name: CONST.VIOLATIONS.RTER, - }; + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1420,10 +1288,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.WARNING, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1451,10 +1316,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.WARNING, - name: CONST.VIOLATIONS.RTER, - }; + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; // When current user checks if violation is dismissed const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, policy); @@ -1466,10 +1328,7 @@ describe('TransactionUtils', () => { describe('Edge cases and data validation', () => { it('should return false when transaction is null', () => { - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; const result = TransactionUtils.isViolationDismissed(undefined, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); expect(result).toBe(false); }); @@ -1490,10 +1349,7 @@ describe('TransactionUtils', () => { }, }, }); - const violation = { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - }; + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, CURRENT_USER_ID, undefined, undefined); expect(result).toBe(false); }); @@ -1521,19 +1377,13 @@ describe('TransactionUtils', () => { describe('getReportOwnerAsAttendee', () => { it('should return undefined when details has no login and display name', () => { - const result = TransactionUtils.getReportOwnerAsAttendee({ - accountID: 1, - }); + const result = TransactionUtils.getReportOwnerAsAttendee({accountID: 1}); expect(result).toBeUndefined(); }); it('should return report owner as attendee for reported expense', () => { const avatar = 'test'; - const result = TransactionUtils.getReportOwnerAsAttendee({ - accountID: SECOND_USER_ID, - login: OTHER_USER_EMAIL, - avatar, - }); + const result = TransactionUtils.getReportOwnerAsAttendee({accountID: SECOND_USER_ID, login: OTHER_USER_EMAIL, avatar}); expect(result).toBeDefined(); expect(result?.email).toBe(OTHER_USER_EMAIL); @@ -1647,12 +1497,7 @@ describe('TransactionUtils', () => { }, }); - const result = TransactionUtils.getOriginalAttendees(transaction, { - accountID: CURRENT_USER_ID, - displayName: '', - avatarUrl: '', - selected: true, - }); + const result = TransactionUtils.getOriginalAttendees(transaction, {accountID: CURRENT_USER_ID, displayName: '', avatarUrl: '', selected: true}); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1663,45 +1508,19 @@ describe('TransactionUtils', () => { const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { - attendees: [ - { - displayName: ' ', - login: ' login-only@example.com ', - avatarUrl: '', - }, - ], + attendees: [{displayName: ' ', login: ' login-only@example.com ', avatarUrl: ''}], }, }); const result = TransactionUtils.getOriginalAttendees(transaction, undefined); - expect(result).toEqual([ - { - displayName: 'login-only@example.com', - login: 'login-only@example.com', - avatarUrl: '', - }, - ]); + expect(result).toEqual([{displayName: 'login-only@example.com', login: 'login-only@example.com', avatarUrl: ''}]); }); it('should handle attendees stored as a plain object', () => { const attendeesArray: Attendee[] = [ - { - email: 'attendee1@example.com', - login: 'attendee1@example.com', - displayName: 'Attendee One', - avatarUrl: '', - accountID: 3, - selected: true, - }, - { - email: 'attendee2@example.com', - login: 'attendee2@example.com', - displayName: 'Attendee Two', - avatarUrl: '', - accountID: 4, - selected: false, - }, + {email: 'attendee1@example.com', login: 'attendee1@example.com', displayName: 'Attendee One', avatarUrl: '', accountID: 3, selected: true}, + {email: 'attendee2@example.com', login: 'attendee2@example.com', displayName: 'Attendee Two', avatarUrl: '', accountID: 4, selected: false}, ]; const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, @@ -1786,12 +1605,7 @@ describe('TransactionUtils', () => { }, }); - const result = TransactionUtils.getAttendees(transaction, { - accountID: CURRENT_USER_ID, - avatarUrl: '', - displayName: '', - selected: true, - }); + const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: '', selected: true}); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1831,11 +1645,7 @@ describe('TransactionUtils', () => { modifiedAttendees: [], }); - const result = TransactionUtils.getAttendees(transaction, { - accountID: CURRENT_USER_ID, - avatarUrl: '', - displayName: '', - }); + const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: ''}); // When modifiedAttendees is empty array and no report owner fallback applies expect(result.length).toBe(1); @@ -1848,37 +1658,16 @@ describe('TransactionUtils', () => { comment: { attendees: [], }, - modifiedAttendees: [ - { - displayName: ' ', - login: ' edited@example.com ', - avatarUrl: '', - }, - ], + modifiedAttendees: [{displayName: ' ', login: ' edited@example.com ', avatarUrl: ''}], }); const result = TransactionUtils.getAttendees(transaction); - expect(result).toEqual([ - { - displayName: 'edited@example.com', - login: 'edited@example.com', - avatarUrl: '', - }, - ]); + expect(result).toEqual([{displayName: 'edited@example.com', login: 'edited@example.com', avatarUrl: ''}]); }); it('should handle comment attendees stored as a plain object', () => { - const attendeesArray: Attendee[] = [ - { - email: 'attendee@example.com', - login: 'attendee@example.com', - displayName: 'Attendee', - avatarUrl: '', - accountID: 7, - selected: true, - }, - ]; + const attendeesArray: Attendee[] = [{email: 'attendee@example.com', login: 'attendee@example.com', displayName: 'Attendee', avatarUrl: '', accountID: 7, selected: true}]; const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, comment: { @@ -1894,14 +1683,7 @@ describe('TransactionUtils', () => { it('should handle modifiedAttendees stored as a plain object', () => { const modifiedAttendeesArray: Attendee[] = [ - { - email: 'modified@example.com', - login: 'modified@example.com', - displayName: 'Modified Attendee', - avatarUrl: '', - accountID: 6, - selected: true, - }, + {email: 'modified@example.com', login: 'modified@example.com', displayName: 'Modified Attendee', avatarUrl: '', accountID: 6, selected: true}, ]; const transaction = generateTransaction({ reportID: FAKE_OPEN_REPORT_ID, @@ -1925,11 +1707,7 @@ describe('TransactionUtils', () => { }, }); - const result = TransactionUtils.getAttendees(transaction, { - accountID: CURRENT_USER_ID, - avatarUrl: '', - displayName: '', - }); + const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: ''}); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1944,11 +1722,7 @@ describe('TransactionUtils', () => { modifiedAttendees: {} as unknown as Attendee[], }); - const result = TransactionUtils.getAttendees(transaction, { - accountID: CURRENT_USER_ID, - avatarUrl: '', - displayName: '', - }); + const result = TransactionUtils.getAttendees(transaction, {accountID: CURRENT_USER_ID, avatarUrl: '', displayName: ''}); expect(result.length).toBe(1); expect(result.at(0)?.accountID).toBe(CURRENT_USER_ID); @@ -1956,92 +1730,43 @@ describe('TransactionUtils', () => { }); describe('getAttendeesListDisplayString', () => { - const localeCompare = (a: string, b: string) => - a.localeCompare(b, undefined, { - numeric: true, - sensitivity: 'variant', - caseFirst: 'upper', - }); + const localeCompare = (a: string, b: string) => a.localeCompare(b, undefined, {numeric: true, sensitivity: 'variant', caseFirst: 'upper'}); it('preserves insertion order when no localeCompare is provided', () => { const attendees: Attendee[] = [ - { - email: 'b@x.com', - displayName: 'banana', - avatarUrl: '', - login: 'b@x.com', - }, - { - email: 'a@x.com', - displayName: 'apple', - avatarUrl: '', - login: 'a@x.com', - }, + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees)).toBe('banana, apple'); }); it('returns attendees alphabetically regardless of insertion order (deploy blocker #89130)', () => { const attendees: Attendee[] = [ - { - email: 'b@x.com', - displayName: 'banana', - avatarUrl: '', - login: 'b@x.com', - }, - { - email: 'a@x.com', - displayName: 'apple', - avatarUrl: '', - login: 'a@x.com', - }, + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('apple, banana'); }); it('uses numeric-aware sort so "User 9" comes before "User 10"', () => { const attendees: Attendee[] = [ - { - email: '10@x.com', - displayName: 'User 10', - avatarUrl: '', - login: '10@x.com', - }, - { - email: '9@x.com', - displayName: 'User 9', - avatarUrl: '', - login: '9@x.com', - }, + {email: '10@x.com', displayName: 'User 10', avatarUrl: '', login: '10@x.com'}, + {email: '9@x.com', displayName: 'User 9', avatarUrl: '', login: '9@x.com'}, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('User 9, User 10'); }); it('compares case-insensitively so the joined string matches pill order', () => { const attendees: Attendee[] = [ - { - email: 'b@x.com', - displayName: 'Bob', - avatarUrl: '', - login: 'b@x.com', - }, - { - email: 'a@x.com', - displayName: 'alice', - avatarUrl: '', - login: 'a@x.com', - }, + {email: 'b@x.com', displayName: 'Bob', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'alice', avatarUrl: '', login: 'a@x.com'}, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('alice, Bob'); }); it('strips the @expensify.sms domain so phone-login attendees render the same as in pills', () => { const attendees: Attendee[] = [ - { - displayName: '+15551234567@expensify.sms', - avatarUrl: '', - login: '+15551234567@expensify.sms', - }, + {displayName: '+15551234567@expensify.sms', avatarUrl: '', login: '+15551234567@expensify.sms'}, {displayName: 'Alice', avatarUrl: '', login: 'alice@x.com'}, ]; expect(TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare)).toBe('+15551234567, Alice'); @@ -2053,18 +1778,8 @@ describe('TransactionUtils', () => { it('does not mutate the input array', () => { const attendees: Attendee[] = [ - { - email: 'b@x.com', - displayName: 'banana', - avatarUrl: '', - login: 'b@x.com', - }, - { - email: 'a@x.com', - displayName: 'apple', - avatarUrl: '', - login: 'a@x.com', - }, + {email: 'b@x.com', displayName: 'banana', avatarUrl: '', login: 'b@x.com'}, + {email: 'a@x.com', displayName: 'apple', avatarUrl: '', login: 'a@x.com'}, ]; const snapshot = [...attendees]; TransactionUtils.getAttendeesListDisplayString(attendees, localeCompare); @@ -2668,11 +2383,7 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = { - ...createRandomPolicy(0), - id: 'fakePolicyID', - areTagsEnabled: true, - }; + const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; const result = TransactionUtils.compareDuplicateTransactionFields(singleLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); expect(result.change.tag).toContain('Engineering'); @@ -2703,11 +2414,7 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = { - ...createRandomPolicy(0), - id: 'fakePolicyID', - areTagsEnabled: true, - }; + const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; const result = TransactionUtils.compareDuplicateTransactionFields(singleLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); // Since only one enabled tag is available and empty is not included, tag should not be in change @@ -3079,11 +2786,7 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = { - ...createRandomPolicy(0), - id: 'fakePolicyID', - areTagsEnabled: true, - }; + const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; const result = TransactionUtils.compareDuplicateTransactionFields(multiLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); expect(result.change.tag).toContain('Engineering:Project A'); @@ -3150,11 +2853,7 @@ describe('TransactionUtils', () => { reportID: fakeReportID, }); - const fakePolicy = { - ...createRandomPolicy(0), - id: 'fakePolicyID', - areTagsEnabled: true, - }; + const fakePolicy = {...createRandomPolicy(0), id: 'fakePolicyID', areTagsEnabled: true}; const result = TransactionUtils.compareDuplicateTransactionFields(singleLevelPolicyTags, transaction1, [transaction2], fakeReport, undefined, fakePolicy, undefined); expect(result.change.tag).toContain('Engineering'); @@ -3960,19 +3659,14 @@ describe('TransactionUtils', () => { describe('when iouRequestType is a non-distance type', () => { it('returns false', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN}); expect(TransactionUtils.isDistanceRequest(transaction)).toBe(false); }); }); describe('when iouRequestType is null', () => { it('returns false', () => { - const transaction = { - ...generateTransaction(), - iouRequestType: null, - } as unknown as Transaction; + const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; expect(TransactionUtils.isDistanceRequest(transaction)).toBe(false); }); }); @@ -3992,46 +3686,14 @@ describe('TransactionUtils', () => { }); describe.each([ - { - fn: 'isDistanceTypeRequest', - match: CONST.IOU.REQUEST_TYPE.DISTANCE, - other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - }, - { - fn: 'isMapDistanceRequest', - match: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - other: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, - }, - { - fn: 'isGPSDistanceRequest', - match: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, - other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - }, - { - fn: 'isManualDistanceRequest', - match: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, - other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - }, - { - fn: 'isOdometerDistanceRequest', - match: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, - other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - }, - { - fn: 'isScanRequest', - match: CONST.IOU.REQUEST_TYPE.SCAN, - other: CONST.IOU.REQUEST_TYPE.MANUAL, - }, - { - fn: 'isPerDiemRequest', - match: CONST.IOU.REQUEST_TYPE.PER_DIEM, - other: CONST.IOU.REQUEST_TYPE.TIME, - }, - { - fn: 'isTimeRequest', - match: CONST.IOU.REQUEST_TYPE.TIME, - other: CONST.IOU.REQUEST_TYPE.PER_DIEM, - }, + {fn: 'isDistanceTypeRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, + {fn: 'isMapDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, other: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}, + {fn: 'isGPSDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, + {fn: 'isManualDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, + {fn: 'isOdometerDistanceRequest', match: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, other: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}, + {fn: 'isScanRequest', match: CONST.IOU.REQUEST_TYPE.SCAN, other: CONST.IOU.REQUEST_TYPE.MANUAL}, + {fn: 'isPerDiemRequest', match: CONST.IOU.REQUEST_TYPE.PER_DIEM, other: CONST.IOU.REQUEST_TYPE.TIME}, + {fn: 'isTimeRequest', match: CONST.IOU.REQUEST_TYPE.TIME, other: CONST.IOU.REQUEST_TYPE.PER_DIEM}, ] as const)('$fn', ({fn, match, other}) => { describe(`when iouRequestType matches`, () => { it('returns true', () => { @@ -4049,10 +3711,7 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns false', () => { - const transaction = { - ...generateTransaction(), - iouRequestType: null, - } as unknown as Transaction; + const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; expect(TransactionUtils[fn](transaction)).toBe(false); }); }); @@ -4085,10 +3744,7 @@ describe('TransactionUtils', () => { describe('when iouRequestType is null', () => { it('returns manual as the fallback', () => { - const transaction = { - ...generateTransaction(), - iouRequestType: null, - } as unknown as Transaction; + const transaction = {...generateTransaction(), iouRequestType: null} as unknown as Transaction; expect(TransactionUtils.getRequestType(transaction)).toBe(CONST.IOU.REQUEST_TYPE.MANUAL); }); }); @@ -4113,42 +3769,29 @@ describe('TransactionUtils', () => { }); it('returns false for a non-distance (scan) request', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns false for an odometer distance request', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(false); }); it('returns true for a map distance request', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a GPS distance request', () => { - const transaction = generateTransaction({ - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, - }); + const transaction = generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); it('returns true for a manual distance request that carries waypoints', () => { const transaction = generateTransaction({ iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, - comment: { - waypoints: { - waypoint0: {lat: 0, lng: 0}, - waypoint1: {lat: 1, lng: 1}, - }, - }, + comment: {waypoints: {waypoint0: {lat: 0, lng: 0}, waypoint1: {lat: 1, lng: 1}}}, }); expect(TransactionUtils.isMapBasedDistanceRequest(transaction)).toBe(true); }); @@ -4203,11 +3846,7 @@ describe('getSupersededPendingCardTransactionIDs', () => { it('does not supersede an unrelated non-card pending transaction', () => { const cardPending = buildCardTransaction('auth1', CONST.TRANSACTION.STATUS.PENDING); const cardPosted = buildCardTransaction('clear1', CONST.TRANSACTION.STATUS.POSTED, 'auth1'); - const manualPending = generateTransaction({ - transactionID: 'manual1', - bank: '', - status: CONST.TRANSACTION.STATUS.PENDING, - }); + const manualPending = generateTransaction({transactionID: 'manual1', bank: '', status: CONST.TRANSACTION.STATUS.PENDING}); const result = TransactionUtils.getSupersededPendingCardTransactionIDs([cardPending, cardPosted, manualPending]); @@ -4251,25 +3890,12 @@ describe('doesMoneyRequestDraftHaveUserInput', () => { }); it('returns false when the draft only has empty waypoint placeholders', () => { - const transaction = generateTransaction({ - comment: {waypoints: {waypoint0: {}, waypoint1: {}}}, - }); + const transaction = generateTransaction({comment: {waypoints: {waypoint0: {}, waypoint1: {}}}}); expect(doesMoneyRequestDraftHaveUserInput(transaction)).toBe(false); }); it('returns true when the user entered a waypoint', () => { - const transaction = generateTransaction({ - comment: { - waypoints: { - waypoint0: { - address: '350 5th Ave, New York', - lat: 40.7484, - lng: -73.9857, - }, - waypoint1: {}, - }, - }, - }); + const transaction = generateTransaction({comment: {waypoints: {waypoint0: {address: '350 5th Ave, New York', lat: 40.7484, lng: -73.9857}, waypoint1: {}}}}); expect(doesMoneyRequestDraftHaveUserInput(transaction)).toBe(true); }); }); From 1a1d1818bf6d0c936129d2df10028252b84b6fac Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 5 Jul 2026 14:42:09 +0530 Subject: [PATCH 3/4] Address currency change condition when changing waypoint --- src/libs/TransactionUtils/index.ts | 8 ++++- tests/unit/TransactionUtilsTest.ts | 53 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 4516f44e8778..dab132b4c632 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -676,12 +676,15 @@ function getUpdatedTransaction({ const distanceInMeters = transactionChanges.routes?.route0?.distance ?? getDistanceInMeters(transaction, unit); const amount = DistanceRequestUtils.getDistanceRequestAmount(distanceInMeters, unit, rate ?? 0); const updatedAmount = isFromExpenseReport || isUnReportedExpense ? -amount : amount; + // Use the rate's resolved currency (which may come from personalPolicyOutputCurrency for a P2P expense), + // not transaction.currency, so the merchant symbol/rate and the recalculated amount stay in the same currency. + const updatedCurrency = mileageRate.currency ?? transaction.currency ?? CONST.CURRENCY.USD; const updatedMerchant = DistanceRequestUtils.getDistanceMerchant( true, distanceInMeters, unit, rate, - transaction.currency, + updatedCurrency, translateLocal, (digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit), getCurrencySymbol, @@ -691,6 +694,9 @@ function getUpdatedTransaction({ updatedTransaction.amount = updatedAmount; updatedTransaction.modifiedAmount = updatedAmount; updatedTransaction.modifiedMerchant = updatedMerchant; + if (getCurrency(updatedTransaction) !== updatedCurrency) { + updatedTransaction.modifiedCurrency = updatedCurrency; + } // Sync `customUnit.quantity` to the new route distance. Without this the prior manual // quantity (set when the user edited distance manually before changing waypoints) would diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 1affc2360be4..dd026c5eb981 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -528,6 +528,59 @@ describe('TransactionUtils', () => { } }); + it('uses the resolved personal-policy currency (not transaction.currency) when recalculating a P2P expense from edited waypoints', async () => { + // Regression: the waypoint/route branch used to format the merchant with transaction.currency and never set + // modifiedCurrency, so a P2P expense whose personal currency differs from transaction.currency would show a + // rate in the old currency for an amount computed in the new one (visible offline / before the server responds). + await Onyx.merge(ONYXKEYS.DEFAULT_P2P_MILEAGE_RATE, {rate: 30, unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}); + await waitForBatchedUpdates(); + + try { + const transaction = generateTransaction({ + currency: CONST.CURRENCY.USD, + comment: { + customUnit: { + customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, + distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + quantity: 10, + }, + waypoints: {waypoint0: {address: 'Old A'}, waypoint1: {address: 'Old B'}}, + }, + }); + + // 32186.88 m ≈ 20 mi; at the EUR P2P rate of 30¢ the amount is 600. + const updatedTransaction = TransactionUtils.getUpdatedTransaction({ + transaction, + isFromExpenseReport: false, + policy: undefined, + transactionChanges: { + waypoints: {waypoint0: {address: 'New A'}, waypoint1: {address: 'New B'}}, + routes: { + route0: { + distance: 32186.88, + geometry: { + coordinates: [ + [0, 0], + [1, 1], + ], + }, + }, + }, + }, + personalPolicyOutputCurrency: 'EUR', + }); + + // Merchant currency and modifiedCurrency both follow the EUR personal-policy rate, not the USD transaction currency. + expect(updatedTransaction.modifiedCurrency).toBe('EUR'); + expect(updatedTransaction.modifiedMerchant).toContain('EUR'); + expect(updatedTransaction.modifiedMerchant).toContain('0.30'); + expect(updatedTransaction.modifiedAmount).toBe(600); + } finally { + await Onyx.merge(ONYXKEYS.DEFAULT_P2P_MILEAGE_RATE, null); + await waitForBatchedUpdates(); + } + }); + it('should negate modifiedAmount when isFromExpenseReport is true', () => { const transaction = generateTransaction(); const newAmount = 500; From 4c34a2e6de03a649d20ed8c02f9d5cfe5977d7fe Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sun, 5 Jul 2026 15:22:37 +0530 Subject: [PATCH 4/4] Simplifying usage of getUpdatedDistanceUnit --- src/libs/DistanceRequestUtils.ts | 16 ++++------------ src/libs/TransactionUtils/index.ts | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 1f5051d74ccd..23913efa5e82 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -598,18 +598,10 @@ function getRate({ * For example, if an expense is '10 mi @ $1.00 / mi' and the rate is updated to '$1.00 / km', * then the updated distance unit should be 'km' from the updated rate, not 'mi' from the currently stored transaction distance unit. */ -function getUpdatedDistanceUnit({ - transaction, - policy, - policyDraft, - personalPolicyOutputCurrency, -}: { - transaction: OnyxEntry; - policy: OnyxEntry; - policyDraft?: OnyxEntry; - personalPolicyOutputCurrency?: string; -}) { - return getRate({transaction, policy, policyDraft, useTransactionDistanceUnit: false, personalPolicyOutputCurrency}).unit; +function getUpdatedDistanceUnit({transaction, policy, policyDraft}: {transaction: OnyxEntry; policy: OnyxEntry; policyDraft?: OnyxEntry}) { + // The distance unit doesn't depend on the currency (the rate is selected by ID/P2P, not currency), so + // personalPolicyOutputCurrency isn't accepted here and is passed as undefined to getRate. + return getRate({transaction, policy, policyDraft, useTransactionDistanceUnit: false, personalPolicyOutputCurrency: undefined}).unit; } /** diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index dab132b4c632..9e288106be8d 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -723,7 +723,7 @@ function getUpdatedTransaction({ const existingDistanceUnit = transaction?.comment?.customUnit?.distanceUnit; // Get the new distance unit from the rate's unit - const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({transaction: updatedTransaction, policy, personalPolicyOutputCurrency}); + const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({transaction: updatedTransaction, policy}); lodashSet(updatedTransaction, 'comment.customUnit.distanceUnit', newDistanceUnit); // If the distanceUnit is set and the rate is changed to one that has a different unit, convert the distance to the new unit.