diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 855f2805908e8..cca795cfa5e53 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -92,6 +92,10 @@ function Badge({ [styles.defaultBadge, styles.condensedBadge, styles.alignSelfCenter, styles.ml2, StyleUtils, success, error, environment, badgeStyles, isCondensed, isStrong], ); + if (!text && !icon) { + return null; + } + return ( {!!icon && ( - + )} - - {text} - + {!!text && ( + + {text} + + )} ); } diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 1165c5da5e505..378d98f906fca 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -450,7 +450,7 @@ function OptionRowLHN({ ) : ( , policy: OnyxEntry, isTimeRequest = false, } = config; const topmostReportId = Navigation.getTopmostReportId(); - const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(option.item, transactionViolations); + const doesReportHaveViolations = !!getViolatingReportIDForRBRInLHN(option.item, transactionViolations); const shouldBeInOptionList = shouldReportBeInOptionList({ report: option.item, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6a63dc83c6e87..2bdc525869335 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -846,6 +846,7 @@ type OptionData = { allReportErrors?: Errors; brickRoadIndicator?: ValueOf | '' | null; actionBadge?: ValueOf; + actionTargetReportActionID?: string; tooltipText?: string | null; alternateTextMaxLines?: number; boldStyle?: boolean; @@ -9091,67 +9092,51 @@ function shouldHideReport( } /** - * Should we display a RBR on the LHN on this report due to violations? + * Returns the reportID of the first child expense report that has violations under the same policy, + * or undefined if none found. Used to find the REPORT_PREVIEW action to deep-link to. */ -function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionViolations: OnyxCollection): boolean { +function getViolatingReportIDForRBRInLHN(report: OnyxEntry, transactionViolations: OnyxCollection): string | null { // We only show the RBR in the highest level, which is the expense chat if (!report || !isPolicyExpenseChat(report)) { - return false; + return null; } // We only show the RBR to the submitter if (!isCurrentUserSubmitter(report)) { - return false; + return null; } if (!report.policyID || !reportsByPolicyID) { - return false; + return null; } // If any report has a violation, then it should have a RBR const potentialReports = Object.values(reportsByPolicyID[report.policyID] ?? {}) ?? []; - return potentialReports.some((potentialReport) => { - if (!potentialReport) { - return false; - } - const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${potentialReport.policyID}`]; - const transactions = getReportTransactions(potentialReport.reportID); + const violatingReport = potentialReports + // eslint-disable-next-line rulesdir/prefer-locale-compare-from-context + .sort((a, b) => (a?.created ?? '').localeCompare(b?.created ?? '')) + .find((potentialReport) => { + if (!potentialReport) { + return false; + } + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${potentialReport.policyID}`]; + const transactions = getReportTransactions(potentialReport.reportID); - // Allow both open and processing reports to show RBR for violations - if (!isOpenOrProcessingReport(potentialReport)) { - return false; - } + // Allow both open and processing reports to show RBR for violations + if (!isOpenOrProcessingReport(potentialReport)) { + return false; + } - return ( - !isInvoiceReport(potentialReport) && - ViolationsUtils.hasVisibleViolationsForUser( - potentialReport, - transactionViolations, - currentUserEmail ?? '', - currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, - policy, - transactions, - ) && - (hasViolations( - potentialReport.reportID, - transactionViolations, - currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, - currentUserEmail ?? '', - true, - transactions, - potentialReport, - policy, - ) || - hasWarningTypeViolations( - potentialReport.reportID, + return ( + !isInvoiceReport(potentialReport) && + ViolationsUtils.hasVisibleViolationsForUser( + potentialReport, transactionViolations, - currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, currentUserEmail ?? '', - true, - transactions, - potentialReport, + currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, policy, - ) || - hasNoticeTypeViolations( + transactions, + ) && + (hasViolations( potentialReport.reportID, transactionViolations, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, @@ -9160,9 +9145,30 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV transactions, potentialReport, policy, - )) - ); - }); + ) || + hasWarningTypeViolations( + potentialReport.reportID, + transactionViolations, + currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, + currentUserEmail ?? '', + true, + transactions, + potentialReport, + policy, + ) || + hasNoticeTypeViolations( + potentialReport.reportID, + transactionViolations, + currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID, + currentUserEmail ?? '', + true, + transactions, + potentialReport, + policy, + )) + ); + }); + return violatingReport ? violatingReport.reportID : null; } /** @@ -9393,7 +9399,7 @@ function hasReportErrorsOtherThanFailedReceipt( let doesTransactionThreadReportHasViolations = false; if (oneTransactionThreadReportID) { const transactionReport = getReport(oneTransactionThreadReportID, allReports); - doesTransactionThreadReportHasViolations = !!transactionReport && shouldDisplayViolationsRBRInLHN(transactionReport, transactionViolations); + doesTransactionThreadReportHasViolations = !!transactionReport && !!getViolatingReportIDForRBRInLHN(transactionReport, transactionViolations); } return ( doesTransactionThreadReportHasViolations || @@ -12741,16 +12747,17 @@ function generateReportAttributes({ transactionViolations: OnyxCollection; isReportArchived: boolean; actionBadge?: ValueOf; + actionTargetReportActionID?: string; }) { const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`]; const parentReportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`]; - const hasViolationsToDisplayInLHN = shouldDisplayViolationsRBRInLHN(report, transactionViolations); + const hasViolationsToDisplayInLHN = !!getViolatingReportIDForRBRInLHN(report, transactionViolations); const hasAnyTypeOfViolations = hasViolationsToDisplayInLHN; const reportErrors = getAllReportErrors(report, reportActionsList, isReportArchived); const hasErrors = Object.entries(reportErrors ?? {}).length > 0; const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList); const parentReportAction = report?.parentReportActionID ? parentReportActionsList?.[report.parentReportActionID] : undefined; - const {reason, actionBadge} = getReasonAndReportActionThatRequiresAttention(report, parentReportAction, isReportArchived) ?? {}; + const {reason, actionBadge, reportAction} = getReasonAndReportActionThatRequiresAttention(report, parentReportAction, isReportArchived) ?? {}; return { hasViolationsToDisplayInLHN, @@ -12761,6 +12768,7 @@ function generateReportAttributes({ parentReportAction, requiresAttention: !!reason, actionBadge, + actionTargetReportActionID: reportAction?.reportActionID, }; } @@ -13273,6 +13281,7 @@ export { getDisplayedReportID, getTransactionsWithReceipts, getUserDetailTooltipText, + getViolatingReportIDForRBRInLHN, getWhisperDisplayNames, getWorkspaceChats, getWorkspaceIcon, @@ -13413,7 +13422,6 @@ export { shouldDisableRename, shouldDisableThread, shouldDisplayThreadReplies, - shouldDisplayViolationsRBRInLHN, shouldReportBeInOptionList, shouldReportShowSubscript, shouldShowFlagComment, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index b45ca22a91824..9dcb53dbb4153 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -56,6 +56,7 @@ import { getIntegrationSyncFailedMessage, getInvoiceCompanyNameUpdateMessage, getInvoiceCompanyWebsiteUpdateMessage, + getIOUReportIDFromReportActionPreview, getLastVisibleMessage, getMessageOfOldDotReportAction, getOriginalMessage, @@ -151,6 +152,7 @@ import { getReportParticipantsTitle, getReportSubtitlePrefix, getUnreportedTransactionMessage, + getViolatingReportIDForRBRInLHN, getWorkspaceNameUpdatedMessage, hasReportErrorsOtherThanFailedReceipt, isAdminRoom, @@ -182,7 +184,6 @@ import { isUnread, isUnreadWithMention, isWorkspaceTaskReport, - shouldDisplayViolationsRBRInLHN, shouldReportBeInOptionList, shouldReportShowSubscript, } from './ReportUtils'; @@ -246,7 +247,7 @@ function shouldDisplayReportInLHN( // Get report metadata and status const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); - const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(report, transactionViolations); + const doesReportHaveViolations = !!getViolatingReportIDForRBRInLHN(report, transactionViolations); const isHidden = isHiddenForCurrentUser(report); const isFocused = report.reportID === currentReportId; const chatReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; @@ -627,20 +628,23 @@ function getReasonAndReportActionThatHasRedBrickRoad( transactionViolations?: OnyxCollection, isReportArchived = false, ): ReasonAndReportActionThatHasRedBrickRoad | null { - const {reportAction} = getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions, isReportArchived); - const errors = reportErrors; - const hasErrors = Object.keys(errors).length !== 0; - if (isReportArchived) { return null; } - if (shouldDisplayViolationsRBRInLHN(report, transactionViolations)) { + const violatingReportID = getViolatingReportIDForRBRInLHN(report, transactionViolations); + if (violatingReportID) { + const reportPreviewAction = Object.values(reportActions ?? {}).find((action) => getIOUReportIDFromReportActionPreview(action) === violatingReportID); return { reason: CONST.RBR_REASONS.HAS_TRANSACTION_THREAD_VIOLATIONS, + reportAction: reportPreviewAction, }; } + const {reportAction} = getAllReportActionsErrorsAndReportActionThatRequiresAttention(report, reportActions, isReportArchived); + const errors = reportErrors; + const hasErrors = Object.keys(errors).length !== 0; + if (hasErrors) { return { reason: CONST.RBR_REASONS.HAS_ERRORS, @@ -657,19 +661,6 @@ function getReasonAndReportActionThatHasRedBrickRoad( return getReceiptUploadErrorReason(report, chatReport, reportActions, transactions); } -function shouldShowRedBrickRoad( - report: Report, - chatReport: OnyxEntry, - reportActions: OnyxEntry, - hasViolations: boolean, - reportErrors: Errors, - transactions: OnyxCollection, - transactionViolations?: OnyxCollection, - isReportArchived = false, -) { - return !!getReasonAndReportActionThatHasRedBrickRoad(report, chatReport, reportActions, hasViolations, reportErrors, transactions, transactionViolations, isReportArchived); -} - /** * Gets all the data necessary for rendering an OptionRowLHN component */ @@ -792,6 +783,7 @@ function getOptionData({ result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat; result.brickRoadIndicator = reportAttributes?.brickRoadStatus; result.actionBadge = reportAttributes?.actionBadge; + result.actionTargetReportActionID = reportAttributes?.actionTargetReportActionID; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; @@ -1360,7 +1352,6 @@ export default { combineReportCategories, getWelcomeMessage, getReasonAndReportActionThatHasRedBrickRoad, - shouldShowRedBrickRoad, getReportsToDisplayInLHN, updateReportsToDisplayInLHN, shouldDisplayReportInLHN, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 2360aff7e04b3..bbf477c001c58 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -209,6 +209,7 @@ export default createOnyxDerivedValueConfig({ reportErrors, oneTransactionThreadReportID, actionBadge: actionGreenBadge, + actionTargetReportActionID: actionGreenTargetReportActionID, } = generateReportAttributes({ report, chatReport, @@ -219,15 +220,28 @@ export default createOnyxDerivedValueConfig({ let brickRoadStatus; let actionBadge; + let actionTargetReportActionID; + const reasonAndReportAction = SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad( + report, + chatReport, + reportActionsList, + hasAnyViolations, + reportErrors, + transactions, + transactionViolations, + !!isReportArchived, + ); // if report has errors or violations, show red dot - if (SidebarUtils.shouldShowRedBrickRoad(report, chatReport, reportActionsList, hasAnyViolations, reportErrors, transactions, transactionViolations, !!isReportArchived)) { + if (reasonAndReportAction) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; actionBadge = CONST.REPORT.ACTION_BADGE.FIX; + actionTargetReportActionID = reasonAndReportAction.reportAction?.reportActionID; } // if report does not have error, check if it should show green dot if (brickRoadStatus !== CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && requiresAttention) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; actionBadge = actionGreenBadge; + actionTargetReportActionID = actionGreenTargetReportActionID; } acc[report.reportID] = { @@ -249,6 +263,7 @@ export default createOnyxDerivedValueConfig({ brickRoadStatus, requiresAttention, actionBadge, + actionTargetReportActionID, reportErrors, oneTransactionThreadReportID, }; diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 2ba3169c34bb2..1c16568608515 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -21,7 +21,7 @@ import DebugTabNavigator from '@libs/Navigation/DebugTabNavigator'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; -import {hasReportViolations, isReportOwner, shouldDisplayViolationsRBRInLHN} from '@libs/ReportUtils'; +import {getViolatingReportIDForRBRInLHN, hasReportViolations, isReportOwner} from '@libs/ReportUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -82,7 +82,7 @@ function DebugReportPage({ return []; } - const shouldDisplayViolations = shouldDisplayViolationsRBRInLHN(report, transactionViolations); + const shouldDisplayViolations = !!getViolatingReportIDForRBRInLHN(report, transactionViolations); const shouldDisplayReportViolations = isReportOwner(report) && hasReportViolations(reportViolations); const hasViolations = !!shouldDisplayViolations || shouldDisplayReportViolations; const {reason: reasonGBR, reportAction: reportActionGBR} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(report, isReportArchived) ?? {}; diff --git a/src/pages/inbox/sidebar/SidebarLinks.tsx b/src/pages/inbox/sidebar/SidebarLinks.tsx index 4c1bdc22ff6be..d32c65ff60a45 100644 --- a/src/pages/inbox/sidebar/SidebarLinks.tsx +++ b/src/pages/inbox/sidebar/SidebarLinks.tsx @@ -6,12 +6,14 @@ import type {ValueOf} from 'type-fest'; import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp'; +import useEnvironment from '@hooks/useEnvironment'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {setSidebarLoaded} from '@libs/actions/App'; import Navigation from '@libs/Navigation/Navigation'; +import type {OptionData} from '@libs/ReportUtils'; import {cancelSpan} from '@libs/telemetry/activeSpans'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import * as ReportActionContextMenu from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -35,6 +37,7 @@ type SidebarLinksProps = { }; function SidebarLinks({insets, optionListItems, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport}: SidebarLinksProps) { + const {isProduction} = useEnvironment(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -51,7 +54,7 @@ function SidebarLinks({insets, optionListItems, priorityMode = CONST.PRIORITY_MO * Show Report page with selected report id */ const showReportPage = useCallback( - (option: Report) => { + (option: Report & Pick) => { // Prevent opening Report page when clicking LHN row quickly after clicking FAB icon // or when clicking the active LHN row on large screens // or when continuously clicking different LHNs, only apply to small screen @@ -70,9 +73,9 @@ function SidebarLinks({insets, optionListItems, priorityMode = CONST.PRIORITY_MO cancelSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${option.reportID}`); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID, isProduction ? undefined : option.actionTargetReportActionID)); }, - [shouldUseNarrowLayout, isActiveReport], + [shouldUseNarrowLayout, isActiveReport, isProduction], ); const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index 379cf0ce8a336..80ed1e688bc04 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -33,6 +33,10 @@ type ReportAttributes = { * The action badge to display instead of the GBR/RBR dot (e.g. 'submit', 'approve', 'pay'). */ actionBadge?: ValueOf; + /** + * The reportActionID that the action badge refers to, used for deep linking when the LHN row is pressed. + */ + actionTargetReportActionID?: string; /** * The errors of the report. */ diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index fd0ca838069f2..500c09a408e61 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -96,6 +96,7 @@ import { getReportPreviewMessage, getReportStatusTranslation, getReportSubtitlePrefix, + getViolatingReportIDForRBRInLHN, getWorkspaceIcon, getWorkspaceNameUpdatedMessage, hasActionWithErrorsForTransaction, @@ -124,7 +125,6 @@ import { shouldBlockSubmitDueToStrictPolicyRules, shouldDisableRename, shouldDisableThread, - shouldDisplayViolationsRBRInLHN, shouldEnableNegative, shouldExcludeAncestorReportAction, shouldHideSingleReportField, @@ -11159,6 +11159,240 @@ describe('ReportUtils', () => { }); }); + describe('getViolatingReportIDForRBRInLHN', () => { + it('should return null for a non-policy-expense-chat report', async () => { + await Onyx.clear(); + + const regularChat: Report = { + ...createRegularChat(800, []), + ownerAccountID: currentUserAccountID, + policyID: 'policy-non-pec', + }; + + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + + const result = getViolatingReportIDForRBRInLHN(regularChat, {}); + expect(result).toBeNull(); + + await Onyx.clear(); + }); + + it('should return null when current user is not the submitter of the policy expense chat', async () => { + await Onyx.clear(); + + const otherAccountID = 999; + const chatReport: Report = { + ...createPolicyExpenseChat(801, false), + ownerAccountID: otherAccountID, + policyID: 'policy-not-submitter', + }; + + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + + const result = getViolatingReportIDForRBRInLHN(chatReport, {}); + expect(result).toBeNull(); + + await Onyx.clear(); + }); + + it('should return null when policy expense chat has no policyID', async () => { + await Onyx.clear(); + + const chatReport: Report = { + ...createPolicyExpenseChat(802), + ownerAccountID: currentUserAccountID, + policyID: undefined, + }; + + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + + const result = getViolatingReportIDForRBRInLHN(chatReport, {}); + expect(result).toBeNull(); + + await Onyx.clear(); + }); + + it('should return the violating report ID for an open expense report with violations', async () => { + await Onyx.clear(); + + const policyID = 'policy-rbr-positive'; + const chatReportID = 'chat-rbr-positive'; + const expenseReportID = 'expense-rbr-positive'; + const transactionID = 'transaction-rbr-positive'; + + const policyData: Policy = { + id: policyID, + name: 'RBR Positive Test Workspace', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + outputCurrency: CONST.CURRENCY.USD, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + employeeList: { + [currentUserEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + owner: currentUserEmail, + isPolicyExpenseChatEnabled: true, + }; + + const chatReport: Report = { + ...createPolicyExpenseChat(803), + reportID: chatReportID, + ownerAccountID: currentUserAccountID, + policyID, + iouReportID: expenseReportID, + hasOutstandingChildRequest: true, + }; + + const expenseReport: Report = { + ...createExpenseReport(804), + reportID: expenseReportID, + chatReportID, + ownerAccountID: currentUserAccountID, + managerID: 42, + policyID, + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + total: 5000, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const baseTransaction = createRandomTransaction(803); + const transaction: Transaction = { + ...baseTransaction, + transactionID, + reportID: expenseReportID, + amount: 5000, + currency: CONST.CURRENCY.USD, + status: CONST.TRANSACTION.STATUS.POSTED, + reimbursable: true, + }; + + const transactionViolationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}` as OnyxKey; + const transactionViolationsCollection: OnyxCollection = { + [transactionViolationsKey]: [ + { + name: CONST.VIOLATIONS.MISSING_CATEGORY, + type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, + }, + ], + }; + + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + + await Promise.all([ + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policyData), + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport), + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport), + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction), + Onyx.merge(transactionViolationsKey, transactionViolationsCollection[transactionViolationsKey]), + ]); + await waitForBatchedUpdates(); + + const result = getViolatingReportIDForRBRInLHN(chatReport, transactionViolationsCollection); + expect(result).toBe(expenseReportID); + + await Onyx.clear(); + }); + + it('should return null when all expense reports in the policy are closed', async () => { + await Onyx.clear(); + + const policyID = 'policy-rbr-closed'; + const chatReportID = 'chat-rbr-closed'; + const expenseReportID = 'expense-rbr-closed'; + const transactionID = 'transaction-rbr-closed'; + + const policyData: Policy = { + id: policyID, + name: 'RBR Closed Test Workspace', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + outputCurrency: CONST.CURRENCY.USD, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + employeeList: { + [currentUserEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + owner: currentUserEmail, + isPolicyExpenseChatEnabled: true, + }; + + const chatReport: Report = { + ...createPolicyExpenseChat(805), + reportID: chatReportID, + ownerAccountID: currentUserAccountID, + policyID, + iouReportID: expenseReportID, + }; + + // Closed/approved report — stateNum > 1, so it won't be in reportsByPolicyID + // and won't pass isOpenOrProcessingReport + const expenseReport: Report = { + ...createExpenseReport(806), + reportID: expenseReportID, + chatReportID, + ownerAccountID: currentUserAccountID, + managerID: 42, + policyID, + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + total: 5000, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + }; + + const baseTransaction = createRandomTransaction(805); + const transaction: Transaction = { + ...baseTransaction, + transactionID, + reportID: expenseReportID, + amount: 5000, + currency: CONST.CURRENCY.USD, + status: CONST.TRANSACTION.STATUS.POSTED, + reimbursable: true, + }; + + const transactionViolationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}` as OnyxKey; + const transactionViolationsCollection: OnyxCollection = { + [transactionViolationsKey]: [ + { + name: CONST.VIOLATIONS.MISSING_CATEGORY, + type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, + }, + ], + }; + + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await waitForBatchedUpdates(); + + await Promise.all([ + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policyData), + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport), + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport), + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction), + Onyx.merge(transactionViolationsKey, transactionViolationsCollection[transactionViolationsKey]), + ]); + await waitForBatchedUpdates(); + + const result = getViolatingReportIDForRBRInLHN(chatReport, transactionViolationsCollection); + expect(result).toBeNull(); + + await Onyx.clear(); + }); + }); + it('should surface a GBR for admin with held expenses requiring approval or payment and avoid showing an RBR', async () => { await Onyx.clear(); @@ -11255,7 +11489,7 @@ describe('ReportUtils', () => { ]); await waitForBatchedUpdates(); - const shouldShowRBR = shouldDisplayViolationsRBRInLHN(chatReport, transactionViolationsCollection); + const shouldShowRBR = !!getViolatingReportIDForRBRInLHN(chatReport, transactionViolationsCollection); expect(shouldShowRBR).toBe(false); const reason = reasonForReportToBeInOptionList({ @@ -11370,7 +11604,7 @@ describe('ReportUtils', () => { ]); await waitForBatchedUpdates(); - const shouldShowRBR = shouldDisplayViolationsRBRInLHN(chatReport, transactionViolationsCollection); + const shouldShowRBR = !!getViolatingReportIDForRBRInLHN(chatReport, transactionViolationsCollection); expect(shouldShowRBR).toBe(false); await Onyx.clear(); }); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 6f55a745dbd15..109efae48ecda 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -533,6 +533,166 @@ describe('SidebarUtils', () => { await Onyx.clear(); }); + + it('returns correct reason when report has transaction thread notice type violation', 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', + }; + + 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, + actorAccountID: 12345, + created: '2024-08-08 18:20:44.171', + }, + }; + + const MOCK_TRANSACTION = { + transactionID: '1', + amount: 10, + modifiedAmount: 10, + reportID: MOCK_REPORT.reportID, + }; + + const MOCK_TRANSACTIONS = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, + } as OnyxCollection; + + const MOCK_TRANSACTION_VIOLATIONS: TransactionViolationsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION.transactionID}` as const]: [ + { + type: CONST.VIOLATION_TYPES.NOTICE, + name: CONST.VIOLATIONS.MODIFIED_AMOUNT, + showInReview: true, + }, + ], + }; + + await act(async () => { + await Onyx.multiSet({ + ...MOCK_REPORTS, + ...MOCK_TRANSACTION_VIOLATIONS, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT_ACTIONS, + [ONYXKEYS.SESSION]: { + accountID: 12345, + }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, + }); + }); + + const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); + const result = SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad( + MOCK_REPORT, + chatReportR14932, + MOCK_REPORT_ACTIONS, + false, + {}, + MOCK_TRANSACTIONS, + MOCK_TRANSACTION_VIOLATIONS as OnyxCollection, + isReportArchived.current, + ); + + expect(result).not.toBeNull(); + expect(result?.reason).toBe(CONST.RBR_REASONS.HAS_TRANSACTION_THREAD_VIOLATIONS); + }); + + it('returns correct reason when submitter has held expenses even if outstanding tasks trigger GBR', async () => { + const policyID = generateReportID(); + const expenseChatID = generateReportID(); + const expenseReportID = generateReportID(); + const holdReportActionID = generateReportID(); + + const policyExpenseChat: Report = { + reportID: expenseChatID, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + type: CONST.REPORT.TYPE.CHAT, + ownerAccountID: 12345, + policyID, + hasOutstandingChildRequest: true, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const expenseReport: Report = { + reportID: expenseReportID, + chatReportID: expenseChatID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: 12345, + managerID: 12345, + policyID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const baseTransaction = createRandomTransaction(700); + const transactionID = generateTransactionID(); + const transaction: Transaction = { + ...baseTransaction, + transactionID, + reportID: expenseReport.reportID, + amount: 12345, + currency: CONST.CURRENCY.USD, + status: CONST.TRANSACTION.STATUS.POSTED, + comment: { + ...(baseTransaction.comment ?? {}), + hold: holdReportActionID, + }, + }; + + const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const; + const transactionViolationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}` as const; + const transactionViolations: OnyxCollection = { + [transactionViolationsKey]: [ + { + name: CONST.VIOLATIONS.HOLD, + type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, + }, + ], + }; + + await act(async () => { + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: { + accountID: 12345, + }, + [`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat, + [`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`]: expenseReport, + [transactionKey]: transaction, + [transactionViolationsKey]: transactionViolations[transactionViolationsKey], + } as unknown as OnyxMultiSetInput); + }); + + await waitForBatchedUpdatesWithAct(); + + const requiresAttention = getReasonAndReportActionThatRequiresAttention(policyExpenseChat); + expect(requiresAttention?.reason).toBe(CONST.REQUIRES_ATTENTION_REASONS.HAS_CHILD_REPORT_AWAITING_ACTION); + + const {reason} = + SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad( + policyExpenseChat, + policyExpenseChat, + {} as OnyxEntry, + true, + {}, + {[transactionKey]: transaction}, + transactionViolations, + false, + ) ?? {}; + + expect(reason).toBe(CONST.RBR_REASONS.HAS_TRANSACTION_THREAD_VIOLATIONS); + }); }); describe('shouldDisplayReportInLHN', () => { @@ -712,500 +872,6 @@ describe('SidebarUtils', () => { }); }); - describe('shouldShowRedBrickRoad', () => { - it('returns true when report has transaction thread violations', 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', - }; - - 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, - actorAccountID: 12345, - created: '2024-08-08 18:20:44.171', - }, - }; - - const MOCK_TRANSACTION = { - transactionID: '1', - amount: 10, - modifiedAmount: 10, - reportID: MOCK_REPORT.reportID, - }; - - const MOCK_TRANSACTIONS = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, - } as OnyxCollection; - - const MOCK_TRANSACTION_VIOLATIONS: TransactionViolationsCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION.transactionID}` as const]: [ - { - type: CONST.VIOLATION_TYPES.VIOLATION, - name: CONST.VIOLATIONS.MISSING_CATEGORY, - showInReview: true, - }, - ], - }; - - await act(async () => { - await Onyx.multiSet({ - ...MOCK_REPORTS, - ...MOCK_TRANSACTION_VIOLATIONS, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT_ACTIONS, - [ONYXKEYS.SESSION]: { - accountID: 12345, - }, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, - }); - }); - - // Simulate how components determined if a report is archived by using this hook - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - {}, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS as OnyxCollection, - isReportArchived.current, - ); - - expect(result).toBe(true); - }); - - it('returns true when report has transaction thread notice type violation', 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', - }; - - 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, - actorAccountID: 12345, - created: '2024-08-08 18:20:44.171', - }, - }; - - const MOCK_TRANSACTION = { - transactionID: '1', - amount: 10, - modifiedAmount: 10, - reportID: MOCK_REPORT.reportID, - }; - - const MOCK_TRANSACTIONS = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, - } as OnyxCollection; - - const MOCK_TRANSACTION_VIOLATIONS: TransactionViolationsCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION.transactionID}` as const]: [ - { - type: CONST.VIOLATION_TYPES.NOTICE, - name: CONST.VIOLATIONS.MODIFIED_AMOUNT, - showInReview: true, - }, - ], - }; - - await act(async () => { - await Onyx.multiSet({ - ...MOCK_REPORTS, - ...MOCK_TRANSACTION_VIOLATIONS, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${MOCK_REPORT.reportID}` as const]: MOCK_REPORT_ACTIONS, - [ONYXKEYS.SESSION]: { - accountID: 12345, - }, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${MOCK_TRANSACTION.transactionID}` as const]: MOCK_TRANSACTION, - }); - }); - - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - {}, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS as OnyxCollection, - isReportArchived.current, - ); - - expect(result).toBe(true); - }); - - it('returns true when submitter has held expenses even if outstanding tasks trigger GBR', async () => { - const policyID = generateReportID(); - const expenseChatID = generateReportID(); - const expenseReportID = generateReportID(); - const holdReportActionID = generateReportID(); - - const policyExpenseChat: Report = { - reportID: expenseChatID, - chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - type: CONST.REPORT.TYPE.CHAT, - ownerAccountID: 12345, - policyID, - hasOutstandingChildRequest: true, - stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS_NUM.OPEN, - }; - - const expenseReport: Report = { - reportID: expenseReportID, - chatReportID: expenseChatID, - type: CONST.REPORT.TYPE.EXPENSE, - ownerAccountID: 12345, - managerID: 12345, - policyID, - stateNum: CONST.REPORT.STATE_NUM.OPEN, - statusNum: CONST.REPORT.STATUS_NUM.OPEN, - }; - - const baseTransaction = createRandomTransaction(700); - const transactionID = generateTransactionID(); - const transaction: Transaction = { - ...baseTransaction, - transactionID, - reportID: expenseReport.reportID, - amount: 12345, - currency: CONST.CURRENCY.USD, - status: CONST.TRANSACTION.STATUS.POSTED, - comment: { - ...(baseTransaction.comment ?? {}), - hold: holdReportActionID, - }, - }; - - const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const; - const transactionViolationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}` as const; - const transactionViolations: OnyxCollection = { - [transactionViolationsKey]: [ - { - name: CONST.VIOLATIONS.HOLD, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - }, - ], - }; - - await act(async () => { - await Onyx.multiSet({ - [ONYXKEYS.SESSION]: { - accountID: 12345, - }, - [`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat, - [`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`]: expenseReport, - [transactionKey]: transaction, - [transactionViolationsKey]: transactionViolations[transactionViolationsKey], - } as unknown as OnyxMultiSetInput); - }); - - await waitForBatchedUpdatesWithAct(); - - const requiresAttention = getReasonAndReportActionThatRequiresAttention(policyExpenseChat); - expect(requiresAttention?.reason).toBe(CONST.REQUIRES_ATTENTION_REASONS.HAS_CHILD_REPORT_AWAITING_ACTION); - - const {reason} = - SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad( - policyExpenseChat, - policyExpenseChat, - {} as OnyxEntry, - true, - {}, - {[transactionKey]: transaction}, - transactionViolations, - false, - ) ?? {}; - - expect(reason).toBe(CONST.RBR_REASONS.HAS_TRANSACTION_THREAD_VIOLATIONS); - - const {result: isReportArchived} = renderHook(() => useReportIsArchived(policyExpenseChat.reportID)); - const hasRedBrickRoad = SidebarUtils.shouldShowRedBrickRoad( - policyExpenseChat, - policyExpenseChat, - {} as OnyxEntry, - true, - {}, - {[transactionKey]: transaction}, - transactionViolations as OnyxCollection, - isReportArchived.current, - ); - - expect(hasRedBrickRoad).toBe(true); - }); - - it('returns true when report has errors', () => { - const MOCK_REPORT: Report = { - reportID: '1', - errorFields: { - someField: { - error: 'Some error occurred', - }, - }, - }; - const MOCK_REPORT_ACTIONS: OnyxEntry = {}; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - const reportErrors = getAllReportErrors(MOCK_REPORT, MOCK_REPORT_ACTIONS); - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - reportErrors, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - - expect(result).toBe(true); - }); - - it('returns true when report has violations', () => { - const MOCK_REPORT: Report = { - reportID: '1', - }; - const MOCK_REPORT_ACTIONS: OnyxEntry = {}; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - true, - {}, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - - expect(result).toBe(true); - }); - - it('returns true when report has report action errors', () => { - const MOCK_REPORT: Report = { - reportID: '1', - }; - const MOCK_REPORT_ACTIONS: OnyxEntry = { - // eslint-disable-next-line @typescript-eslint/naming-convention - '1': { - reportActionID: '1', - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - actorAccountID: 12345, - created: '2024-08-08 18:20:44.171', - message: [ - { - type: '', - text: '', - }, - ], - errors: { - someError: 'Some error occurred', - }, - }, - }; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - const reportErrors = getAllReportErrors(MOCK_REPORT, MOCK_REPORT_ACTIONS); - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - reportErrors, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - - expect(result).toBe(true); - }); - - it('returns true when report has export errors', () => { - const MOCK_REPORT: Report = { - reportID: '1', - errorFields: { - export: { - error: 'Some error occurred', - }, - }, - }; - const MOCK_REPORT_ACTIONS: OnyxEntry = {}; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - const reportErrors = getAllReportErrors(MOCK_REPORT, MOCK_REPORT_ACTIONS); - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - reportErrors, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - - expect(result).toBe(true); - }); - - it('returns false when report has no errors', () => { - const MOCK_REPORT: Report = { - reportID: '1', - }; - const MOCK_REPORT_ACTIONS: OnyxEntry = {}; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - {}, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - - expect(result).toBe(false); - }); - - it('shows RBR when copilot with full access pays a report but deposit and withdrawal accounts are the same', () => { - const copilotAccountID = 99999; - const ownerAccountID = 12345; - - const MOCK_EXPENSE_REPORT: Report = { - reportID: '1', - type: CONST.REPORT.TYPE.EXPENSE, - ownerAccountID, - managerID: copilotAccountID, - statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, - stateNum: CONST.REPORT.STATE_NUM.APPROVED, - }; - - const MOCK_PAY_ACTION: ReportAction = { - reportActionID: '1', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - actorAccountID: copilotAccountID, - created: '2024-08-08 18:20:44.171', - delegateAccountID: copilotAccountID, - message: [ - { - type: 'COMMENT', - text: '', - }, - ], - originalMessage: { - type: CONST.IOU.REPORT_ACTION_TYPE.PAY, - IOUReportID: MOCK_EXPENSE_REPORT.reportID, - amount: 10000, - currency: CONST.CURRENCY.USD, - paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - } as OriginalMessageIOU, - errors: { - [`${Date.now()}`]: CONST.ERROR.BANK_ACCOUNT_SAME_DEPOSIT_AND_WITHDRAWAL_ERROR, - }, - }; - - const MOCK_REPORT_ACTIONS: OnyxEntry = { - // eslint-disable-next-line @typescript-eslint/naming-convention - '1': MOCK_PAY_ACTION, - }; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - const reportErrors = getAllReportErrors(MOCK_EXPENSE_REPORT, MOCK_REPORT_ACTIONS); - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_EXPENSE_REPORT?.reportID)); - - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_EXPENSE_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - reportErrors, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - expect(result).toBe(true); - - const {reason, reportAction} = - SidebarUtils.getReasonAndReportActionThatHasRedBrickRoad( - MOCK_EXPENSE_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - reportErrors, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ) ?? {}; - expect(reason).toBe(CONST.RBR_REASONS.HAS_ERRORS); - expect(reportAction).toMatchObject(MOCK_PAY_ACTION); - }); - - it('returns false when report is archived', () => { - const MOCK_REPORT: Report = { - reportID: '5', - errorFields: { - export: { - error: 'Some error occurred', - }, - }, - }; - // This report with reportID 5 is already archived from previous tests - // where we set reportNameValuePairs with private_isArchived - const MOCK_REPORT_ACTIONS: OnyxEntry = {}; - const MOCK_TRANSACTIONS = {}; - const MOCK_TRANSACTION_VIOLATIONS: OnyxCollection = {}; - - // Simulate how components determined if a report is archived by using this hook - const {result: isReportArchived} = renderHook(() => useReportIsArchived(MOCK_REPORT?.reportID)); - const result = SidebarUtils.shouldShowRedBrickRoad( - MOCK_REPORT, - chatReportR14932, - MOCK_REPORT_ACTIONS, - false, - {}, - MOCK_TRANSACTIONS, - MOCK_TRANSACTION_VIOLATIONS, - isReportArchived.current, - ); - - expect(result).toBe(false); - }); - }); - describe('getWelcomeMessage', () => { const MOCK_CONCIERGE_REPORT_ID = 'concierge-report-id'; diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index 600cedbd58968..cc85bda5fbae4 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -38,7 +38,7 @@ jest.mock('@libs/ReportUtils', () => ({ generateReportAttributes: jest.requireActual('@libs/ReportUtils').generateReportAttributes, getAllReportActionsErrorsAndReportActionThatRequiresAttention: jest.requireActual('@libs/ReportUtils').getAllReportActionsErrorsAndReportActionThatRequiresAttention, getAllReportErrors: jest.requireActual('@libs/ReportUtils').getAllReportErrors, - shouldDisplayViolationsRBRInLHN: jest.requireActual('@libs/ReportUtils').shouldDisplayViolationsRBRInLHN, + getViolatingReportIDForRBRInLHN: jest.requireActual('@libs/ReportUtils').getViolatingReportIDForRBRInLHN, generateIsEmptyReport: jest.requireActual('@libs/ReportUtils').generateIsEmptyReport, isExpenseReport: jest.requireActual('@libs/ReportUtils').isExpenseReport, isSelfDM: jest.requireActual('@libs/ReportUtils').isSelfDM,