diff --git a/__mocks__/reportData/violations.ts b/__mocks__/reportData/violations.ts index b7982210591a..73b2080730c1 100644 --- a/__mocks__/reportData/violations.ts +++ b/__mocks__/reportData/violations.ts @@ -28,6 +28,7 @@ const receiptErrorsR14932: ReceiptErrors = { source: CONST.POLICY.ID_FAKE, filename: CONST.POLICY.ID_FAKE, action: CONST.POLICY.ID_FAKE, + error: CONST.IOU.RECEIPT_ERROR, retryParams: { transactionID: RECEIPT_ERRORS_TRANSACTION_ID_R14932, source: CONST.POLICY.ID_FAKE, diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 80458c7d9372..5229f1c61706 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -2,7 +2,9 @@ import {deepEqual} from 'fast-equals'; import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import Log from '@libs/Log'; +import {getTransactionThreadReportID} from '@libs/MergeTransactionUtils'; import {getPolicyEmployeeListByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import {isOneTransactionReport} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -117,7 +119,13 @@ function SidebarOrderedReportsContextProvider({ } } if (transactionsUpdates) { - for (const key of Object.values(transactionsUpdates ?? {}).map((transaction) => `${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`)) { + // We need to select the report linked to a transaction, to properly recalculate getReceiptUploadErrorReason, which is the expense report if it is isOneTransactionReport + // or the transaction thread report if it is otherwise. + for (const key of Object.values(transactionsUpdates ?? {}).map((transaction) => + transaction?.reportID && isOneTransactionReport(chatReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`]) + ? `${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}` + : `${ONYXKEYS.COLLECTION.REPORT}${getTransactionThreadReportID(transaction)}`, + )) { reportsToUpdate.add(key); } } @@ -188,6 +196,7 @@ function SidebarOrderedReportsContextProvider({ reportNameValuePairs, reportAttributes, draftComments: reportsDrafts, + transactions, }); } else { Log.info('[useSidebarOrderedReports] building reportsToDisplay from scratch'); @@ -199,6 +208,7 @@ function SidebarOrderedReportsContextProvider({ priorityMode, reportsDrafts, transactionViolations, + transactions, reportNameValuePairs, reportAttributes, ); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5349f2ed2625..79872d4f96f5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -189,6 +189,7 @@ import { getMostRecentActiveDEWApproveFailedAction, getMostRecentActiveDEWSubmitFailedAction, getNumberOfMoneyRequests, + getOneTransactionThreadReportAction, getOneTransactionThreadReportID, getOriginalMessage, getRenamedAction, @@ -284,6 +285,7 @@ import { getTaxAmount, getTaxCode, getAmount as getTransactionAmount, + getTransactionID, getWaypoints, hasMissingSmartscanFields as hasMissingSmartscanFieldsTransactionUtils, hasNoticeTypeViolation, @@ -9232,11 +9234,34 @@ function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry< return allReportErrors; } +function getReceiptUploadErrorReason(report: Report, chatReport: OnyxEntry, reportActions: OnyxEntry, transactions: OnyxCollection) { + const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + const transactionThreadReportAction = getOneTransactionThreadReportAction(report, chatReport, reportActions ?? []); + if (transactionThreadReportAction) { + const transactionID = isMoneyRequestAction(transactionThreadReportAction) ? getOriginalMessage(transactionThreadReportAction)?.IOUTransactionID : undefined; + const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (hasReceiptError(transaction)) { + return { + reason: CONST.RBR_REASONS.HAS_ERRORS, + }; + } + } + const transactionID = getTransactionID(report); + const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + if (isTransactionThread(parentReportAction) && hasReceiptError(transaction)) { + return { + reason: CONST.RBR_REASONS.HAS_ERRORS, + }; + } + return null; +} + function hasReportErrorsOtherThanFailedReceipt( report: Report, chatReport: OnyxEntry, doesReportHaveViolations: boolean, transactionViolations: OnyxCollection, + transactions: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { const allReportErrors = reportAttributes?.[report?.reportID]?.reportErrors ?? {}; @@ -9251,7 +9276,8 @@ function hasReportErrorsOtherThanFailedReceipt( doesTransactionThreadReportHasViolations || doesReportHaveViolations || // eslint-disable-next-line @typescript-eslint/no-deprecated - Object.values(allReportErrors).some((error) => error?.[0] !== translateLocal('iou.error.genericSmartscanFailureMessage')) + Object.values(allReportErrors).some((error) => error?.[0] !== translateLocal('iou.error.genericSmartscanFailureMessage')) || + !!getReceiptUploadErrorReason(report, chatReport, transactionReportActions, transactions) ); } @@ -13232,6 +13258,7 @@ export { getReportStatusColorStyle, getMovedActionMessage, excludeParticipantsForDisplay, + getReceiptUploadErrorReason, getAncestors, // This will be fixed as follow up https://github.com/Expensify/App/pull/75357 // eslint-disable-next-line @typescript-eslint/no-deprecated diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index bc509a066f2e..10448c92bf2e 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -47,7 +47,6 @@ import { getInvoiceCompanyWebsiteUpdateMessage, getLastVisibleMessage, getMessageOfOldDotReportAction, - getOneTransactionThreadReportAction, getOriginalMessage, getPlaidBalanceFailureMessage, getPolicyChangeLogAddEmployeeMessage, @@ -113,12 +112,10 @@ import { isActionOfType, isCardIssuedAction, isInviteOrRemovedAction, - isMoneyRequestAction, isOldDotReportAction, isRenamedAction, isTagModificationAction, isTaskAction, - isTransactionThread, } from './ReportActionsUtils'; import type {OptionData} from './ReportUtils'; import { @@ -133,6 +130,7 @@ import { getMovedTransactionMessage, getParticipantsAccountIDsForDisplay, getPolicyName, + getReceiptUploadErrorReason, getReportDescription, getReportMetadata, // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -142,7 +140,6 @@ import { getReportSubtitlePrefix, getUnreportedTransactionMessage, getWorkspaceNameUpdatedMessage, - hasReceiptError, hasReportErrorsOtherThanFailedReceipt, isAdminRoom, isAnnounceRoom, @@ -176,7 +173,6 @@ import { shouldReportShowSubscript, } from './ReportUtils'; import {getTaskReportActionMessage} from './TaskUtils'; -import {getTransactionID} from './TransactionUtils'; type WelcomeMessage = {phrase1?: string; messageText?: string; messageHtml?: string}; @@ -208,6 +204,7 @@ function shouldDisplayReportInLHN( betas: OnyxEntry, transactionViolations: OnyxCollection, draftComment: OnyxEntry, + transactions: OnyxCollection, isReportArchived?: boolean, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -226,7 +223,7 @@ function shouldDisplayReportInLHN( const isFocused = report.reportID === currentReportId; const chatReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; const parentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; - const hasErrorsOtherThanFailedReceipt = hasReportErrorsOtherThanFailedReceipt(report, chatReport, doesReportHaveViolations, transactionViolations, reportAttributes); + const hasErrorsOtherThanFailedReceipt = hasReportErrorsOtherThanFailedReceipt(report, chatReport, doesReportHaveViolations, transactionViolations, transactions, reportAttributes); const isReportInAccessible = report?.errorFields?.notFound; if (isOneTransactionThread(report, parentReport, parentReportAction)) { return {shouldDisplay: false}; @@ -278,6 +275,7 @@ function getReportsToDisplayInLHN( priorityMode: OnyxEntry, draftComments: OnyxCollection, transactionViolations: OnyxCollection, + transactions: OnyxCollection, reportNameValuePairs?: OnyxCollection, reportAttributes?: ReportAttributesDerivedValue['reports'], ) { @@ -300,6 +298,7 @@ function getReportsToDisplayInLHN( betas, transactionViolations, reportDraftComment, + transactions, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]), reportAttributes, ); @@ -325,6 +324,7 @@ type UpdateReportsToDisplayInLHNProps = { reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; draftComments: OnyxCollection; + transactions: OnyxCollection; }; function updateReportsToDisplayInLHN({ @@ -338,6 +338,7 @@ function updateReportsToDisplayInLHN({ reportNameValuePairs, reportAttributes, draftComments, + transactions, }: UpdateReportsToDisplayInLHNProps) { const displayedReportsCopy = {...displayedReports}; for (const reportID of updatedReportsKeys) { @@ -359,6 +360,7 @@ function updateReportsToDisplayInLHN({ betas, transactionViolations, reportDraftComment, + transactions, isArchivedReport(reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`] ?? {}), reportAttributes, ); @@ -618,27 +620,8 @@ function getReasonAndReportActionThatHasRedBrickRoad( reason: CONST.RBR_REASONS.HAS_VIOLATIONS, }; } - const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); - const transactionThreadReportAction = getOneTransactionThreadReportAction(report, chatReport, reportActions ?? []); - - if (transactionThreadReportAction) { - const transactionID = isMoneyRequestAction(transactionThreadReportAction) ? getOriginalMessage(transactionThreadReportAction)?.IOUTransactionID : undefined; - const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (hasReceiptError(transaction)) { - return { - reason: CONST.RBR_REASONS.HAS_ERRORS, - }; - } - } - const transactionID = getTransactionID(report); - const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - if (isTransactionThread(parentReportAction) && hasReceiptError(transaction)) { - return { - reason: CONST.RBR_REASONS.HAS_ERRORS, - }; - } - return null; + return getReceiptUploadErrorReason(report, chatReport, reportActions, transactions); } function shouldShowRedBrickRoad( @@ -1284,4 +1267,5 @@ export default { shouldShowRedBrickRoad, getReportsToDisplayInLHN, updateReportsToDisplayInLHN, + shouldDisplayReportInLHN, }; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 58fcb8c57f9f..df5a83109aa0 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -255,6 +255,9 @@ type ReceiptError = { /** Parameters required to retry the failed action */ retryParams: StartSplitBilActionParams | CreateTrackExpenseParams | RequestMoneyInformation | ReplaceReceipt; + + /** The type of receipt error */ + error: typeof CONST.IOU.RECEIPT_ERROR; }; /** Collection of receipt errors, indexed by a UNIX timestamp of when the error occurred */ diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 348f9d055308..c7aaecf55c24 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -100,11 +100,11 @@ describe('SidebarUtils', () => { test('[SidebarUtils] getReportsToDisplayInLHN on 15k reports for default priorityMode', async () => { await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, {}, transactionViolations)); + await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.DEFAULT, {}, transactionViolations, {})); }); test('[SidebarUtils] getReportsToDisplayInLHN on 15k reports for GSD priorityMode', async () => { await waitForBatchedUpdates(); - await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, {}, transactionViolations)); + await measureFunction(() => SidebarUtils.getReportsToDisplayInLHN(currentReportId, allReports, mockedBetas, policies, CONST.PRIORITY_MODE.GSD, {}, transactionViolations, {})); }); }); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index a5b424dbac22..90085df72e52 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -531,6 +531,183 @@ describe('SidebarUtils', () => { }); }); + describe('shouldDisplayReportInLHN', () => { + it('returns shouldDisplay as true if a one transaction thread expense report has receipt upload error', async () => { + const MOCK_REPORT: Report = { + reportID: '1', + ownerAccountID: 12345, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + policyID: '6', + type: CONST.REPORT.TYPE.EXPENSE, + }; + + const MOCK_REPORTS: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT, + }; + + const MOCK_REPORT_ACTIONS: ReportActions = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: '1', + type: 'create', + }, + message: [{type: 'COMMENT', html: '10.00 expense', text: '10.00 expense'}], + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + }, + }; + + const MOCK_TRANSACTION: Transaction = { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: MOCK_REPORT.reportID, + errors: { + '1772030710775000': { + error: CONST.IOU.RECEIPT_ERROR, + source: '', + filename: 'download.jpeg', + action: 'replaceReceipt', + retryParams: {transactionID: '', source: '', transactionPolicy: undefined}, + }, + }, + created: '2024-08-08 18:20:44.171', + currency: 'USD', + merchant: 'merchant', + }; + + const MOCK_TRANSACTIONS = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, + } as OnyxCollection; + + await act(async () => { + await Onyx.multiSet({ + ...MOCK_REPORTS, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT_ACTIONS, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, + }); + }); + + const result = SidebarUtils.shouldDisplayReportInLHN(MOCK_REPORT, MOCK_REPORTS as OnyxCollection, undefined, true, undefined, {}, undefined, MOCK_TRANSACTIONS); + + expect(result).toStrictEqual({shouldDisplay: true, hasErrorsOtherThanFailedReceipt: true}); + }); + + it('returns shouldDisplay as true if transaction thread report has receipt upload error', async () => { + const MOCK_REPORT: Report = { + reportID: '1', + ownerAccountID: 12345, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + policyID: '6', + type: CONST.REPORT.TYPE.EXPENSE, + }; + + const MOCK_TRANSACTION_THREAD_REPORT: Report = { + reportID: '2', + ownerAccountID: 12345, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policyID: '6', + parentReportID: MOCK_REPORT.reportID, + parentReportActionID: '1', + type: CONST.REPORT.TYPE.CHAT, + }; + + const MOCK_REPORTS: ReportCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.REPORT}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT, + [`${ONYXKEYS.COLLECTION.REPORT}${MOCK_TRANSACTION_THREAD_REPORT.reportID}` as const]: MOCK_TRANSACTION_THREAD_REPORT, + }; + + const MOCK_REPORT_ACTIONS: ReportActions = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '1': { + reportActionID: '1', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: '1', + type: 'create', + }, + message: [{type: 'COMMENT', html: '10.00 expense', text: '10.00 expense'}], + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + }, + '2': { + reportActionID: '2', + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: '2', + type: 'create', + }, + message: [{type: 'COMMENT', html: '10.00 expense', text: '10.00 expense'}], + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + }, + }; + + const MOCK_TRANSACTION: Transaction = { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: MOCK_REPORT.reportID, + errors: { + '1772030710775000': { + error: CONST.IOU.RECEIPT_ERROR, + source: '', + filename: 'download.jpeg', + action: 'replaceReceipt', + retryParams: {transactionID: '', source: '', transactionPolicy: undefined}, + }, + }, + created: '2024-08-08 18:20:44.171', + currency: 'USD', + merchant: 'merchant', + }; + + const MOCK_TRANSACTION2: Transaction = { + transactionID: '2', + amount: 10, + modifiedAmount: 10, + reportID: MOCK_REPORT.reportID, + created: '2024-08-08 18:20:44.171', + currency: 'USD', + merchant: 'merchant', + }; + + const MOCK_TRANSACTIONS = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION2.transactionID}` as const]: MOCK_TRANSACTION2, + } as OnyxCollection; + + await act(async () => { + await Onyx.multiSet({ + ...MOCK_REPORTS, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT_ACTIONS, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION2.transactionID}` as const]: MOCK_TRANSACTION2, + }); + }); + + const result = SidebarUtils.shouldDisplayReportInLHN( + MOCK_TRANSACTION_THREAD_REPORT, + MOCK_REPORTS as OnyxCollection, + undefined, + true, + undefined, + {}, + undefined, + MOCK_TRANSACTIONS, + ); + + expect(result).toStrictEqual({shouldDisplay: true, hasErrorsOtherThanFailedReceipt: true}); + }); + }); + describe('shouldShowRedBrickRoad', () => { it('returns true when report has transaction thread violations', async () => { const MOCK_REPORT: Report = {