diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f39a37d5a7a2d..285a89575e5bf 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -32,7 +32,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, @@ -97,7 +97,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, @@ -179,7 +179,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/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/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 60345938b458a..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, @@ -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 b5456372c9abe..a605f50a9aaf7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -135,6 +135,7 @@ import { getAccountIDsByLogins, getDisplayNameOrDefault, getEffectiveDisplayName, + getLoginByAccountID, getLoginsByAccountIDs, getPersonalDetailByEmail, getPersonalDetailsByIDs, @@ -146,6 +147,7 @@ import { getCleanedTagName, getConnectedIntegration, getCorrectedAutoReportingFrequency, + getDefaultApprover, getForwardsToAccount, getManagerAccountEmail, getManagerAccountID, @@ -155,6 +157,7 @@ import { getRuleApprovers, getSubmitToAccountID, hasDependentTags as hasDependentTagsPolicyUtils, + hasDynamicExternalWorkflow, isExpensifyTeam, isInstantSubmitEnabled, isPaidGroupPolicy as isPaidGroupPolicyPolicyUtils, @@ -1438,7 +1441,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; } @@ -1963,6 +1966,9 @@ function requiresManualSubmission(report: OnyxEntry, policy: OnyxEntry

): boolean { if (!report) { return false; @@ -1975,6 +1981,63 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry): boolean { return isProcessingReport(report) && submitsToAccountID === report.managerID; } +function isAwaitingFirstLevelApprovalNew(report: OnyxEntry, reportActions: ReportAction[], policy: OnyxEntry): boolean { + if (!report) { + return false; + } + + if (!isProcessingReport(report)) { + return false; + } + + if (isIOUReportUsingReport(report)) { + return true; + } + + if (hasDynamicExternalWorkflow(policy)) { + return false; + } + + if (policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC) { + 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 + ?.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); + if (!originalMessage) { + return false; + } + let submittedTo: number | undefined = 'submittedTo' in originalMessage ? originalMessage?.submittedTo : undefined; + + if (!submittedTo) { + const submittedToLogin = 'to' in originalMessage ? (originalMessage?.to ?? '') : ''; + submittedTo = getAccountIDsByLogins([submittedToLogin])?.at(0); + } + + if (!submittedTo) { + 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; + } + + return report.managerID === submittedTo; +} + /** * Updates optimistic transaction violations to OnyxData for the given policy and categories onyx update. * @@ -2684,21 +2747,17 @@ 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; } 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)) { @@ -2727,17 +2786,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); } /** @@ -2767,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); } @@ -2953,7 +3003,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; @@ -4659,6 +4709,7 @@ function canEditReportPolicy(report: OnyxEntry, reportPolicy: OnyxEntry< } if (isSubmitted) { + // eslint-disable-next-line @typescript-eslint/no-deprecated return (isSubmitter && isAwaitingFirstLevelApproval(report)) || isManager || isAdmin; } @@ -12981,7 +13032,7 @@ export { canAccessReport, isReportNotFound, canAddTransaction, - canDeleteTransaction, + canAddOrDeleteTransactions, canBeAutoReimbursed, canCreateRequest, canCreateTaskInReport, @@ -13197,7 +13248,9 @@ export { isOpenReport, requiresManualSubmission, isReportIDApproved, + // eslint-disable-next-line @typescript-eslint/no-deprecated isAwaitingFirstLevelApproval, + isAwaitingFirstLevelApprovalNew, isPublicAnnounceRoom, isPublicRoom, isReportApproved, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 8bc327aac487f..b7460981d5e31 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -2659,7 +2659,6 @@ function getTransactionFromTransactionListItem(item: TransactionListItemType): O isTaxAmountColumnWide, violations, hash, - canDelete, accountID, policyID, ...transaction diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index a8f1257a37af7..f99ba81a3dd33 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -48,8 +48,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, @@ -291,7 +291,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/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 08b66a5086afc..8b90d653cbbb7 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -86,9 +86,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/MoneyRequestReportUtilsTest.ts b/tests/unit/MoneyRequestReportUtilsTest.ts index 7bfdb21051833..9cae1e021d328 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/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 08f5933099d87..8b0f065abb87a 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -30,11 +30,11 @@ import { buildOptimisticReportPreview, buildParticipantsFromAccountIDs, buildTransactionThread, + canAddOrDeleteTransactions, canAddTransaction, canCreateRequest, canDeleteMoneyRequestReport, canDeleteReportAction, - canDeleteTransaction, canEditMoneyRequest, canEditReportDescription, canEditRoomVisibility, @@ -5984,7 +5984,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 = { @@ -5996,7 +5996,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 = canAddOrDeleteTransactions(report, policy, isReportArchived.current); // Then the result is true expect(result).toBe(true); @@ -6013,7 +6013,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 = canAddOrDeleteTransactions(report, policy, isReportArchived.current); // Then the result is false expect(result).toBe(false); @@ -6062,7 +6062,7 @@ describe('ReportUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${openReport.reportID}`, openReport); - expect(canDeleteTransaction(openReport, false)).toBe(true); + expect(canAddOrDeleteTransactions(openReport, policy, false)).toBe(true); }); it('should return false for closed report when workflow is disabled', async () => { @@ -6075,7 +6075,7 @@ describe('ReportUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${closedReport.reportID}`, closedReport); - expect(canDeleteTransaction(closedReport, false)).toBe(false); + expect(canAddOrDeleteTransactions(closedReport, policy, false)).toBe(false); }); }); }); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 2d689a07118d2..6abd283de26b0 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -386,7 +386,6 @@ const searchResults: OnyxTypes.SearchResults = { [`report_${reportID5}`]: report5, [`transactions_${transactionID}`]: { amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -416,7 +415,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: '', @@ -477,7 +474,6 @@ const searchResults: OnyxTypes.SearchResults = { }, [`transactions_${transactionID4}`]: { amount: 3200, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -763,7 +759,6 @@ const transactionsListItems = [ policy, reportAction: reportAction1, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -815,7 +810,6 @@ const transactionsListItems = [ policy, reportAction: reportAction2, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -877,7 +871,6 @@ const transactionsListItems = [ policy, reportAction: reportAction3, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -934,7 +927,6 @@ const transactionsListItems = [ policy, reportAction: reportAction4, holdReportAction: undefined, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -1028,7 +1020,6 @@ const transactionReportGroupListItems = [ reportAction: reportAction1, holdReportAction: undefined, amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -1123,7 +1114,6 @@ const transactionReportGroupListItems = [ reportAction: reportAction2, holdReportAction: undefined, amount: -5000, - canDelete: true, cardID: undefined, cardName: undefined, category: '', @@ -1706,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); }); @@ -1739,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); }); @@ -2331,7 +2321,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: '', @@ -2454,7 +2443,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',