From 74e7cf71e6a95654aebec7998302fb1b4a0f4927 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Mon, 16 Feb 2026 09:44:31 +0100 Subject: [PATCH 1/3] add tests --- tests/actions/IOUTest/DuplicateTest.ts | 110 +++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/actions/IOUTest/DuplicateTest.ts b/tests/actions/IOUTest/DuplicateTest.ts index 828a0b9a35d55..9a85e0aea92e3 100644 --- a/tests/actions/IOUTest/DuplicateTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -1161,6 +1161,116 @@ describe('actions/Duplicate', () => { const apiParams = requestMoneyCall?.[1] as Record; expect(apiParams?.transactionThreadReportID).not.toBe(existingLinkedReportActionChildReportID); }); + + it('should call trackExpense API when targetPolicy is not provided', async () => { + const {waypoints, ...restOfComment} = mockTransaction.comment ?? {}; + const mockCashExpenseTransaction = { + ...mockTransaction, + amount: mockTransaction.amount * -1, + comment: { + ...restOfComment, + }, + }; + + await Onyx.clear(); + + // When duplicating the transaction without targetPolicy + duplicateExpenseTransaction({ + transaction: mockCashExpenseTransaction, + optimisticChatReportID: mockOptimisticChatReportID, + optimisticIOUReportID: mockOptimisticIOUReportID, + isASAPSubmitBetaEnabled: mockIsASAPSubmitBetaEnabled, + introSelected: undefined, + activePolicyID: undefined, + quickAction: undefined, + policyRecentlyUsedCurrencies: [], + isSelfTourViewed: false, + customUnitPolicyID: '', + targetPolicy: undefined, + targetPolicyCategories: undefined, + targetReport: undefined, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + + await waitForBatchedUpdates(); + + // Then the API should have been called with TRACK_EXPENSE instead of REQUEST_MONEY + const trackExpenseCall = writeSpy.mock.calls.find((call: [string, Record]) => call[0] === WRITE_COMMANDS.TRACK_EXPENSE); + const requestMoneyCall = writeSpy.mock.calls.find((call: [string, Record]) => call[0] === WRITE_COMMANDS.REQUEST_MONEY); + + expect(trackExpenseCall).toBeDefined(); + expect(requestMoneyCall).toBeUndefined(); + + // Then a transaction should be created successfully + let duplicatedTransaction: OnyxEntry; + + await getOnyxData({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + duplicatedTransaction = Object.values(allTransactions ?? {}).find((t) => !!t); + }, + }); + + expect(duplicatedTransaction).toBeDefined(); + expect(duplicatedTransaction?.transactionID).not.toBe(mockCashExpenseTransaction.transactionID); + }); + + it('should preserve all transaction fields when duplicating Cash expense', async () => { + // Given a transaction with all fields populated using mockTransaction values + const {waypoints, ...restOfComment} = mockTransaction.comment ?? {}; + const mockCashExpense: Transaction = { + ...mockTransaction, + amount: mockTransaction.amount * -1, + comment: { + ...restOfComment, + }, + }; + + await Onyx.clear(); + + // When duplicating the transaction + duplicateExpenseTransaction({ + transaction: mockCashExpense, + optimisticChatReportID: mockOptimisticChatReportID, + optimisticIOUReportID: mockOptimisticIOUReportID, + isASAPSubmitBetaEnabled: mockIsASAPSubmitBetaEnabled, + introSelected: undefined, + activePolicyID: undefined, + quickAction: undefined, + policyRecentlyUsedCurrencies: [], + isSelfTourViewed: false, + customUnitPolicyID: '', + targetPolicy: mockPolicy, + targetPolicyCategories: fakePolicyCategories, + targetReport: policyExpenseChat, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + + await waitForBatchedUpdates(); + + // The duplicated transaction should have all fields preserved + let duplicatedTransaction: OnyxEntry; + + await getOnyxData({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + duplicatedTransaction = Object.values(allTransactions ?? {}).find((t) => !!t); + }, + }); + + expect(duplicatedTransaction).toBeDefined(); + expect(duplicatedTransaction?.transactionID).not.toBe(mockCashExpense.transactionID); + expect(duplicatedTransaction?.category).toBe(mockTransaction.category); + expect(duplicatedTransaction?.tag).toBe(mockTransaction.tag); + expect(duplicatedTransaction?.billable).toBe(mockTransaction.billable); + expect(duplicatedTransaction?.reimbursable).toBe(mockTransaction.reimbursable); + expect(duplicatedTransaction?.currency).toBe(mockTransaction.currency); + expect(Math.abs(duplicatedTransaction?.amount ?? 0)).toBe(Math.abs(mockTransaction.amount)); + }); }); describe('resolveDuplicate', () => { From 6c81d3d9523c63b1788bd3453b4ff0da84227b7c Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Mon, 16 Feb 2026 10:09:47 +0100 Subject: [PATCH 2/3] add recentWaypoints to duplicateExpenseTransaction args --- src/components/MoneyReportHeader.tsx | 3 +++ src/components/MoneyRequestHeader.tsx | 3 +++ src/libs/actions/IOU/Duplicate.ts | 4 ++-- tests/actions/IOUTest/DuplicateTest.ts | 13 ++++++++++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 33869633a487f..7691971b8fd7f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -369,6 +369,7 @@ function MoneyReportHeader({ const canTriggerAutomaticPDFDownload = useRef(false); const hasFinishedPDFDownload = reportPDFFilename && reportPDFFilename !== CONST.REPORT_DETAILS_MENU_ITEM.ERROR; + const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS, {canBeMissing: true}); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true}); const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true, selector: hasSeenTourSelector}); @@ -708,6 +709,7 @@ function MoneyReportHeader({ targetReport: activePolicyExpenseChat, betas, personalDetails, + recentWaypoints, }); } }, @@ -724,6 +726,7 @@ function MoneyReportHeader({ isSelfTourViewed, betas, personalDetails, + recentWaypoints, ], ); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 4006796771d46..0ecea80ec46a5 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -159,6 +159,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS, {canBeMissing: true}); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const isReportInRHP = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; @@ -214,6 +215,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre targetReport: activePolicyExpenseChat, betas, personalDetails, + recentWaypoints, }); } }, @@ -230,6 +232,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre isSelfTourViewed, betas, personalDetails, + recentWaypoints, ], ); diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index da50ad37aa49c..f936826c6bd67 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -35,7 +35,6 @@ import { getCurrentUserEmail, getMoneyRequestParticipantsFromReport, getPolicyTags, - getRecentWaypoints, getUserAccountID, requestMoney, submitPerDiemExpense, @@ -505,6 +504,7 @@ type DuplicateExpenseTransactionParams = { targetReport?: OnyxTypes.Report; betas: OnyxEntry; personalDetails: OnyxEntry; + recentWaypoints: OnyxEntry; }; function duplicateExpenseTransaction({ @@ -523,6 +523,7 @@ function duplicateExpenseTransaction({ targetReport, betas, personalDetails, + recentWaypoints, }: DuplicateExpenseTransactionParams) { if (!transaction) { return; @@ -530,7 +531,6 @@ function duplicateExpenseTransaction({ const userAccountID = getUserAccountID(); const currentUserEmail = getCurrentUserEmail(); - const recentWaypoints = getRecentWaypoints(); const participants = getMoneyRequestParticipantsFromReport(targetReport, userAccountID); const transactionDetails = getTransactionDetails(transaction); diff --git a/tests/actions/IOUTest/DuplicateTest.ts b/tests/actions/IOUTest/DuplicateTest.ts index 9a85e0aea92e3..349f36226b19a 100644 --- a/tests/actions/IOUTest/DuplicateTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -16,7 +16,7 @@ import IntlStore from '@src/languages/IntlStore'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as API from '@src/libs/API'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OriginalMessageIOU, Report, ReportActions} from '@src/types/onyx'; +import type {OriginalMessageIOU, RecentWaypoint, Report, ReportActions} from '@src/types/onyx'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; import type Transaction from '@src/types/onyx/Transaction'; @@ -890,6 +890,7 @@ describe('actions/Duplicate', () => { describe('duplicateExpenseTransaction', () => { let writeSpy: jest.SpyInstance; + let recentWaypoints: RecentWaypoint[] = []; const mockOptimisticChatReportID = '789'; const mockOptimisticIOUReportID = '987'; @@ -917,6 +918,10 @@ describe('actions/Duplicate', () => { } return Promise.resolve(); }); + Onyx.connect({ + key: ONYXKEYS.NVP_RECENT_WAYPOINTS, + callback: (val) => (recentWaypoints = val ?? []), + }); return Onyx.clear(); }); @@ -952,6 +957,7 @@ describe('actions/Duplicate', () => { targetReport: policyExpenseChat, betas: [CONST.BETAS.ALL], personalDetails: {}, + recentWaypoints, }); await waitForBatchedUpdates(); @@ -1011,6 +1017,7 @@ describe('actions/Duplicate', () => { targetReport: policyExpenseChat, betas: [CONST.BETAS.ALL], personalDetails: {}, + recentWaypoints, }); await waitForBatchedUpdates(); @@ -1070,6 +1077,7 @@ describe('actions/Duplicate', () => { targetReport: policyExpenseChat, betas: [CONST.BETAS.ALL], personalDetails: {}, + recentWaypoints, }); await waitForBatchedUpdates(); @@ -1146,6 +1154,7 @@ describe('actions/Duplicate', () => { targetReport: policyExpenseChat, betas: [CONST.BETAS.ALL], personalDetails: {}, + recentWaypoints, }); await waitForBatchedUpdates(); @@ -1191,6 +1200,7 @@ describe('actions/Duplicate', () => { targetReport: undefined, betas: [CONST.BETAS.ALL], personalDetails: {}, + recentWaypoints, }); await waitForBatchedUpdates(); @@ -1247,6 +1257,7 @@ describe('actions/Duplicate', () => { targetReport: policyExpenseChat, betas: [CONST.BETAS.ALL], personalDetails: {}, + recentWaypoints, }); await waitForBatchedUpdates(); From 63debd2794d5b9aeafd3f9ff6fd0f7da06b7d2db Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Mon, 16 Feb 2026 18:55:09 +0100 Subject: [PATCH 3/3] update recentWaypoints in tests --- tests/actions/IOUTest/DuplicateTest.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/actions/IOUTest/DuplicateTest.ts b/tests/actions/IOUTest/DuplicateTest.ts index 349f36226b19a..4d32df4bb0ade 100644 --- a/tests/actions/IOUTest/DuplicateTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -901,7 +901,7 @@ describe('actions/Duplicate', () => { const policyExpenseChat = createRandomReport(1, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT); const fakePolicyCategories = createRandomPolicyCategories(3); - beforeEach(() => { + beforeEach(async () => { jest.clearAllMocks(); global.fetch = getGlobalFetchMock(); // eslint-disable-next-line rulesdir/no-multiple-api-calls @@ -918,10 +918,7 @@ describe('actions/Duplicate', () => { } return Promise.resolve(); }); - Onyx.connect({ - key: ONYXKEYS.NVP_RECENT_WAYPOINTS, - callback: (val) => (recentWaypoints = val ?? []), - }); + recentWaypoints = (await getOnyxValue(ONYXKEYS.NVP_RECENT_WAYPOINTS)) ?? []; return Onyx.clear(); });