From 1e9b7ccd8fb8243060375a1f2ec22e56fe77cda1 Mon Sep 17 00:00:00 2001 From: Linh Date: Sun, 11 Oct 2026 16:34:00 +0700 Subject: [PATCH 1/4] Remove getReportTransactions callers' reliance on the deprecated default in getPayMoneyRequestParams (Part 8) getPayMoneyRequestParams now takes the report's transactions as an explicit param instead of falling back to the deprecated Onyx.connect-backed global. payMoneyRequest and payInvoice forward it; components and hooks pass the transactions they already have, useHoldMenuSubmit adds useReportTransactions, and bulk pay groups allTransactions by report once before its loop. --- config/eslint/eslint.seatbelt.tsv | 1 - .../MoneyReportHeaderSecondaryActions.tsx | 2 + .../PayPrimaryAction.tsx | 2 + .../PayActionButton.tsx | 2 + .../ListItem/ActionCell/PayActionCell.tsx | 2 + src/hooks/useHoldMenuSubmit.ts | 3 ++ src/hooks/useSearchBulkActions.ts | 16 ++++++ src/hooks/useSelectionModePayment.ts | 2 + src/libs/actions/IOU/PayMoneyRequest.ts | 17 ++++-- src/libs/actions/Search.ts | 5 ++ tests/actions/IOUTest/PayMoneyRequestTest.ts | 52 +++++++++++++++---- 11 files changed, 89 insertions(+), 15 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 5fec781e7e52..4a635fa86685 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -595,7 +595,6 @@ "../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 2 "../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-deprecated/translateLocal" 1 "../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 -"../../src/libs/actions/IOU/PayMoneyRequest.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 2 "../../src/libs/actions/IOU/PayMoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/libs/actions/IOU/PayMoneyRequest.ts" "no-restricted-imports" 1 "../../src/libs/actions/IOU/PayMoneyRequest.ts" "no-restricted-syntax" 1 diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index 941fbf534f52..feaae4fe94c3 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -209,6 +209,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo paymentMethodType: type, chatReport, invoiceReport: moneyRequestReport, + invoiceReportTransactions: allTransactions, introSelected, currentUserAccountIDParam: accountID, currentUserEmailParam: email ?? '', @@ -234,6 +235,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo paymentType: type, chatReport, iouReport: moneyRequestReport, + iouReportTransactions: allTransactions, introSelected, currentUserAccountID: accountID, currentUserLogin: currentUserLogin ?? '', diff --git a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx index 8da49d2a3f78..d3486ca92b44 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx @@ -146,6 +146,7 @@ function PayPrimaryAction({reportID, chatReportID, isDisabled}: PayPrimaryAction paymentMethodType: type, chatReport, invoiceReport: moneyRequestReport, + invoiceReportTransactions: transactions, introSelected, currentUserAccountIDParam: accountID, currentUserEmailParam: email ?? '', @@ -171,6 +172,7 @@ function PayPrimaryAction({reportID, chatReportID, isDisabled}: PayPrimaryAction paymentType: type, chatReport, iouReport: moneyRequestReport, + iouReportTransactions: transactions, introSelected, currentUserAccountID: accountID, currentUserLogin: currentUserLogin ?? '', diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx index 07856fdb2e3b..fe87858db3e8 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx @@ -91,6 +91,7 @@ function PayActionButton() { paymentMethodType: type, chatReport: currentChatReport, invoiceReport: iouReport, + invoiceReportTransactions: transactions, introSelected, currentUserAccountIDParam: currentUserAccountID, currentUserEmailParam: currentUserEmail, @@ -115,6 +116,7 @@ function PayActionButton() { paymentType: type, chatReport: currentChatReport, iouReport, + iouReportTransactions: transactions, introSelected, currentUserAccountID, currentUserLogin: currentUserDetails.login ?? '', diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 32c772abe7f4..94bfdaccaa8e 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -140,6 +140,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisab paymentMethodType: type, chatReport, invoiceReport: iouReport, + invoiceReportTransactions: transactions, introSelected, currentUserAccountIDParam: currentUserAccountID, currentUserEmailParam: email ?? '', @@ -178,6 +179,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, shouldDisab chatReport: chatReportForPayment, isFallbackChatReport, iouReport, + iouReportTransactions: transactions, introSelected, currentUserAccountID, currentUserLogin: currentUserLogin ?? '', diff --git a/src/hooks/useHoldMenuSubmit.ts b/src/hooks/useHoldMenuSubmit.ts index b21e8da14ec1..e9f51000950e 100644 --- a/src/hooks/useHoldMenuSubmit.ts +++ b/src/hooks/useHoldMenuSubmit.ts @@ -26,6 +26,7 @@ import usePayChatReportActions from './usePayChatReportActions'; import usePermissions from './usePermissions'; import {usePersonalDetail} from './usePersonalDetails'; import usePolicy from './usePolicy'; +import useReportTransactions from './useReportTransactions'; type ActionHandledType = DeepValueOf; @@ -47,6 +48,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment const activePolicy = usePolicy(activePolicyID); const policy = usePolicy(moneyRequestReport?.policyID); const chatReportPolicy = usePolicy(chatReport?.policyID); + const moneyRequestReportTransactions = useReportTransactions(moneyRequestReport?.reportID); const getChatReportActions = usePayChatReportActions(chatReport, undefined); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); @@ -111,6 +113,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment paymentType, chatReport: currentChatReport, iouReport: currentMoneyRequestReport, + iouReportTransactions: moneyRequestReportTransactions, introSelected, currentUserAccountID: currentUserDetails.accountID, currentUserLogin: currentUserDetails.login ?? '', diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 1ed5db2350b5..cb98beae143e 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1694,6 +1694,19 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { }; }); + const onyxTransactionsByReportID = new Map(); + for (const transaction of Object.values(allTransactions ?? {})) { + if (!transaction?.reportID) { + continue; + } + const reportTransactions = onyxTransactionsByReportID.get(transaction.reportID); + if (reportTransactions) { + reportTransactions.push(transaction); + } else { + onyxTransactionsByReportID.set(transaction.reportID, [transaction]); + } + } + let paidReportCount = 0; for (const item of itemsToPay) { if (!item.reportID) { @@ -1708,6 +1721,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { } const isItemInvoice = isInvoiceReport(iouReport); + const iouReportTransactions = onyxTransactionsByReportID.get(item.reportID) ?? []; const fallbackChatReportID = item.chatReportID ?? iouReport.chatReportID ?? iouReport.parentReportID; const fallbackPolicyID = iouReport.policyID ?? item.policyID; const {chatReport, isFallbackChatReport} = getChatReportWithFallback( @@ -1774,6 +1788,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { paymentMethodType: paymentItem.paymentType as PaymentMethodType, chatReport, invoiceReport: iouReport, + invoiceReportTransactions: iouReportTransactions, introSelected, currentUserAccountIDParam: accountID, currentUserEmailParam: email ?? '', @@ -1803,6 +1818,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { paymentType: paymentItem.paymentType as PaymentMethodType, chatReport, iouReport, + iouReportTransactions, introSelected, currentUserAccountID: accountID, currentUserLogin: currentUserLogin ?? '', diff --git a/src/hooks/useSelectionModePayment.ts b/src/hooks/useSelectionModePayment.ts index 60aaa6b22843..6afa44b92463 100644 --- a/src/hooks/useSelectionModePayment.ts +++ b/src/hooks/useSelectionModePayment.ts @@ -193,6 +193,7 @@ function useSelectionModePayment({ paymentMethodType: type, chatReport, invoiceReport: moneyRequestReport, + invoiceReportTransactions: transactions, introSelected, currentUserAccountIDParam: accountID, currentUserEmailParam: email ?? '', @@ -217,6 +218,7 @@ function useSelectionModePayment({ paymentType: type, chatReport, iouReport: moneyRequestReport, + iouReportTransactions: transactions, introSelected, currentUserAccountID: accountID, currentUserLogin: currentUserLogin ?? '', diff --git a/src/libs/actions/IOU/PayMoneyRequest.ts b/src/libs/actions/IOU/PayMoneyRequest.ts index 93ba0d50cf95..137b0a06b446 100644 --- a/src/libs/actions/IOU/PayMoneyRequest.ts +++ b/src/libs/actions/IOU/PayMoneyRequest.ts @@ -17,7 +17,6 @@ import { buildOptimisticCancelPaymentReportAction, buildOptimisticIOUReportAction, getReimbursableTotal, - getReportTransactions, getUnheldReimbursableTotal, hasHeldExpenses as hasHeldExpensesReportUtils, hasOutstandingChildRequest, @@ -63,6 +62,8 @@ type PayInvoiceArgs = { paymentMethodType: PaymentMethodType; chatReport: OnyxTypes.Report; invoiceReport: OnyxEntry; + /** Expenses currently on invoiceReport, used to decide the amount to pay and to unhold them. */ + invoiceReportTransactions: OnyxTypes.Transaction[]; introSelected: OnyxEntry; currentUserAccountIDParam: number; currentUserEmailParam: string; @@ -102,6 +103,8 @@ type PayMoneyRequestFunctionParams = { paymentType: PaymentMethodType; chatReport: OnyxTypes.Report; iouReport: OnyxEntry; + /** Expenses currently on iouReport, used to decide the amount to pay and to unhold them. */ + iouReportTransactions: OnyxTypes.Transaction[]; introSelected: OnyxEntry; currentUserAccountID: number; currentUserLogin: string; @@ -131,6 +134,7 @@ type PayMoneyRequestFunctionParams = { function getPayMoneyRequestParams({ initialChatReport, iouReport, + iouReportTransactions, recipient, paymentMethodType, full, @@ -158,6 +162,7 @@ function getPayMoneyRequestParams({ }: { initialChatReport: OnyxTypes.Report; iouReport: OnyxEntry; + iouReportTransactions: OnyxTypes.Transaction[]; recipient: Participant; paymentMethodType: PaymentMethodType; full: boolean; @@ -254,14 +259,13 @@ function getPayMoneyRequestParams({ chatReport = existingB2BInvoiceReport; } - const reportTransactions = getReportTransactions(iouReport?.reportID); const unheldReimbursableTotal = getUnheldReimbursableTotal(iouReport); let total = getReimbursableTotal(iouReport); - if (hasHeldExpensesReportUtils(reportTransactions) && !full && !!unheldReimbursableTotal) { + if (hasHeldExpensesReportUtils(iouReportTransactions) && !full && !!unheldReimbursableTotal) { total = unheldReimbursableTotal; } - const shouldMoveScanFailedTransactions = !!full && isExpenseReport(iouReport) && shouldSplitScanFailedTransactions(reportTransactions, iouReport); + const shouldMoveScanFailedTransactions = !!full && isExpenseReport(iouReport) && shouldSplitScanFailedTransactions(iouReportTransactions, iouReport); const optimisticIOUReportAction = buildOptimisticIOUReportAction({ type: CONST.IOU.REPORT_ACTION_TYPE.PAY, @@ -444,7 +448,6 @@ function getPayMoneyRequestParams({ // Optimistically unhold all transactions if we pay all requests if (full) { - const iouReportTransactions = getReportTransactions(iouReport?.reportID); for (const transaction of iouReportTransactions) { onyxData.optimisticData?.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -873,6 +876,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { paymentType, chatReport, iouReport, + iouReportTransactions, introSelected, currentUserAccountID, currentUserLogin, @@ -915,6 +919,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { const {params: payMoneyRequestParams, onyxData} = getPayMoneyRequestParams({ initialChatReport: chatReport, iouReport, + iouReportTransactions, recipient, paymentMethodType: paymentType, full, @@ -1121,6 +1126,7 @@ function payInvoice({ paymentMethodType, chatReport, invoiceReport, + invoiceReportTransactions, introSelected, currentUserAccountIDParam, currentUserEmailParam, @@ -1160,6 +1166,7 @@ function payInvoice({ } = getPayMoneyRequestParams({ initialChatReport: chatReport, iouReport: invoiceReport, + iouReportTransactions: invoiceReportTransactions, recipient, paymentMethodType, full: true, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 6d329ab15fe8..b1d578817555 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -380,6 +380,7 @@ function handleActionButtonPress({ conciergeChat, getCurrencyDecimals, rules, + iouReportTransactions: allReportTransactions, }); return; case CONST.SEARCH.ACTION_TYPES.APPROVE: @@ -622,6 +623,8 @@ type GetPayActionCallbackParams = { conciergeChat: OnyxEntry; getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals']; rules: OnyxCollection; + /** Expenses on the report being paid, as shown in the search results. */ + iouReportTransactions: Transaction[]; }; function getPayActionCallback({ @@ -652,6 +655,7 @@ function getPayActionCallback({ conciergeChat, getCurrencyDecimals, rules, + iouReportTransactions, }: GetPayActionCallbackParams) { if (!item.reportID) { Log.info('[SearchPay] Dropping row pay: item has no reportID'); @@ -684,6 +688,7 @@ function getPayActionCallback({ paymentType: lastPolicyPaymentMethod, chatReport: chatReportForPayment, iouReport: snapshotReport, + iouReportTransactions, introSelected, currentUserAccountID: currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserLogin: currentUserLogin ?? '', diff --git a/tests/actions/IOUTest/PayMoneyRequestTest.ts b/tests/actions/IOUTest/PayMoneyRequestTest.ts index fadb94342190..03666b907dfe 100644 --- a/tests/actions/IOUTest/PayMoneyRequestTest.ts +++ b/tests/actions/IOUTest/PayMoneyRequestTest.ts @@ -25,7 +25,7 @@ import type Transaction from '@src/types/onyx/Transaction'; import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {OnyxEntry, OnyxInputValue} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -93,6 +93,18 @@ const RORY_EMAIL = 'rory@expensifail.com'; const RORY_ACCOUNT_ID = 3; OnyxUpdateManager(); +/** Reads the expenses currently in Onyx for a report, i.e. what a component would pass as the report's transactions. */ +async function getReportTransactionsFromOnyx(reportID: string | undefined): Promise { + let allTransactions: OnyxCollection; + await getOnyxData({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + callback: (transactions) => { + allTransactions = transactions; + }, + }); + return Object.values(allTransactions ?? {}).filter((transaction): transaction is Transaction => !!transaction && transaction.reportID === reportID); +} + describe('actions/IOU/PayMoneyRequest', () => { beforeAll(() => { Onyx.init({ @@ -237,17 +249,19 @@ describe('actions/IOU/PayMoneyRequest', () => { }); }), ) - .then(() => { + .then(async () => { mockFetch?.pause?.(); if (!chatReport || !iouReport) { return waitForBatchedUpdates(); } + const iouReportTransactions = await getReportTransactionsFromOnyx(iouReport.reportID); payMoneyRequest({ isASAPSubmitBetaEnabled: false, conciergeChat: undefined, paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions, introSelected: undefined, currentUserAccountID, currentUserLogin, @@ -463,16 +477,18 @@ describe('actions/IOU/PayMoneyRequest', () => { }); }), ) - .then(() => { + .then(async () => { if (!chatReport || !expenseReport) { return waitForBatchedUpdates(); } + const iouReportTransactions = await getReportTransactionsFromOnyx(expenseReport.reportID); payMoneyRequest({ isASAPSubmitBetaEnabled: false, conciergeChat: undefined, paymentType: CONST.IOU.PAYMENT_TYPE.VBBA, chatReport, iouReport: expenseReport, + iouReportTransactions, introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -643,17 +659,19 @@ describe('actions/IOU/PayMoneyRequest', () => { }); }), ) - .then(() => { + .then(async () => { mockFetch?.fail?.(); if (!chatReport || !expenseReport) { return waitForBatchedUpdates(); } + const iouReportTransactions = await getReportTransactionsFromOnyx(expenseReport.reportID); payMoneyRequest({ isASAPSubmitBetaEnabled: false, conciergeChat: undefined, paymentType: 'ACH', chatReport, iouReport: expenseReport, + iouReportTransactions, introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -712,6 +730,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -813,6 +832,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -858,6 +878,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -971,6 +992,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport: partialPayChatReport, iouReport, + iouReportTransactions: [transaction1, transaction2], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1071,6 +1093,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport: expenseReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1173,11 +1196,12 @@ describe('actions/IOU/PayMoneyRequest', () => { return expenseReport; }; - const pay = (expenseReport: Report) => + const pay = (expenseReport: Report, iouReportTransactions: Transaction[]) => payMoneyRequest({ paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport: expenseReport, + iouReportTransactions, introSelected: undefined, currentUserAccountID: RORY_ACCOUNT_ID, currentUserLogin: RORY_EMAIL, @@ -1201,7 +1225,7 @@ describe('actions/IOU/PayMoneyRequest', () => { const scanFailedTransaction = buildTransaction('scanFailed1', 0, true); const expenseReport = await setUpReport([validTransaction, scanFailedTransaction]); - const newReportID = pay(expenseReport); + const newReportID = pay(expenseReport, [validTransaction, scanFailedTransaction]); await waitForBatchedUpdates(); expect(newReportID).toBeDefined(); @@ -1219,7 +1243,7 @@ describe('actions/IOU/PayMoneyRequest', () => { const scanFailedTransaction = buildTransaction('scanFailed1', 0, true); const expenseReport = await setUpReport([scanFailedTransaction]); - const newReportID = pay(expenseReport); + const newReportID = pay(expenseReport, [scanFailedTransaction]); await waitForBatchedUpdates(); expect(newReportID).toBeUndefined(); @@ -1232,7 +1256,7 @@ describe('actions/IOU/PayMoneyRequest', () => { const scanFailedTransaction = buildTransaction('scanFailed1', -5000, true); const expenseReport = await setUpReport([validTransaction, scanFailedTransaction]); - const newReportID = pay(expenseReport); + const newReportID = pay(expenseReport, [validTransaction, scanFailedTransaction]); await waitForBatchedUpdates(); expect(newReportID).toBeUndefined(); @@ -1268,6 +1292,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1323,6 +1348,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1498,6 +1524,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1572,6 +1599,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1636,6 +1664,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -1679,6 +1708,7 @@ describe('actions/IOU/PayMoneyRequest', () => { paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport, + iouReportTransactions: [], introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -2107,17 +2137,19 @@ describe('actions/IOU/PayMoneyRequest', () => { }, }), ) - .then(() => { + .then(async () => { // When the expense report is paid elsewhere (but really, any payment option would work) if (!chatReport || !expenseReport) { return waitForBatchedUpdates(); } + const iouReportTransactions = await getReportTransactionsFromOnyx(expenseReport.reportID); payMoneyRequest({ isASAPSubmitBetaEnabled: false, conciergeChat: undefined, paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport: expenseReport, + iouReportTransactions, introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, @@ -2623,12 +2655,14 @@ describe('actions/IOU/PayMoneyRequest', () => { // When the expense report is paid elsewhere (but really, any payment option would work) if (chatReport && expenseReport) { mockFetch?.pause?.(); + const iouReportTransactions = await getReportTransactionsFromOnyx(expenseReport.reportID); payMoneyRequest({ isASAPSubmitBetaEnabled: false, conciergeChat: undefined, paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport: expenseReport, + iouReportTransactions, introSelected: undefined, currentUserAccountID: CARLOS_ACCOUNT_ID, currentUserLogin: CARLOS_EMAIL, From de6c5a8d613b9003476c92f494ac75a83d47c00d Mon Sep 17 00:00:00 2001 From: Linh Date: Sun, 11 Oct 2026 17:10:09 +0700 Subject: [PATCH 2/4] chore: remove comment --- src/libs/actions/IOU/PayMoneyRequest.ts | 2 -- src/libs/actions/Search.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/libs/actions/IOU/PayMoneyRequest.ts b/src/libs/actions/IOU/PayMoneyRequest.ts index 137b0a06b446..6b8be8316975 100644 --- a/src/libs/actions/IOU/PayMoneyRequest.ts +++ b/src/libs/actions/IOU/PayMoneyRequest.ts @@ -62,7 +62,6 @@ type PayInvoiceArgs = { paymentMethodType: PaymentMethodType; chatReport: OnyxTypes.Report; invoiceReport: OnyxEntry; - /** Expenses currently on invoiceReport, used to decide the amount to pay and to unhold them. */ invoiceReportTransactions: OnyxTypes.Transaction[]; introSelected: OnyxEntry; currentUserAccountIDParam: number; @@ -103,7 +102,6 @@ type PayMoneyRequestFunctionParams = { paymentType: PaymentMethodType; chatReport: OnyxTypes.Report; iouReport: OnyxEntry; - /** Expenses currently on iouReport, used to decide the amount to pay and to unhold them. */ iouReportTransactions: OnyxTypes.Transaction[]; introSelected: OnyxEntry; currentUserAccountID: number; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index b1d578817555..9a88bedab095 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -623,7 +623,6 @@ type GetPayActionCallbackParams = { conciergeChat: OnyxEntry; getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals']; rules: OnyxCollection; - /** Expenses on the report being paid, as shown in the search results. */ iouReportTransactions: Transaction[]; }; From 04ef058ac1d358a4aeb1f8c86e22a5f90b684691 Mon Sep 17 00:00:00 2001 From: Linh Date: Sun, 11 Oct 2026 18:11:41 +0700 Subject: [PATCH 3/4] Use derived report transactions lookup for bulk pay instead of looping allTransactions --- src/hooks/useSearchBulkActions.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index cb98beae143e..6fb2c69ffb42 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -1694,19 +1694,6 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { }; }); - const onyxTransactionsByReportID = new Map(); - for (const transaction of Object.values(allTransactions ?? {})) { - if (!transaction?.reportID) { - continue; - } - const reportTransactions = onyxTransactionsByReportID.get(transaction.reportID); - if (reportTransactions) { - reportTransactions.push(transaction); - } else { - onyxTransactionsByReportID.set(transaction.reportID, [transaction]); - } - } - let paidReportCount = 0; for (const item of itemsToPay) { if (!item.reportID) { @@ -1721,7 +1708,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { } const isItemInvoice = isInvoiceReport(iouReport); - const iouReportTransactions = onyxTransactionsByReportID.get(item.reportID) ?? []; + const iouReportTransactions = Object.values(allReportsTransactionsAndViolations?.[item.reportID]?.transactions ?? {}); const fallbackChatReportID = item.chatReportID ?? iouReport.chatReportID ?? iouReport.parentReportID; const fallbackPolicyID = iouReport.policyID ?? item.policyID; const {chatReport, isFallbackChatReport} = getChatReportWithFallback( @@ -1875,6 +1862,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { personalPolicyID, lastPaymentMethods, allTransactions, + allReportsTransactionsAndViolations, policyIDsWithVBBA, clearSelectedTransactions, accountID, From f95a96278dc3fad074e741b8b049cdc4a8cea8d4 Mon Sep 17 00:00:00 2001 From: Linh Date: Thu, 15 Oct 2026 11:59:51 +0700 Subject: [PATCH 4/4] Rename allTransactions to moneyReportTransactions --- .../MoneyReportHeaderSecondaryActions.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index feaae4fe94c3..ea2554c1086e 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -156,7 +156,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo const {transactions: reportTransactions, violations} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID); // While offline, keep pending-delete transactions so a queued-for-delete expense still counts as a real transaction until the delete syncs. const nonPendingDeleteTransactions = Object.values(reportTransactions).filter((t) => isOffline || t.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const allTransactions = Object.values(reportTransactions); + const moneyReportTransactions = Object.values(reportTransactions); const singleTransaction = nonPendingDeleteTransactions.length === 1 ? nonPendingDeleteTransactions.at(0) : undefined; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(singleTransaction?.comment?.originalTransactionID)}`); const isTrackIntentUser = isTrackOnboardingChoice(introSelected?.choice); @@ -173,7 +173,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, true); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); - const isAnyTransactionOnHold = hasHeldExpensesReportUtils(allTransactions); + const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyReportTransactions); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); @@ -209,7 +209,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo paymentMethodType: type, chatReport, invoiceReport: moneyRequestReport, - invoiceReportTransactions: allTransactions, + invoiceReportTransactions: moneyReportTransactions, introSelected, currentUserAccountIDParam: accountID, currentUserEmailParam: email ?? '', @@ -235,7 +235,7 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo paymentType: type, chatReport, iouReport: moneyRequestReport, - iouReportTransactions: allTransactions, + iouReportTransactions: moneyReportTransactions, introSelected, currentUserAccountID: accountID, currentUserLogin: currentUserLogin ?? '', @@ -285,9 +285,10 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo !canIOUBePaid && canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, currentUserLogin ?? '', accountID, undefined, true, isChatReportArchived, invoiceReceiverPolicy); const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere; - const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => isPending(t)); + const hasOnlyPendingTransactions = moneyReportTransactions.length > 0 && moneyReportTransactions.every((t) => isPending(t)); const shouldShowApproveButton = - (canApproveIOU(moneyRequestReport, policy, reportMetadata, currentUserPersonalDetails.accountID, allTransactions) && !hasOnlyPendingTransactions) || isApprovedAnimationRunning; + (canApproveIOU(moneyRequestReport, policy, reportMetadata, currentUserPersonalDetails.accountID, moneyReportTransactions) && !hasOnlyPendingTransactions) || + isApprovedAnimationRunning; const isApproveDisabled = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport); const totalAmount = getTotalAmountForIOUReportPreviewButton(moneyRequestReport, policy, CONST.REPORT.PRIMARY_ACTIONS.PAY, nonPendingDeleteTransactions, convertToDisplayString);