diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index b4481d4cabdc..d43f333fa10c 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -69,7 +69,7 @@ function AddPaymentMethodMenu({ // Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report // which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee. const isIOU = isIOUReport(iouReport); - const canUseBusinessBankAccount = isExpenseReport(iouReport) || (isIOU && !hasRequestFromCurrentAccount(iouReport?.reportID, session?.accountID ?? CONST.DEFAULT_NUMBER_ID)); + const canUseBusinessBankAccount = isExpenseReport(iouReport) || (isIOU && !hasRequestFromCurrentAccount(iouReport, session?.accountID ?? CONST.DEFAULT_NUMBER_ID)); const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOU; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 17849830b8dd..508bee4f7ddb 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -963,7 +963,7 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt return []; } - const canUseBusinessBankAccount = moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport.reportID, accountID ?? CONST.DEFAULT_NUMBER_ID); + const canUseBusinessBankAccount = moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport, accountID ?? CONST.DEFAULT_NUMBER_ID); if (!canUseBusinessBankAccount) { return []; } diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 62d479c88ff8..19074f67ff2c 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -278,7 +278,7 @@ function SettlementButton({ }; const canUseWallet = !isExpenseReport && !isInvoiceReport && isCurrencySupportedForGlobalReimbursement(currency as CurrencyType); - const canUseBusinessBankAccount = isExpenseReport || (isIOUReport(iouReport) && reportID && !hasRequestFromCurrentAccount(reportID, accountID ?? CONST.DEFAULT_NUMBER_ID)); + const canUseBusinessBankAccount = isExpenseReport || (isIOUReport(iouReport) && reportID && !hasRequestFromCurrentAccount(iouReport, accountID ?? CONST.DEFAULT_NUMBER_ID)); const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport(iouReport); const isPersonalOnlyOption = canUsePersonalBankAccount && !canUseBusinessBankAccount; diff --git a/src/hooks/useBulkPayOptions.ts b/src/hooks/useBulkPayOptions.ts index 8c505849862f..6ddab11be5b2 100644 --- a/src/hooks/useBulkPayOptions.ts +++ b/src/hooks/useBulkPayOptions.ts @@ -74,7 +74,7 @@ function useBulkPayOptions({ const isExpenseReport = isExpenseReportUtil(selectedReportID); const isInvoiceReport = isInvoiceReportUtil(selectedReportID); const shouldShowPayElsewhereOption = !isInvoiceReport; - const canUseBusinessBankAccount = isExpenseReport || (isIOUReport && selectedReportID && !hasRequestFromCurrentAccount(selectedReportID, accountID ?? CONST.DEFAULT_NUMBER_ID)); + const canUseBusinessBankAccount = isExpenseReport || (isIOUReport && selectedReportID && !hasRequestFromCurrentAccount(iouReport, accountID ?? CONST.DEFAULT_NUMBER_ID)); const canUsePersonalBankAccount = isIOUReport; const isPersonalOnlyOption = canUsePersonalBankAccount && !canUseBusinessBankAccount; const shouldShowBusinessBankAccountOptions = isExpenseReport && !isPersonalOnlyOption; diff --git a/src/hooks/useSelectionModeReportActions.ts b/src/hooks/useSelectionModeReportActions.ts index b24de6afef1d..e7fdde1b7aaa 100644 --- a/src/hooks/useSelectionModeReportActions.ts +++ b/src/hooks/useSelectionModeReportActions.ts @@ -198,7 +198,7 @@ function useSelectionModeReportActions({ return []; } - const canUseBusinessBankAccount = report?.reportID && !hasRequestFromCurrentAccount(report.reportID, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID); + const canUseBusinessBankAccount = report?.reportID && !hasRequestFromCurrentAccount(report, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID); if (!canUseBusinessBankAccount) { return []; } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1c31711ce7a7..2e32e9f521c9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2419,7 +2419,8 @@ function getReportActionMessageFragments(translate: LocalizedTranslate, action: * @param currentAccountID * @returns */ -function hasRequestFromCurrentAccount(reportID: string | undefined, currentAccountID: number): boolean { +function hasRequestFromCurrentAccount(report: OnyxEntry, currentAccountID: number): boolean { + const reportID = report?.reportID; if (!reportID) { return false; } @@ -2427,8 +2428,8 @@ function hasRequestFromCurrentAccount(reportID: string | undefined, currentAccou const reportActions = Object.values(getAllReportActions(reportID)); if (reportActions.length === 0) { // In case the reportActions of the report have not been loaded, we will check based on the transactions. - const report = getReportOrDraftReport(reportID); - return doesReportContainRequestsFromMultipleUsers(report, true); + const resolvedReport = getReportOrDraftReport(reportID, undefined, undefined, undefined, report); + return doesReportContainRequestsFromMultipleUsers(resolvedReport, true); } return reportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.actorAccountID === currentAccountID && !isDeletedAction(action)); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 82789d2a4eb5..806bc09e8872 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1283,9 +1283,15 @@ function getChatType(report: OnyxInputOrEntry | Participant): ValueOf>, fallbackReport?: Report, reportDrafts?: OnyxCollection): OnyxEntry { - const searchReport = searchReports?.find((report) => report?.reportID === reportID); - const onyxReport = deprecatedAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; +function getReportOrDraftReport( + reportID: string | undefined, + searchReports?: Array>, + fallbackReport?: Report, + reportDrafts?: OnyxCollection, + report?: OnyxEntry, +): OnyxEntry { + const searchReport = searchReports?.find((searchItem) => searchItem?.reportID === reportID); + const onyxReport = report ?? deprecatedAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; return searchReport ?? onyxReport ?? (reportDrafts ?? allReportsDraft)?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`] ?? fallbackReport; } diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index a9e5d5f5b452..01f302427c1c 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -6068,11 +6068,10 @@ function moveIOUReportToPolicy( if (!policy || !iouReport || !isIOUReportUsingReport(iouReport)) { return; } - const reportID = iouReport.reportID; const isReimbursed = isReportManuallyReimbursed(iouReport); // We do not want to create negative amount expenses - if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID) && !isFromSettlementButton) { + if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID) && !isFromSettlementButton) { return; } @@ -6132,7 +6131,7 @@ function moveIOUReportToPolicyAndInviteSubmitter( const isReimbursed = isReportManuallyReimbursed(iouReport); // We only allow moving IOU report to a policy if it doesn't have requests from multiple users, as we do not want to create negative amount expenses - if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { + if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) { return; } diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index a74603ef8da4..8b7ee6c8f784 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -3426,6 +3426,79 @@ describe('actions/Report', () => { expect(Object.values(reportActions ?? {}).at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.MOVED); }); + it('should return undefined when iouReport is not an IOU report', () => { + const expenseReport: OnyxTypes.Report = { + ...createRandomReport(1, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + }; + const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN}; + const result = Report.moveIOUReportToPolicy(expenseReport, policy); + expect(result).toBeUndefined(); + }); + + it('should return undefined when hasRequestFromCurrentAccount is true and isFromSettlementButton is false', async () => { + const currentUserAccountID = 12; + const iouReport: OnyxTypes.Report = { + ...createRandomReport(1, undefined), + type: CONST.REPORT.TYPE.IOU, + managerID: currentUserAccountID, + }; + const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN}; + + // Create a non-deleted IOU report action from the current account so hasRequestFromCurrentAccount returns true + const iouReportAction: OnyxTypes.ReportAction = { + reportActionID: '100', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: currentUserAccountID, + created: '2024-01-01 00:00:00', + message: [{html: '$100.00 expense', text: '$100.00 expense', type: 'COMMENT'}], + } as OnyxTypes.ReportAction; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, {[iouReportAction.reportActionID]: iouReportAction}); + await waitForBatchedUpdates(); + + const result = Report.moveIOUReportToPolicy(iouReport, policy, false); + expect(result).toBeUndefined(); + }); + + it('should proceed when hasRequestFromCurrentAccount is true but isFromSettlementButton is true', async () => { + const currentUserAccountID = 12; + const ownerAccountID = 1; + const ownerEmail = 'owner@gmail.com'; + const adminEmail = 'admin@gmail.com'; + const iouReport: OnyxTypes.Report = { + ...createRandomReport(1, undefined), + type: CONST.REPORT.TYPE.IOU, + ownerAccountID, + managerID: currentUserAccountID, + }; + const policy: OnyxTypes.Policy = { + ...createRandomPolicy(1), + role: CONST.POLICY.ROLE.ADMIN, + employeeList: {[adminEmail]: {email: adminEmail, role: CONST.POLICY.ROLE.ADMIN}, [ownerEmail]: {email: ownerEmail, role: CONST.POLICY.ROLE.USER}}, + }; + + // Create a non-deleted IOU report action from the current account so hasRequestFromCurrentAccount returns true + const iouReportAction: OnyxTypes.ReportAction = { + reportActionID: '100', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: currentUserAccountID, + created: '2024-01-01 00:00:00', + message: [{html: '$100.00 expense', text: '$100.00 expense', type: 'COMMENT'}], + } as OnyxTypes.ReportAction; + + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[ownerAccountID]: {login: ownerEmail}}); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, {[iouReportAction.reportActionID]: iouReportAction}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await waitForBatchedUpdates(); + + // When isFromSettlementButton is true, it should proceed despite hasRequestFromCurrentAccount being true + const result = Report.moveIOUReportToPolicy(iouReport, policy, true); + expect(result).toBeDefined(); + expect(result?.policyExpenseChatReportID).toBeDefined(); + }); + describe('convertIOUReportToExpenseReport', () => { it('should set reportName to default formula when policy field list is empty', () => { // Given a policy with an empty field list @@ -3703,6 +3776,21 @@ describe('actions/Report', () => { expect(updatedReport?.policyID).toBe(policy.id); expect(updatedReport?.total).toBe(-3000); }); + + it('should return undefined when policy is missing', () => { + const iouReport: OnyxTypes.Report = { + ...createRandomReport(1, undefined), + type: CONST.REPORT.TYPE.IOU, + }; + const result = Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, undefined as unknown as OnyxTypes.Policy, (phone: string) => phone, {}); + expect(result).toBeUndefined(); + }); + + it('should return undefined when iouReport is missing', () => { + const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN}; + const result = Report.moveIOUReportToPolicyAndInviteSubmitter(undefined, policy, (phone: string) => phone, {}); + expect(result).toBeUndefined(); + }); }); describe('buildOptimisticChangePolicyData', () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 148ad091b59d..93dc2db7b97f 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1151,12 +1151,12 @@ describe('ReportActionsUtils', () => { }); it('should return false for a deleted IOU report action', () => { - const result = ReportActionsUtils.hasRequestFromCurrentAccount(deletedIOUReportID, currentUserAccountID); + const result = ReportActionsUtils.hasRequestFromCurrentAccount({reportID: deletedIOUReportID} as Report, currentUserAccountID); expect(result).toBe(false); }); it('should return true for an active IOU report action', () => { - const result = ReportActionsUtils.hasRequestFromCurrentAccount(activeIOUReportID, currentUserAccountID); + const result = ReportActionsUtils.hasRequestFromCurrentAccount({reportID: activeIOUReportID} as Report, currentUserAccountID); expect(result).toBe(true); }); @@ -1183,7 +1183,7 @@ describe('ReportActionsUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionFromOtherUser.transactionID}`, transactionFromOtherUser); // Then: should return true - let result = ReportActionsUtils.hasRequestFromCurrentAccount(unloadedActionsReportID, currentUserAccountID); + let result = ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, currentUserAccountID); expect(result).toBe(true); // When: all transactions from the current user account have been deleted @@ -1193,9 +1193,35 @@ describe('ReportActionsUtils', () => { }); // Then: should return false - result = ReportActionsUtils.hasRequestFromCurrentAccount(unloadedActionsReportID, currentUserAccountID); + result = ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, currentUserAccountID); expect(result).toBe(false); }); + + it('should use explicit allReports parameter when provided and reportActions are unloaded', async () => { + const unloadedActionsReportID = '6'; + const iouReport = { + type: CONST.REPORT.TYPE.IOU, + reportID: unloadedActionsReportID, + }; + const transactionFromCurrentUser = { + ...createRandomTransaction(1), + reportID: unloadedActionsReportID, + amount: -100, + }; + const transactionFromOtherUser = { + ...createRandomTransaction(2), + reportID: unloadedActionsReportID, + amount: 500, + }; + + // Set up transactions + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionFromCurrentUser.transactionID}`, transactionFromCurrentUser); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionFromOtherUser.transactionID}`, transactionFromOtherUser); + + // Pass report explicitly instead of relying on Onyx state + const result = ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, currentUserAccountID); + expect(result).toBe(true); + }); }); describe('getLastVisibleAction', () => { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 84376c6634f2..9c2e5ebee304 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -11301,6 +11301,24 @@ describe('ReportUtils', () => { const result = getReportOrDraftReport(mockReportID); expect(result).toEqual(mockOnyxReport); }); + + test('returns explicit report parameter instead of Onyx state', async () => { + const explicitReport: Report = { + ...createRandomReport(mockReportIDIndex, undefined), + reportName: 'Explicit Report', + type: CONST.REPORT.TYPE.CHAT, + }; + // Set a different report in Onyx to verify the explicit param takes precedence + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); + const result = getReportOrDraftReport(mockReportID, undefined, undefined, undefined, explicitReport); + expect(result).toEqual(explicitReport); + expect(result).not.toEqual(mockOnyxReport); + }); + + test('returns undefined when explicit report is undefined and no Onyx state', () => { + const result = getReportOrDraftReport(mockReportID, undefined, undefined, undefined, undefined); + expect(result).toBeUndefined(); + }); }); describe('buildOptimisticExpenseReport', () => {