From 94b4452ab590108dedd4403f668ed39d54e8a914 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 2 Dec 2025 16:47:50 +0700 Subject: [PATCH 01/14] refactor-isAwaitingFirstLevelApproval --- src/libs/ReportUtils.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5ce0be6833cd2..c735ed007fdcf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -154,6 +154,7 @@ import { getPolicyRole, getRuleApprovers, getSubmitToAccountID, + hasDynamicExternalWorkflow, hasDependentTags as hasDependentTagsPolicyUtils, isExpensifyTeam, isInstantSubmitEnabled, @@ -1967,11 +1968,31 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return false; } + if (!isProcessingReport(report)) { + return false; + } + + if (isIOUReportUsingReport(report)) { + return true; + } + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated - const submitsToAccountID = getSubmitToAccountID(getPolicy(report.policyID), report); + const policy = getPolicy(report?.policyID); + + if (hasDynamicExternalWorkflow(policy)) { + return false; + } + + if (policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC) { + return true; + } + + const submitsToAccountID = getSubmitToAccountID(policy, report); - return isProcessingReport(report) && submitsToAccountID === report.managerID; + // Fallback to comparing current manager of the report to the submitsTo in the policy. + // If they match, then the report should still be awaiting first level approval. + return submitsToAccountID === report?.managerID; } /** From fb877f406ee31539b40f576ad792250980a83480 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 3 Dec 2025 17:28:03 +0700 Subject: [PATCH 02/14] update canDeleteTransaction --- src/hooks/useSelectedTransactionsActions.ts | 4 ++-- src/libs/ReportUtils.ts | 26 +++++---------------- src/pages/ReportDetailsPage.tsx | 4 ++-- tests/unit/ReportUtilsTest.ts | 8 +++---- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 5a6d2567dd516..b28d406163ba9 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -9,8 +9,8 @@ import Navigation from '@libs/Navigation/Navigation'; import {getIOUActionForTransactionID, getReportAction, isDeletedAction} from '@libs/ReportActionsUtils'; import {isMergeAction} from '@libs/ReportSecondaryActionUtils'; import { + canAddOrDeleteTransactions, canDeleteCardTransactionByLiabilityType, - canDeleteTransaction, canEditFieldOfMoneyRequest, canHoldUnholdReportAction, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, @@ -313,7 +313,7 @@ function useSelectedTransactionsActions({ return canRemoveTransaction && isIOUActionOwner && !isActionDeleted; }); - const canRemoveReportTransaction = canDeleteTransaction(report, isReportArchived); + const canRemoveReportTransaction = canAddOrDeleteTransactions(report, policy, isReportArchived); if (canRemoveReportTransaction && canAllSelectedTransactionsBeRemoved) { options.push({ diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c735ed007fdcf..fb594df43d5dd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -154,8 +154,8 @@ import { getPolicyRole, getRuleApprovers, getSubmitToAccountID, - hasDynamicExternalWorkflow, hasDependentTags as hasDependentTagsPolicyUtils, + hasDynamicExternalWorkflow, isExpensifyTeam, isInstantSubmitEnabled, isPaidGroupPolicy as isPaidGroupPolicyPolicyUtils, @@ -1438,7 +1438,7 @@ function isIOUReport(reportOrID: OnyxInputOrEntry | string): boolean { /** * Checks if a report is an IOU report using report */ -function isIOUReportUsingReport(report: OnyxEntry): report is Report { +function isIOUReportUsingReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.IOU; } @@ -2704,14 +2704,10 @@ function getChildReportNotificationPreference(reportAction: OnyxInputOrEntry, isReportArchived = false): boolean { +function canAddOrDeleteTransactions(moneyRequestReport: OnyxEntry, policy?: OnyxEntry, isReportArchived = false): boolean { if (!isMoneyRequestReport(moneyRequestReport) || isReportArchived) { return false; } - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(moneyRequestReport?.policyID); - // Adding or deleting transactions is not allowed on a closed report if (moneyRequestReport?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && !isOpenReport(moneyRequestReport)) { return false; @@ -2747,17 +2743,7 @@ function canAddTransaction(moneyRequestReport: OnyxEntry, isReportArchiv return false; } - return canAddOrDeleteTransactions(moneyRequestReport, isReportArchived); -} - -/** - * Checks whether the supplied report supports deleting more transactions from it. - * Return true if: - * - report is a non-settled IOU - * - report is a non-approved IOU - */ -function canDeleteTransaction(moneyRequestReport: OnyxEntry, isReportArchived = false): boolean { - return canAddOrDeleteTransactions(moneyRequestReport, isReportArchived); + return canAddOrDeleteTransactions(moneyRequestReport, policy, isReportArchived); } /** @@ -2972,7 +2958,7 @@ function canDeleteReportAction( if (isActionOwner) { if (!isEmptyObject(report) && (isMoneyRequestReport(report) || isInvoiceReport(report))) { - return canDeleteTransaction(report) && canCardTransactionBeDeleted; + return canAddOrDeleteTransactions(report, policy ?? undefined) && canCardTransactionBeDeleted; } if (isTrackExpenseAction(reportAction)) { return canCardTransactionBeDeleted; @@ -12953,7 +12939,7 @@ export { canAccessReport, isReportNotFound, canAddTransaction, - canDeleteTransaction, + canAddOrDeleteTransactions, canBeAutoReimbursed, canCreateRequest, canCreateTaskInReport, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 1ee00e7d1ca74..02c80c3f3bff2 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -47,8 +47,8 @@ import Permissions from '@libs/Permissions'; import {isPolicyAdmin as isPolicyAdminUtil, isPolicyEmployee as isPolicyEmployeeUtil, shouldShowPolicy} from '@libs/PolicyUtils'; import {getOneTransactionThreadReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import { + canAddOrDeleteTransactions, canDeleteCardTransactionByLiabilityType, - canDeleteTransaction, canEditReportDescription as canEditReportDescriptionUtil, canJoinChat, canLeaveChat, @@ -288,7 +288,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail !isClosedReport(report) && isTaskModifiable && isTaskActionable; - const canDeleteRequest = isActionOwner && (canDeleteTransaction(moneyRequestReport, isMoneyRequestReportArchived) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; + const canDeleteRequest = isActionOwner && (canAddOrDeleteTransactions(moneyRequestReport, policy, isMoneyRequestReportArchived) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${iouTransactionID}`, {canBeMissing: true}); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(iouTransactionID ? [iouTransactionID] : []); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 7ea6399d82d22..329cd803318a0 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -5868,7 +5868,7 @@ describe('ReportUtils', () => { // When it's checked if the transactions can be deleted // Simulate how components determined if a report is archived by using this hook const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); - const result = canDeleteTransaction(report, isReportArchived.current); + const result = canDeleteTransaction(report, policy, isReportArchived.current); // Then the result is true expect(result).toBe(true); @@ -5885,7 +5885,7 @@ describe('ReportUtils', () => { // When it's checked if the transactions can be deleted const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); - const result = canDeleteTransaction(report, isReportArchived.current); + const result = canDeleteTransaction(report, policy, isReportArchived.current); // Then the result is false expect(result).toBe(false); @@ -5934,7 +5934,7 @@ describe('ReportUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${openReport.reportID}`, openReport); - expect(canDeleteTransaction(openReport, false)).toBe(true); + expect(canDeleteTransaction(openReport, policy, false)).toBe(true); }); it('should return false for closed report when workflow is disabled', async () => { @@ -5947,7 +5947,7 @@ describe('ReportUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${closedReport.reportID}`, closedReport); - expect(canDeleteTransaction(closedReport, false)).toBe(false); + expect(canDeleteTransaction(closedReport, policy, false)).toBe(false); }); }); }); From f2aae03ed78b51386e03bdde7d9a4ae4f23acab1 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 3 Dec 2025 17:42:08 +0700 Subject: [PATCH 03/14] remove canDelete field --- src/components/Search/index.tsx | 12 +++++++----- src/types/onyx/SearchResults.ts | 3 --- tests/unit/ReportUtilsTest.ts | 12 ++++++------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a07e2ff19403b..38b0c79ad42b5 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -33,7 +33,7 @@ import Log from '@libs/Log'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import Performance from '@libs/Performance'; -import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils'; +import {canAddOrDeleteTransactions, canEditFieldOfMoneyRequest, canHoldUnholdReportAction, isOneTransactionReport, selectFilteredReportActions} from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; import { createAndOpenSearchTransactionThread, @@ -100,7 +100,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType, outsta item.keyForList, { isSelected: true, - canDelete: item.canDelete, + canDelete: canAddOrDeleteTransactions(item.report, item.policy), canHold: canHoldRequest, isHeld: isOnHold(item), canUnhold: canUnholdRequest, @@ -182,7 +182,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact ...selectedTransactions, [item.keyForList]: { isSelected: true, - canDelete: item.canDelete, + canDelete: canAddOrDeleteTransactions(item.report, item.policy), canHold: canHoldRequest, isHeld: isOnHold(item), canUnhold: canUnholdRequest, @@ -514,7 +514,8 @@ function Search({ ), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID]?.isSelected || isExpenseReportType, - canDelete: transactionItem.canDelete, + canDelete: canAddOrDeleteTransactions(transactionItem.report, transactionItem.policy), + reportID: transactionItem.reportID, policyID: transactionItem.report?.policyID, amount: transactionItem.modifiedAmount ?? transactionItem.amount, @@ -559,7 +560,8 @@ function Search({ ), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing isSelected: areAllMatchingItemsSelected || selectedTransactions[transactionItem.transactionID].isSelected, - canDelete: transactionItem.canDelete, + canDelete: canAddOrDeleteTransactions(transactionItem.report, transactionItem.policy), + reportID: transactionItem.reportID, policyID: transactionItem.report?.policyID, amount: transactionItem.modifiedAmount ?? transactionItem.amount, diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 4329b0b4e4b1a..07f5b55b47185 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -176,9 +176,6 @@ type SearchTransaction = { /** The transaction amount */ amount: number; - /** If the transaction can be deleted */ - canDelete: boolean; - /** The edited transaction amount */ modifiedAmount: number; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 329cd803318a0..272d677bab994 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -30,11 +30,11 @@ import { buildParticipantsFromAccountIDs, buildReportNameFromParticipantNames, buildTransactionThread, + canAddOrDeleteTransactions, canAddTransaction, canCreateRequest, canDeleteMoneyRequestReport, canDeleteReportAction, - canDeleteTransaction, canEditMoneyRequest, canEditReportDescription, canEditRoomVisibility, @@ -5856,7 +5856,7 @@ describe('ReportUtils', () => { }); }); - describe('canDeleteTransaction', () => { + describe('canAddOrDeleteTransactions', () => { it('should return true for a non-archived report', async () => { // Given a non-archived expense report const report: Report = { @@ -5868,7 +5868,7 @@ describe('ReportUtils', () => { // When it's checked if the transactions can be deleted // Simulate how components determined if a report is archived by using this hook const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); - const result = canDeleteTransaction(report, policy, isReportArchived.current); + const result = canAddOrDeleteTransactions(report, policy, isReportArchived.current); // Then the result is true expect(result).toBe(true); @@ -5885,7 +5885,7 @@ describe('ReportUtils', () => { // When it's checked if the transactions can be deleted const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); - const result = canDeleteTransaction(report, policy, isReportArchived.current); + const result = canAddOrDeleteTransactions(report, policy, isReportArchived.current); // Then the result is false expect(result).toBe(false); @@ -5934,7 +5934,7 @@ describe('ReportUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${openReport.reportID}`, openReport); - expect(canDeleteTransaction(openReport, policy, false)).toBe(true); + expect(canAddOrDeleteTransactions(openReport, policy, false)).toBe(true); }); it('should return false for closed report when workflow is disabled', async () => { @@ -5947,7 +5947,7 @@ describe('ReportUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${closedReport.reportID}`, closedReport); - expect(canDeleteTransaction(closedReport, policy, false)).toBe(false); + expect(canAddOrDeleteTransactions(closedReport, policy, false)).toBe(false); }); }); }); From 9843862526da7b54b84a0fc6d09a4fdb06bf023e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 3 Dec 2025 17:55:16 +0700 Subject: [PATCH 04/14] fix UTs --- tests/unit/MoneyRequestReportUtilsTest.ts | 1 - tests/unit/Search/SearchUIUtilsTest.ts | 16 ++-------------- tests/unit/Search/handleActionButtonPressTest.ts | 1 - tests/unit/TransactionGroupListItemTest.tsx | 1 - 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/tests/unit/MoneyRequestReportUtilsTest.ts b/tests/unit/MoneyRequestReportUtilsTest.ts index 2c9d3338dd29b..02b78be1b8440 100644 --- a/tests/unit/MoneyRequestReportUtilsTest.ts +++ b/tests/unit/MoneyRequestReportUtilsTest.ts @@ -55,7 +55,6 @@ const transactionItemBaseMock: TransactionListItemType = { policy: policyBaseMock, reportAction: reportActionBaseMock, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 1ec9cc52548f2..5f07a104c29a9 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -382,7 +382,6 @@ const searchResults: OnyxTypes.SearchResults = { [`report_${reportID5}`]: report5, [`transactions_${transactionID}`]: { amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -414,7 +413,6 @@ const searchResults: OnyxTypes.SearchResults = { }, [`transactions_${transactionID2}`]: { amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -447,7 +445,6 @@ const searchResults: OnyxTypes.SearchResults = { ...allViolations, [`transactions_${transactionID3}`]: { amount: 1200, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -479,7 +476,6 @@ const searchResults: OnyxTypes.SearchResults = { }, [`transactions_${transactionID4}`]: { amount: 3200, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -767,7 +763,6 @@ const transactionsListItems = [ policy, reportAction: reportAction1, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -821,7 +816,6 @@ const transactionsListItems = [ policy, reportAction: reportAction2, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -885,7 +879,6 @@ const transactionsListItems = [ policy, reportAction: reportAction3, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -944,7 +937,6 @@ const transactionsListItems = [ policy, reportAction: reportAction4, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -1039,7 +1031,6 @@ const transactionReportGroupListItems = [ reportAction: reportAction1, holdReportAction: undefined, amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -1135,7 +1126,6 @@ const transactionReportGroupListItems = [ reportAction: reportAction2, holdReportAction: undefined, amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -1786,7 +1776,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 48; + const expectedPropertyCount = 47; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); @@ -1819,7 +1809,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 48; + const expectedPropertyCount = 47; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); @@ -2429,7 +2419,6 @@ describe('SearchUIUtils', () => { // eslint-disable-next-line @typescript-eslint/naming-convention transactions_1805965960759424086: { amount: 0, - canDelete: false, category: 'Employee Meals Remote (Fringe Benefit)', comment: { comment: '', @@ -2552,7 +2541,6 @@ describe('SearchUIUtils', () => { // eslint-disable-next-line @typescript-eslint/naming-convention transactions_1805965960759424086: { amount: 0, - canDelete: false, cardID: undefined, cardName: undefined, category: 'Employee Meals Remote (Fringe Benefit)', diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index 4deb193a807c0..5eda3218fc485 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -181,7 +181,6 @@ const mockReportItemWithHold = { action: 'view', allActions: ['view'], amount: -12300, - canDelete: true, category: '', comment: { comment: '', diff --git a/tests/unit/TransactionGroupListItemTest.tsx b/tests/unit/TransactionGroupListItemTest.tsx index 6e360fdafeb79..05cc0ab57376b 100644 --- a/tests/unit/TransactionGroupListItemTest.tsx +++ b/tests/unit/TransactionGroupListItemTest.tsx @@ -26,7 +26,6 @@ jest.mock('@libs/SearchUIUtils', () => ({ const mockTransaction: TransactionListItemType = { accountID: 1, amount: 0, - canDelete: true, category: '', groupAmount: 1284, groupCurrency: 'USD', From 33ae538e7bf890da63f8c796be1a1293f6eb212b Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 5 Dec 2025 01:07:29 +0700 Subject: [PATCH 05/14] Update logic --- src/libs/ReportUtils.ts | 49 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f68c1e26d563d..8a82c37aafeb2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -136,6 +136,7 @@ import { getAccountIDsByLogins, getDisplayNameOrDefault, getEffectiveDisplayName, + getLoginByAccountID, getLoginsByAccountIDs, getPersonalDetailByEmail, getPersonalDetailsByIDs, @@ -147,6 +148,7 @@ import { getCleanedTagName, getConnectedIntegration, getCorrectedAutoReportingFrequency, + getDefaultApprover, getForwardsToAccount, getManagerAccountEmail, getManagerAccountID, @@ -1970,6 +1972,18 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return false; } + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line @typescript-eslint/no-deprecated + const submitsToAccountID = getSubmitToAccountID(getPolicy(report.policyID), report); + + return isProcessingReport(report) && submitsToAccountID === report.managerID; +} + +function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportActions: ReportAction[], policyParam?: OnyxEntry): boolean { + if (!report) { + return false; + } + if (!isProcessingReport(report)) { return false; } @@ -1980,7 +1994,7 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(report?.policyID); + const policy = policyParam ?? getPolicy(report?.policyID); if (hasDynamicExternalWorkflow(policy)) { return false; @@ -1990,11 +2004,33 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return true; } - const submitsToAccountID = getSubmitToAccountID(policy, report); + const usedReportAction = isInstantSubmitEnabled(policy) + ? reportActions?.find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) + : reportActions + ?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.SUBMITTED) + ?.sort((a, b) => { + if (!a.created || !b.created) { + return !a.created ? 1 : -1; + } + return a.created < b.created ? 1 : -1; + }) + ?.at(0); + + const originalMessage = getOriginalMessage(usedReportAction); + let submittedTo = originalMessage?.submittedTo; + + if (!submittedTo) { + submittedTo = getAccountIDsByLogins([originalMessage?.to])?.at(0); + } + + if (!submittedTo) { + const managerID = originalMessage?.managerOnVacation ?? report.managerID; + const approverAccountID = + policy?.employeeList?.[getLoginByAccountID(report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID) ?? '']?.submitsTo ?? getAccountIDsByLogins([getDefaultApprover(policy)])?.at(0); + return report.managerID === approverAccountID; + } - // Fallback to comparing current manager of the report to the submitsTo in the policy. - // If they match, then the report should still be awaiting first level approval. - return submitsToAccountID === report?.managerID; + return report.managerID === submittedTo; } /** @@ -2716,7 +2752,7 @@ function canAddOrDeleteTransactions(moneyRequestReport: OnyxEntry, polic } if (isInstantSubmitEnabled(policy) && isProcessingReport(moneyRequestReport)) { - return isAwaitingFirstLevelApproval(moneyRequestReport); + return isAwaitingFirstLevelApprovalNew(moneyRequestReport, Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`] ?? {}), policy); } if (isReportApproved({report: moneyRequestReport}) || isClosedReport(moneyRequestReport) || isSettled(moneyRequestReport?.reportID)) { @@ -13206,6 +13242,7 @@ export { requiresManualSubmission, isReportIDApproved, isAwaitingFirstLevelApproval, + isAwaitingFirstLevelApprovalNew, isPublicAnnounceRoom, isPublicRoom, isReportApproved, From be5d0eeebeac2b5ad37820c2caf234750f54c073 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 5 Dec 2025 01:34:11 +0700 Subject: [PATCH 06/14] minor Update --- src/libs/ReportUtils.ts | 2 +- tests/unit/Search/SearchUIUtilsTest.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8a82c37aafeb2..e66358fcc7f67 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2027,7 +2027,7 @@ function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportAction const managerID = originalMessage?.managerOnVacation ?? report.managerID; const approverAccountID = policy?.employeeList?.[getLoginByAccountID(report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID) ?? '']?.submitsTo ?? getAccountIDsByLogins([getDefaultApprover(policy)])?.at(0); - return report.managerID === approverAccountID; + return managerID === approverAccountID; } return report.managerID === submittedTo; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 5f07a104c29a9..03e5cbf3872fb 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1776,7 +1776,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 47; + const expectedPropertyCount = 46; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); @@ -1809,7 +1809,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 47; + const expectedPropertyCount = 46; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); From 0e831bea99ca3c8e98141ec5aea92169643bfd38 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 5 Dec 2025 01:41:04 +0700 Subject: [PATCH 07/14] update the param --- src/libs/ReportUtils.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ac85a471f770d..7ebedbc493b1a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1978,7 +1978,7 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return isProcessingReport(report) && submitsToAccountID === report.managerID; } -function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportActions: ReportAction[], policyParam?: OnyxEntry): boolean { +function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportActions: ReportAction[], policy: OnyxEntry): boolean { if (!report) { return false; } @@ -1991,10 +1991,6 @@ function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportAction return true; } - // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 - // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = policyParam ?? getPolicy(report?.policyID); - if (hasDynamicExternalWorkflow(policy)) { return false; } From d2725f166d49e7964340cf36860acea0a53f9f3a Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 8 Dec 2025 14:56:21 +0700 Subject: [PATCH 08/14] adjust eslint error --- src/libs/ReportUtils.ts | 10 +++++++--- tests/unit/Search/SearchUIUtilsTest.ts | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2e11f575f8b48..8c591c012adc0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2012,14 +2012,18 @@ function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportAction ?.at(0); const originalMessage = getOriginalMessage(usedReportAction); - let submittedTo = originalMessage?.submittedTo; + if (!originalMessage) { + return false; + } + let submittedTo: number | undefined = 'submittedTo' in originalMessage ? originalMessage?.submittedTo : undefined; if (!submittedTo) { - submittedTo = getAccountIDsByLogins([originalMessage?.to])?.at(0); + const submittedToLogin = 'to' in originalMessage ? (originalMessage?.to ?? '') : ''; + submittedTo = getAccountIDsByLogins([submittedToLogin])?.at(0); } if (!submittedTo) { - const managerID = originalMessage?.managerOnVacation ?? report.managerID; + const managerID = 'managerOnVacation' in originalMessage && originalMessage?.managerOnVacation ? originalMessage?.managerOnVacation : report.managerID; const approverAccountID = policy?.employeeList?.[getLoginByAccountID(report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID) ?? '']?.submitsTo ?? getAccountIDsByLogins([getDefaultApprover(policy)])?.at(0); return managerID === approverAccountID; diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index de3441a873afd..d13935878eabc 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -2815,7 +2815,6 @@ describe('SearchUIUtils', () => { violations, hash: itemHash, moneyRequestReportActionID, - canDelete, accountID, policyID: searchPolicyID, ...expectedTransaction From 77182b2fa346793a8e42622b225df38b1ab1a359 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 8 Dec 2025 19:38:46 +0700 Subject: [PATCH 09/14] solve comment --- src/libs/ReportSecondaryActionUtils.ts | 1 + src/libs/ReportUtils.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 60345938b458a..292fd6d61026c 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -125,6 +125,7 @@ function isSplitAction(report: Report, reportTransactions: Transaction[], origin } // Hide split option for the submitter if the report is forwarded + // eslint-disable-next-line @typescript-eslint/no-deprecated return (isSubmitter && isAwaitingFirstLevelApproval(report)) || isAdmin || isManager; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8c591c012adc0..904eeab20e77c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1966,6 +1966,9 @@ function requiresManualSubmission(report: OnyxEntry, policy: OnyxEntry

): boolean { if (!report) { return false; @@ -1999,6 +2002,9 @@ function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportAction return true; } + // If the report is part of a policy with Instant Submit, this data should be stored in the CREATED action + // as Instant Submit reports do not have a SUBMITTED action. + // For all other cases, use the most recent SUBMITTED action instead. const usedReportAction = isInstantSubmitEnabled(policy) ? reportActions?.find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) : reportActions @@ -2810,6 +2816,7 @@ function isMoneyRequestReportEligibleForMerge(reportID: string, isAdmin: boolean } if (isSubmitter) { + // eslint-disable-next-line @typescript-eslint/no-deprecated return isOpenReport(report) || (isIOUReport(report) && isProcessingReport(report)) || isAwaitingFirstLevelApproval(report); } @@ -4701,6 +4708,7 @@ function canEditReportPolicy(report: OnyxEntry, reportPolicy: OnyxEntry< } if (isSubmitted) { + // eslint-disable-next-line @typescript-eslint/no-deprecated return (isSubmitter && isAwaitingFirstLevelApproval(report)) || isManager || isAdmin; } From 33a74c58030b2bc508855d8a3d6239125574b6d7 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 8 Dec 2025 20:02:42 +0700 Subject: [PATCH 10/14] add deprecater --- src/libs/ReportSecondaryActionUtils.ts | 2 +- src/libs/ReportUtils.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 292fd6d61026c..646d6d5fc78d3 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -33,7 +33,7 @@ import { hasOnlyNonReimbursableTransactions, hasReportBeenReopened as hasReportBeenReopenedUtils, hasReportBeenRetracted as hasReportBeenRetractedUtils, - isArchivedReport, + isArchivedReport, // eslint-disable-next-line @typescript-eslint/no-deprecated isAwaitingFirstLevelApproval, isClosedReport as isClosedReportUtils, isCurrentUserSubmitter, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 904eeab20e77c..edcec17e7fd36 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13301,6 +13301,7 @@ export { isOpenReport, requiresManualSubmission, isReportIDApproved, + // eslint-disable-next-line @typescript-eslint/no-deprecated isAwaitingFirstLevelApproval, isAwaitingFirstLevelApprovalNew, isPublicAnnounceRoom, From 4b98045d7dc389c9ccae99d1f3e9cb898bd427de Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 8 Dec 2025 23:59:41 +0700 Subject: [PATCH 11/14] update UTs --- tests/unit/Search/SearchUIUtilsTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 84183d7232b2b..84a1bb786f04d 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -1696,7 +1696,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 46; + const expectedPropertyCount = 45; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); @@ -1729,7 +1729,7 @@ describe('SearchUIUtils', () => { expect(distanceTransaction).toBeDefined(); expect(distanceTransaction?.iouRequestType).toBe(CONST.IOU.REQUEST_TYPE.DISTANCE); - const expectedPropertyCount = 46; + const expectedPropertyCount = 45; expect(Object.keys(distanceTransaction ?? {}).length).toBe(expectedPropertyCount); }); From da1623a56d4ceadc9f4d6371cf6aa14e4160d153 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 9 Dec 2025 00:11:18 +0700 Subject: [PATCH 12/14] update UTs --- src/libs/SearchUIUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 40c5e350db11d..afdceca0686d9 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2651,7 +2651,6 @@ function getTransactionFromTransactionListItem(item: TransactionListItemType): O isTaxAmountColumnWide, violations, hash, - canDelete, accountID, policyID, ...transaction From c866c5f42a521ccbb678f4e380b26797d11a9b3e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 9 Dec 2025 09:51:20 +0700 Subject: [PATCH 13/14] Trigger GH actions --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 8b0f065abb87a..ab8a5e3facb9e 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -2980,7 +2980,7 @@ describe('ReportUtils', () => { }); }); - describe('return multiple expense options if', () => { + describe('return multiple expense options', () => { it('it is a 1:1 DM', () => { const report = { ...LHNTestUtils.getFakeReport(), From a18123f7f39d04688adadbb98291173e075840c7 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 9 Dec 2025 09:51:34 +0700 Subject: [PATCH 14/14] revert --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index ab8a5e3facb9e..8b0f065abb87a 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -2980,7 +2980,7 @@ describe('ReportUtils', () => { }); }); - describe('return multiple expense options', () => { + describe('return multiple expense options if', () => { it('it is a 1:1 DM', () => { const report = { ...LHNTestUtils.getFakeReport(),