From 94d891b91864b48b1a45c6af66acf03694d2fd78 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 10:59:02 -0600 Subject: [PATCH 1/8] Write a test for archived reports --- tests/unit/IOUUtilsTest.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index c74fdd10b511c..b47f89c6775fb 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -324,6 +324,31 @@ describe('canSubmitReport', () => { expect(canSubmitReport(expenseReport, fakePolicy, [], undefined)).toBe(false); }); + + it('returns false if the report is archived', async () => { + const policy: Policy = { + ...createRandomPolicy(7), + ownerAccountID: currentUserAccountID, + areRulesEnabled: true, + preventSelfApproval: false, + }; + const report: Report = { + ...createRandomReport(7), + type: CONST.REPORT.TYPE.EXPENSE, + managerID: currentUserAccountID, + ownerAccountID: currentUserAccountID, + policyID: policy.id, + }; + + // This is what indicates that a report is archived (see ReportUtils.isArchivedReport()) + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`, { + private_isArchived: new Date().toString(), + }); + + // Simulate how components call canModifyTask() by using the hook useReportIsArchived() to see if the report is archived + // const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); + expect(canSubmitReport(report, policy, [], undefined)).toBe(false); + }); }); describe('Check valid amount for IOU/Expense request', () => { From f7491589e01cec1277278a417f36551794f6d8d6 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 11:02:20 -0600 Subject: [PATCH 2/8] Implement new argument and ensure test passes --- src/libs/actions/IOU.ts | 10 ++-------- tests/unit/IOUUtilsTest.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 05c138aff3db8..30df76001a8d7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -124,7 +124,6 @@ import { getOutstandingChildRequest, getParsedComment, getPersonalDetailsForAccountID, - getReportNameValuePairs, getReportNotificationPreference, getReportOrDraftReport, getReportTransactions, @@ -8867,15 +8866,10 @@ function canSubmitReport( policy: OnyxEntry | SearchPolicy, transactions: OnyxTypes.Transaction[] | SearchTransaction[], allViolations: OnyxCollection | undefined, + isReportArchived = false, ) { const currentUserAccountID = getCurrentUserAccountID(); const isOpenExpenseReport = isOpenExpenseReportReportUtils(report); - - // This will get removed as part of https://github.com/Expensify/App/issues/59961 - // eslint-disable-next-line deprecation/deprecation - const reportNameValuePairs = getReportNameValuePairs(report?.reportID); - - const isArchived = isArchivedReport(reportNameValuePairs); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const transactionIDList = transactions.map((transaction) => transaction.transactionID); const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList, allViolations); @@ -8887,7 +8881,7 @@ function canSubmitReport( return ( transactions.length > 0 && isOpenExpenseReport && - !isArchived && + !isReportArchived && !hasOnlyPendingCardOrScanFailTransactions && !hasAllPendingRTERViolations && hasTransactionWithoutRTERViolation && diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index b47f89c6775fb..8960a92340e96 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -1,5 +1,7 @@ +import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import DateUtils from '@libs/DateUtils'; import {canSubmitReport} from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -16,6 +18,9 @@ import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import currencyList from './currencyList.json'; +// This keeps the error "@rnmapbox/maps native code not available." from causing the tests to fail +jest.mock('@components/ConfirmedRoute.tsx'); + const testDate = DateUtils.getDBTime(); const currentUserAccountID = 5; @@ -346,8 +351,8 @@ describe('canSubmitReport', () => { }); // Simulate how components call canModifyTask() by using the hook useReportIsArchived() to see if the report is archived - // const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); - expect(canSubmitReport(report, policy, [], undefined)).toBe(false); + const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); + expect(canSubmitReport(report, policy, [], undefined, isReportArchived.current)).toBe(false); }); }); From 9107879bc0987d2d10e8601216652cd5e5514bff Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 11:03:00 -0600 Subject: [PATCH 3/8] Implement new parameter --- src/components/MoneyReportHeaderOld.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeaderOld.tsx b/src/components/MoneyReportHeaderOld.tsx index 923e976d2edc7..8ddfbc1f1dab4 100644 --- a/src/components/MoneyReportHeaderOld.tsx +++ b/src/components/MoneyReportHeaderOld.tsx @@ -220,7 +220,7 @@ function MoneyReportHeaderOld({policy, report: moneyRequestReport, transactionTh const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const filteredTransactions = transactions?.filter((t) => t) ?? []; - const shouldShowSubmitButton = canSubmitReport(moneyRequestReport, policy, filteredTransactions, violations); + const shouldShowSubmitButton = canSubmitReport(moneyRequestReport, policy, filteredTransactions, violations, isArchivedReport); const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && !!connectedIntegration && isAdmin && canBeExported(moneyRequestReport); From dee682103c8a264ec76ecbb0b0885c48c6c10140 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 11:04:04 -0600 Subject: [PATCH 4/8] Rename and implement new argument --- src/components/ReportActionItem/ReportPreviewOld.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreviewOld.tsx b/src/components/ReportActionItem/ReportPreviewOld.tsx index ac9fdccaf5139..1ca90831f2ce4 100644 --- a/src/components/ReportActionItem/ReportPreviewOld.tsx +++ b/src/components/ReportActionItem/ReportPreviewOld.tsx @@ -257,10 +257,10 @@ function ReportPreviewOld({ formattedMerchant = null; } - const isArchived = useReportIsArchived(iouReport?.reportID); + const isIouReportArchived = useReportIsArchived(iouReport?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const filteredTransactions = transactions?.filter((transaction) => transaction) ?? []; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations); + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations, isIouReportArchived); const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport); // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on @@ -419,10 +419,10 @@ function ReportPreviewOld({ const getPendingMessageProps: () => PendingMessageProps = () => { if (isPayAtEndExpense) { - if (!isArchived) { + if (!isIouReportArchived) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.bookingPending')}; } - if (isArchived && archiveReason === CONST.REPORT.ARCHIVE_REASON.BOOKING_END_DATE_HAS_PASSED) { + if (isIouReportArchived && archiveReason === CONST.REPORT.ARCHIVE_REASON.BOOKING_END_DATE_HAS_PASSED) { return { shouldShow: true, messageIcon: Expensicons.Box, From d6e3d52c35ae3e815180d63d3135e40807259a75 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 11:05:04 -0600 Subject: [PATCH 5/8] Implement new argument --- .../MoneyRequestReportPreviewContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 2207a7c656f00..8fd19d6556b99 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -228,7 +228,7 @@ function MoneyRequestReportPreviewContent({ }; const shouldShowApproveButton = useMemo(() => canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations); + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations, isIouReportArchived); const shouldShowSettlementButton = !shouldShowSubmitButton && (shouldShowPayButton || shouldShowApproveButton) && !shouldShowRTERViolationMessage && !shouldShowBrokenConnectionViolation; const previewMessage = useMemo(() => { From daa9de6a48f37e80808275df664ecda23a54b74d Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 11:08:38 -0600 Subject: [PATCH 6/8] Implement new argument --- src/libs/SearchUIUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 9f26b01feb883..e656001a6591a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -44,6 +44,7 @@ import { hasOnlyHeldExpenses, hasViolations, isAllowedToApproveExpenseReport as isAllowedToApproveExpenseReportUtils, + isArchivedReport, isClosedReport, isInvoiceReport, isMoneyRequestReport, @@ -381,6 +382,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr const chatReport = data[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`] ?? {}; const chatReportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.chatReportID}`] ?? undefined; + const isArchived = isArchivedReport(chatReportRNVP); if (canIOUBePaid(report, chatReport, policy, allReportTransactions, false, chatReportRNVP, invoiceReceiverPolicy) && !hasOnlyHeldExpenses(report.reportID, allReportTransactions)) { return CONST.SEARCH.ACTION_TYPES.PAY; @@ -393,7 +395,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr } // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead - if (canSubmitReport(report, policy, allReportTransactions, allViolations) && isAllowedToApproveExpenseReport) { + if (canSubmitReport(report, policy, allReportTransactions, allViolations, isArchived) && isAllowedToApproveExpenseReport) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } From be7f8da77a37dad3708ff9e4a313da38505bce00 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 5 May 2025 12:03:50 -0600 Subject: [PATCH 7/8] Implement isarchived check for new report header --- src/components/MoneyReportHeader.tsx | 4 ++-- src/libs/ReportPrimaryActionUtils.ts | 12 +++++++----- src/libs/ReportSecondaryActionUtils.ts | 12 +++++++++--- src/libs/SearchUIUtils.ts | 5 +++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index f0522404186a2..4f008a0b440b6 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -484,8 +484,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (!moneyRequestReport) { return []; } - return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy); - }, [moneyRequestReport, policy, transactions, violations]); + return getSecondaryReportActions(moneyRequestReport, transactions, violations, policy, reportNameValuePairs); + }, [moneyRequestReport, policy, transactions, violations, reportNameValuePairs]); const secondaryActionsImplemenation: Record, DropdownOption>> = { [CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS]: { diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 9515fd164b909..ccd4825b56167 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -50,7 +50,11 @@ function isAddExpenseAction(report: Report, reportTransactions: Transaction[]) { return isExpenseReport && canAddTransaction && isReportSubmitter && reportTransactions.length === 0; } -function isSubmitAction(report: Report, reportTransactions: Transaction[], policy?: Policy) { +function isSubmitAction(report: Report, reportTransactions: Transaction[], policy?: Policy, reportNameValuePairs?: ReportNameValuePairs) { + if (isArchivedReport(reportNameValuePairs)) { + return false; + } + const isExpenseReport = isExpenseReportUtils(report); const isReportSubmitter = isCurrentUserSubmitter(report.reportID); const isOpenReport = isOpenReportUtils(report); @@ -120,9 +124,7 @@ function isPayAction(report: Report, policy?: Policy, reportNameValuePairs?: Rep const isReportFinished = (isReportApproved && !report.isWaitingOnBankAccount) || isSubmittedWithoutApprovalsEnabled || isReportClosed; const {reimbursableSpend} = getMoneyRequestSpendBreakdown(report); - const isChatReportArchived = isArchivedReport(reportNameValuePairs); - - if (isChatReportArchived) { + if (isArchivedReport(reportNameValuePairs)) { return false; } @@ -275,7 +277,7 @@ function getReportPrimaryAction( return CONST.REPORT.PRIMARY_ACTIONS.REMOVE_HOLD; } - if (isSubmitAction(report, reportTransactions, policy)) { + if (isSubmitAction(report, reportTransactions, policy, reportNameValuePairs)) { return CONST.REPORT.PRIMARY_ACTIONS.SUBMIT; } diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 7e614608a083d..1bdfa3415d71c 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -1,7 +1,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -import type {Policy, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {getCurrentUserAccountID, getCurrentUserEmail} from './actions/Report'; import { @@ -19,6 +19,7 @@ import { import {getIOUActionForReportID, getReportActions, isPayAction} from './ReportActionsUtils'; import { canAddTransaction, + isArchivedReport, isClosedReport as isClosedReportUtils, isCurrentUserSubmitter, isExpenseReport as isExpenseReportUtils, @@ -45,7 +46,11 @@ function isAddExpenseAction(report: Report, reportTransactions: Transaction[]) { return canAddTransaction(report); } -function isSubmitAction(report: Report, reportTransactions: Transaction[], policy?: Policy): boolean { +function isSubmitAction(report: Report, reportTransactions: Transaction[], policy?: Policy, reportNameValuePairs?: ReportNameValuePairs): boolean { + if (isArchivedReport(reportNameValuePairs)) { + return false; + } + const transactionAreComplete = reportTransactions.every((transaction) => transaction.amount !== 0 || transaction.modifiedAmount !== 0); if (!transactionAreComplete) { @@ -408,6 +413,7 @@ function getSecondaryReportActions( reportTransactions: Transaction[], violations: OnyxCollection, policy?: Policy, + reportNameValuePairs?: ReportNameValuePairs, ): Array> { const options: Array> = []; @@ -415,7 +421,7 @@ function getSecondaryReportActions( options.push(CONST.REPORT.SECONDARY_ACTIONS.ADD_EXPENSE); } - if (isSubmitAction(report, reportTransactions, policy)) { + if (isSubmitAction(report, reportTransactions, policy, reportNameValuePairs)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT); } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e656001a6591a..6a566d9c67f2d 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -382,7 +382,6 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr const chatReport = data[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`] ?? {}; const chatReportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.chatReportID}`] ?? undefined; - const isArchived = isArchivedReport(chatReportRNVP); if (canIOUBePaid(report, chatReport, policy, allReportTransactions, false, chatReportRNVP, invoiceReceiverPolicy) && !hasOnlyHeldExpenses(report.reportID, allReportTransactions)) { return CONST.SEARCH.ACTION_TYPES.PAY; @@ -394,12 +393,14 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.APPROVE; } + const reportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`] ?? undefined; + const isArchived = isArchivedReport(reportRNVP); + // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead if (canSubmitReport(report, policy, allReportTransactions, allViolations, isArchived) && isAllowedToApproveExpenseReport) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } - const reportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`] ?? undefined; if (reportRNVP?.exportFailedTime) { return CONST.SEARCH.ACTION_TYPES.REVIEW; } From a4b561cc0e911138e6e09116e97d827c0bae9971 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Wed, 7 May 2025 11:20:38 -0600 Subject: [PATCH 8/8] Rename variable --- src/libs/SearchUIUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c57849b7d9c16..fd9cd7f418f77 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -429,15 +429,15 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.APPROVE; } - const reportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`] ?? undefined; - const isArchived = isArchivedReport(reportRNVP); + const reportNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`] ?? undefined; + const isArchived = isArchivedReport(reportNVP); // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead if (canSubmitReport(report, policy, allReportTransactions, allViolations, isArchived) && isAllowedToApproveExpenseReport) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } - if (reportRNVP?.exportFailedTime) { + if (reportNVP?.exportFailedTime) { return CONST.SEARCH.ACTION_TYPES.REVIEW; }