Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getTransactionDetails,
hasOnlyHeldExpenses,
isArchivedReport,
isAwaitingFirstLevelApproval,
isClosedReport as isClosedReportUtils,
isCurrentUserSubmitter,
isExpenseReport as isExpenseReportUtils,
Expand All @@ -37,6 +38,7 @@ import {
isProcessingReport as isProcessingReportUtils,
isReportApproved as isReportApprovedUtils,
isReportManager as isReportManagerUtils,
isSelfDM as isSelfDMReportUtils,
isSettled,
isWorkspaceEligibleForReportChange,
} from './ReportUtils';
Expand All @@ -45,6 +47,7 @@ import {
allHavePendingRTERViolation,
getOriginalTransactionWithSplitInfo,
hasReceipt as hasReceiptTransactionUtils,
isCardTransaction as isCardTransactionUtils,
isDuplicate,
isOnHold as isOnHoldTransactionUtils,
isPending,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down
228 changes: 227 additions & 1 deletion tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down