diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bd4bc2c4cc13..fc79d251ffd4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -70,7 +70,7 @@ import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/ony import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; -import {createDraftTransaction, getIOUReportActionToApproveOrPay, setMoneyRequestParticipants, unholdRequest} from './actions/IOU'; +import {canApproveIOU, canIOUBePaid, canSubmitReport, createDraftTransaction, getIOUReportActionToApproveOrPay, setMoneyRequestParticipants, unholdRequest} from './actions/IOU'; import {createDraftWorkspace} from './actions/Policy/Policy'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; import {handleReportChanged} from './actions/Report'; @@ -2547,6 +2547,30 @@ function isMoneyRequestReportEligibleForMerge(reportID: string, isAdmin: boolean return isManager && isExpenseReport(report) && isProcessingReport(report); } +function hasOutstandingChildRequest(chatReport: Report, iouReportOrID: OnyxEntry | string) { + const reportActions = getAllReportActions(chatReport.reportID); + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line deprecation/deprecation + const policy = getPolicy(chatReport.policyID); + return Object.values(reportActions).some((action) => { + const iouReportID = getIOUReportIDFromReportActionPreview(action); + if ( + !iouReportID || + action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || + isDeletedAction(action) || + (typeof iouReportOrID === 'string' && iouReportID === iouReportOrID) + ) { + return false; + } + + const iouReport = typeof iouReportOrID !== 'string' && iouReportOrID?.reportID === iouReportID ? iouReportOrID : getReportOrDraftReport(iouReportID); + const transactions = getReportTransactions(iouReportID); + return ( + canIOUBePaid(iouReport, chatReport, policy, transactions) || canApproveIOU(iouReport, policy, transactions) || canSubmitReport(iouReport, policy, transactions, undefined, false) + ); + }); +} + /** * Checks whether the card transaction support deleting based on liability type */ @@ -11766,6 +11790,7 @@ export { getArchiveReason, getApprovalChain, isIndividualInvoiceRoom, + hasOutstandingChildRequest, isAuditor, hasMissingInvoiceBankAccount, reasonForReportToBeInOptionList, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a16c6d68a272..b4fa5f5f6c27 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -89,7 +89,6 @@ import { import { getAllReportActions, getIOUActionForReportID, - getIOUReportIDFromReportActionPreview, getLastVisibleAction, getLastVisibleMessage, getOneTransactionThreadReportID, @@ -156,6 +155,7 @@ import { getTransactionDetails, hasHeldExpenses as hasHeldExpensesReportUtils, hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils, + hasOutstandingChildRequest, hasReportBeenReopened, hasReportBeenRetracted, isArchivedReport, @@ -8281,12 +8281,12 @@ function deleteMoneyRequest( }); } - if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { + if (chatReport && updatedIOUReport && !shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: { - hasOutstandingChildRequest: false, + hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, updatedIOUReport), }, }); } @@ -8308,16 +8308,18 @@ function deleteMoneyRequest( reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}, )?.created; - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText, - lastVisibleActionCreated, - }, - }); + if (chatReport) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, iouReport?.reportID), + iouReportID: null, + lastMessageText, + lastVisibleActionCreated, + }, + }); + } optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, @@ -9138,25 +9140,6 @@ function getReportFromHoldRequestsOnyxData( }; } -function hasOutstandingChildRequest(chatReport: OnyxTypes.Report, excludedIOUReport: OnyxEntry, policyId?: string) { - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line deprecation/deprecation - const policy = getPolicy(policyId); - if (!policy?.achAccount?.bankAccountID) { - return false; - } - const reportActions = getAllReportActions(chatReport.reportID); - return !!Object.values(reportActions).find((action) => { - const iouReportID = getIOUReportIDFromReportActionPreview(action); - if (iouReportID === excludedIOUReport?.reportID) { - return false; - } - const iouReport = getReportOrDraftReport(iouReportID); - const transactions = getReportTransactions(iouReportID); - return canIOUBePaid(iouReport, chatReport, policy, transactions) || canIOUBePaid(iouReport, chatReport, policy, transactions, true); - }); -} - function getPayMoneyRequestParams( initialChatReport: OnyxTypes.Report, iouReport: OnyxEntry, @@ -9254,7 +9237,7 @@ function getPayMoneyRequestParams( const optimisticChatReport = { ...chatReport, lastReadTime: DateUtils.getDBTime(), - hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, iouReport, iouReport?.policyID), + hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, iouReport?.reportID), iouReportID: null, lastMessageText: getReportActionText(optimisticIOUReportAction), lastMessageHtml: getReportActionHtml(optimisticIOUReportAction), @@ -9668,10 +9651,6 @@ function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry, updatedIouReport: OnyxEntry): boolean { - return !!getIOUReportActionToApproveOrPay(chatReport, updatedIouReport); -} - function isLastApprover(approvalChain: string[]): boolean { if (approvalChain.length === 0) { return true; @@ -9736,20 +9715,23 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: value: updatedExpenseReport, }; - const optimisticChatReportData: OnyxUpdate = { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.chatReportID}`, - value: { - hasOutstandingChildRequest: hasIOUToApproveOrPay(chatReport, updatedExpenseReport), - }, - }; + let optimisticChatReportData: OnyxUpdate | undefined; + if (chatReport) { + optimisticChatReportData = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.chatReportID}`, + value: { + hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, updatedExpenseReport), + }, + }; + } const optimisticNextStepData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: optimisticNextStep, }; - const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData, optimisticNextStepData, optimisticChatReportData]; + const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData, optimisticNextStepData, ...(optimisticChatReportData ? [optimisticChatReportData] : [])]; const successData: OnyxUpdate[] = [ { @@ -12678,6 +12660,7 @@ export { assignReportToMe, getPerDiemExpenseInformation, getSendInvoiceInformation, + hasOutstandingChildRequest, }; export type { GPSPoint as GpsPoint, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 0135af97f81a..05248a4c5794 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -140,11 +140,13 @@ import { getReportLastVisibleActionCreated, getReportMetadata, getReportNotificationPreference, + getReportOrDraftReport, getReportPreviewMessage, getReportTransactions, getReportViolations, getRouteFromLink, getTitleReportField, + hasOutstandingChildRequest, isChatThread as isChatThreadReportUtils, isConciergeChatReport, isExpenseReport, @@ -5121,11 +5123,14 @@ function deleteAppReport(reportID: string | undefined) { }); } - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, - value: {hasOutstandingChildRequest: false}, - }); + const chatReport = getReportOrDraftReport(report?.parentReportID); + if (chatReport) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, + value: {hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, report?.reportID)}, + }); + } failureData.push({ onyxMethod: Onyx.METHOD.MERGE, diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 0c537f5a9686..8c8caaffd541 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -25,6 +25,7 @@ import createCollection from '../utils/collections/createCollection'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; import {createRandomReport} from '../utils/collections/reports'; +import createRandomTransaction from '../utils/collections/transaction'; import getIsUsingFakeTimers from '../utils/getIsUsingFakeTimers'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -1832,6 +1833,101 @@ describe('actions/Report', () => { // The length is 3 to include the CREATED action expect(Object.keys(selfDMReportActions ?? {}).length).toBe(3); }); + + it('should not reset the chatReport hasOutstandingChildRequest if there is another outstanding report', async () => { + const currentUserAccountID = 1; + const fakePolicy: OnyxTypes.Policy = { + ...createRandomPolicy(6), + role: 'admin', + ownerAccountID: currentUserAccountID, + areRulesEnabled: true, + preventSelfApproval: false, + autoReportingFrequency: 'immediate', + harvesting: { + enabled: false, + }, + }; + const chatReport: OnyxTypes.Report = {...createRandomReport(11), policyID: fakePolicy.id, chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, hasOutstandingChildRequest: true}; + + const expenseReport1: OnyxTypes.Report = { + ...createRandomReport(5), + type: CONST.REPORT.TYPE.EXPENSE, + managerID: currentUserAccountID, + ownerAccountID: currentUserAccountID, + policyID: fakePolicy.id, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + chatReportID: chatReport.reportID, + parentReportID: chatReport.reportID, + }; + const reportPreview1: OnyxTypes.ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + originalMessage: { + linkedReportID: expenseReport1.reportID, + }, + }; + const expenseReport2: OnyxTypes.Report = { + ...createRandomReport(6), + type: CONST.REPORT.TYPE.EXPENSE, + managerID: currentUserAccountID, + ownerAccountID: currentUserAccountID, + policyID: fakePolicy.id, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + chatReportID: chatReport.reportID, + parentReportID: chatReport.reportID, + }; + const transaction: OnyxTypes.Transaction = {...createRandomTransaction(22), reportID: expenseReport2.reportID}; + const reportPreview2: OnyxTypes.ReportAction = { + ...createRandomReportAction(22), + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + originalMessage: { + linkedReportID: expenseReport2.reportID, + }, + }; + const iouAction1: OnyxTypes.ReportAction = { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: DateUtils.getDBTime(), + originalMessage: { + amount: 100, + currency: CONST.CURRENCY.USD, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport1.reportID}`, { + [iouAction1.reportActionID]: iouAction1, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { + [reportPreview1.reportActionID]: reportPreview1, + [reportPreview2.reportActionID]: reportPreview2, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport1.reportID}`, expenseReport1); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport2.reportID}`, expenseReport2); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + + // When deleting the first expense report + Report.deleteAppReport(expenseReport1.reportID); + await waitForBatchedUpdates(); + + const report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + callback: (val) => { + Onyx.disconnect(connection); + resolve(val); + }, + }); + }); + + // The hasOutstandingChildRequest should still remain true as there is a second outstanding report. + expect(report?.hasOutstandingChildRequest).toBe(true); + }); }); describe('changeReportPolicy', () => { diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index 9a05ba6ad992..459600e1e815 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -69,7 +69,6 @@ export default function createRandomReportAction(index: number): ReportAction { automatic: randBoolean(), shouldShow: randBoolean(), lastModified: getRandomDate(), - pendingAction: rand(Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)), delegateAccountID: index, errors: {}, isAttachmentOnly: randBoolean(),