diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index dc023a2890a2..1d7aa33f30e0 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -26,6 +26,7 @@ import { getTransactionDetails, hasOnlyHeldExpenses, isArchivedReport, + isAwaitingFirstLevelApproval, isClosedReport as isClosedReportUtils, isCurrentUserSubmitter, isExpenseReport as isExpenseReportUtils, @@ -37,6 +38,7 @@ import { isProcessingReport as isProcessingReportUtils, isReportApproved as isReportApprovedUtils, isReportManager as isReportManagerUtils, + isSelfDM as isSelfDMReportUtils, isSettled, isWorkspaceEligibleForReportChange, } from './ReportUtils'; @@ -45,6 +47,7 @@ import { allHavePendingRTERViolation, getOriginalTransactionWithSplitInfo, hasReceipt as hasReceiptTransactionUtils, + isCardTransaction as isCardTransactionUtils, isDuplicate, isOnHold as isOnHoldTransactionUtils, isPending, @@ -396,29 +399,42 @@ function isMoveTransactionAction(reportTransactions: Transaction[], reportAction return canMoveExpense; } -function isDeleteAction(report: Report): boolean { +function isDeleteAction(report: Report, reportTransactions: Transaction[], reportActions: ReportAction[], policy?: Policy): boolean { const isExpenseReport = isExpenseReportUtils(report); const isIOUReport = isIOUReportUtils(report); + const isUnreported = isSelfDMReportUtils(report); + const transaction = reportTransactions.at(0); + const transactionID = transaction?.transactionID; + const isOwner = transactionID ? getIOUActionForTransactionID(reportActions, transactionID)?.actorAccountID === getCurrentUserAccountID() : false; + const isReportOpenOrProcessing = isOpenReportUtils(report) || isProcessingReportUtils(report); + const isSingleTransaction = reportTransactions.length === 1; - if (!isExpenseReport && !isIOUReport) { - return false; + if (isUnreported) { + return isOwner; } - const isReportSubmitter = isCurrentUserSubmitter(report.reportID); - - if (!isReportSubmitter) { - return false; + // Users cannot delete a report in the unrepeorted or IOU cases, but they can delete individual transactions. + // So we check if the reportTransactions length is 1 which means they're viewing a single transaction and thus can delete it. + if (isIOUReport) { + return isSingleTransaction && isOwner && isReportOpenOrProcessing; } - const isReportOpen = isOpenReportUtils(report); - const isProcessingReport = isProcessingReportUtils(report); - const isReportApproved = isReportApprovedUtils({report}); + if (isExpenseReport) { + const isCardTransactionWithCorporateLiability = + isSingleTransaction && isCardTransactionUtils(transaction) && transaction?.comment?.liabilityType === CONST.TRANSACTION.LIABILITY_TYPE.RESTRICT; - if (isReportApproved) { - return false; + if (isCardTransactionWithCorporateLiability) { + return false; + } + + const isReportSubmitter = isCurrentUserSubmitter(report.reportID); + const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; + const isForwarded = isProcessingReportUtils(report) && isApprovalEnabled && !isAwaitingFirstLevelApproval(report); + + return isReportSubmitter && isReportOpenOrProcessing && !isForwarded; } - return isReportOpen || isProcessingReport; + return false; } function isRetractAction(report: Report, policy?: Policy): boolean { @@ -539,7 +555,7 @@ function getSecondaryReportActions( options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS); - if (isDeleteAction(report)) { + if (isDeleteAction(report, reportTransactions, reportActions ?? [], policy)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.DELETE); } @@ -564,7 +580,7 @@ function getSecondaryTransactionThreadActions( options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS); - if (isDeleteAction(parentReport)) { + if (isDeleteAction(parentReport, [reportTransaction], reportActions ?? [])) { options.push(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DELETE); } diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index cd60c1e0bc73..027958abeaa2 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -41,7 +41,7 @@ describe('getSecondaryAction', () => { jest.clearAllMocks(); Onyx.clear(); await Onyx.merge(ONYXKEYS.SESSION, SESSION); - await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS}); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[EMPLOYEE_ACCOUNT_ID]: PERSONAL_DETAILS, [APPROVER_ACCOUNT_ID]: {accountID: APPROVER_ACCOUNT_ID, login: APPROVER_EMAIL}}); }); it('should always return default options', () => { @@ -654,6 +654,232 @@ describe('getSecondaryAction', () => { const result = getSecondaryReportActions(report, [{} as Transaction], {}, policy); expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true); }); + + it('includes DELETE option for owner of unreported transaction', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + reportID: CONST.REPORT.UNREPORTED_REPORT_ID, + } as unknown as Transaction; + + const reportActions = [ + { + reportActionID: '1', + actorAccountID: EMPLOYEE_ACCOUNT_ID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: TRANSACTION_ID, + IOUReportID: CONST.REPORT.UNREPORTED_REPORT_ID, + }, + }, + ] as unknown as ReportAction[]; + + const policy = {} as unknown as Policy; + + const result = getSecondaryReportActions(report, [transaction], {}, policy, undefined, reportActions); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true); + }); + + it('includes DELETE option for owner of single processing IOU transaction', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + reportID: REPORT_ID, + } as unknown as Transaction; + + const reportActions = [ + { + reportActionID: '1', + actorAccountID: EMPLOYEE_ACCOUNT_ID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: TRANSACTION_ID, + IOUReportID: REPORT_ID, + }, + }, + ] as unknown as ReportAction[]; + + const policy = {} as unknown as Policy; + + const result = getSecondaryReportActions(report, [transaction], {}, policy, undefined, reportActions); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true); + }); + + it('does not include DELETE option for IOU report', () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + const TRANSACTION_ID_2 = 'TRANSACTION_ID_2'; + + const transaction1 = { + transactionID: TRANSACTION_ID, + reportID: REPORT_ID, + } as unknown as Transaction; + + const transaction2 = { + transactionID: TRANSACTION_ID_2, + reportID: REPORT_ID, + } as unknown as Transaction; + + const reportActions = [ + { + reportActionID: '1', + actorAccountID: EMPLOYEE_ACCOUNT_ID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: TRANSACTION_ID, + IOUReportID: REPORT_ID, + }, + }, + { + reportActionID: '2', + actorAccountID: EMPLOYEE_ACCOUNT_ID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + originalMessage: { + IOUTransactionID: TRANSACTION_ID_2, + IOUReportID: REPORT_ID, + }, + }, + ] as unknown as ReportAction[]; + + const policy = {} as unknown as Policy; + + const result = getSecondaryReportActions(report, [transaction1, transaction2], {}, policy, undefined, reportActions); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(false); + }); + + it('includes DELETE option for owner of single processing expense transaction', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + reportID: REPORT_ID, + } as unknown as Transaction; + + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryReportActions(report, [transaction], {}, policy); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true); + }); + + it('includes DELETE option for owner of processing expense report', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + const TRANSACTION_ID_2 = 'TRANSACTION_ID_2'; + + const transaction1 = { + transactionID: TRANSACTION_ID, + reportID: REPORT_ID, + } as unknown as Transaction; + + const transaction2 = { + transactionID: TRANSACTION_ID_2, + reportID: REPORT_ID, + } as unknown as Transaction; + + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryReportActions(report, [transaction1, transaction2], {}, policy); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true); + }); + + it('does not include DELETE option for corporate liability card transaction', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + reportID: REPORT_ID, + managedCard: true, + comment: { + liabilityType: CONST.TRANSACTION.LIABILITY_TYPE.RESTRICT, + }, + } as unknown as Transaction; + + const policy = {} as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const result = getSecondaryReportActions(report, [transaction], {}, policy); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(false); + }); + + it('does not include DELETE option for report that has been forwarded', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + managerID: MANAGER_ACCOUNT_ID, + policyID: POLICY_ID, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + } as unknown as Report; + + const TRANSACTION_ID = 'TRANSACTION_ID'; + + const transaction = { + transactionID: TRANSACTION_ID, + reportID: REPORT_ID, + } as unknown as Transaction; + + const policy = { + id: POLICY_ID, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + approver: APPROVER_EMAIL, + } as unknown as Policy; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy); + + const result = getSecondaryReportActions(report, [transaction], {}, policy); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(false); + }); }); describe('getSecondaryTransactionThreadActions', () => {