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/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 89cd20e19bcb..9e288106be8d 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,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}); + const mileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, personalPolicyOutputCurrency}); const {unit, rate} = mileageRate; // Use route distance directly since waypoints changed and the route was recalculated. @@ -674,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, @@ -689,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 @@ -730,7 +738,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}); + const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false, personalPolicyOutputCurrency}); const {unit, rate} = updatedMileageRate; const distanceInMeters = getDistanceInMeters(updatedTransaction, unit); @@ -818,7 +826,7 @@ 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..dd026c5eb981 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -415,6 +415,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 +464,7 @@ describe('TransactionUtils', () => { isFromExpenseReport: false, policy: fakePolicy, transactionChanges: {distance: newDistance}, + personalPolicyOutputCurrency: undefined, }); // Then: quantity should be updated @@ -485,6 +487,100 @@ 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('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; @@ -493,6 +589,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: true, transactionChanges: {amount: newAmount}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.modifiedAmount).toBe(-newAmount); @@ -506,6 +603,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: false, transactionChanges: {amount: newAmount}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.modifiedAmount).toBe(newAmount); @@ -518,6 +616,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: true, transactionChanges: {taxCode: 'id_TAX_RATE_1', taxAmount: 50, taxValue: '5%'}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.taxValue).toBe('5%'); @@ -532,6 +631,7 @@ describe('TransactionUtils', () => { transaction, isFromExpenseReport: false, transactionChanges: {taxValue: '5%'}, + personalPolicyOutputCurrency: undefined, }); expect(updatedTransaction.taxValue).toBe('10%');