Skip to content
2 changes: 1 addition & 1 deletion src/hooks/useMoneyReportHeaderStatusBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function useMoneyReportHeaderStatusBar(reportID: string | undefined, chatReportI
const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(transactions);
const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction);
const isReportSettled = isSettledReportUtils(moneyRequestReport);
const hasDuplicates = !isReportSettled && hasDuplicateTransactions(email ?? '', accountID, moneyRequestReport, ownerLogin, policy, allTransactionViolations);
const hasDuplicates = !isReportSettled && hasDuplicateTransactions(email ?? '', accountID, moneyRequestReport, ownerLogin, policy, allTransactionViolations, transactions);
const shouldShowMarkAsResolved = isMarkAsResolvedAction(moneyRequestReport, transactionViolations);

const shouldShowStatusBar =
Expand Down
10 changes: 4 additions & 6 deletions src/libs/ReportPreviewActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {isAddExpenseAction} from './ReportPrimaryActionUtils';
import {
getMoneyRequestSpendBreakdown,
getParentReport,
getReportTransactions,
hasExportError as hasExportErrorUtil,
hasOnlyNonReimbursableTransactions,
isClosedReport,
Expand Down Expand Up @@ -80,7 +79,7 @@ function canSubmit(
return isExpense && isSubmitter && isOpen && !isAnyReceiptBeingScanned && !!transactions && transactions.length > 0;
}

function canApprove(report: Report, currentUserAccountID: number, reportMetadata: OnyxEntry<ReportMetadata>, policy?: Policy, transactions?: Transaction[]) {
function canApprove(report: Report, currentUserAccountID: number, reportMetadata: OnyxEntry<ReportMetadata>, policy: Policy | undefined, transactions: Transaction[]) {
if (isArchivedOrPendingDeletePolicy(policy)) {
return false;
}
Expand All @@ -94,14 +93,13 @@ function canApprove(report: Report, currentUserAccountID: number, reportMetadata
const isApprovalEnabled = policy?.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL;
const managerID = report.managerID ?? CONST.DEFAULT_NUMBER_ID;
const isCurrentUserManager = managerID === currentUserAccountID;
const reportTransactions = transactions ?? getReportTransactions(report?.reportID);
const isAnyReceiptBeingScanned = transactions?.some((transaction) => isScanning(transaction));
const isAnyReceiptBeingScanned = transactions.some((transaction) => isScanning(transaction));

if (isAnyReceiptBeingScanned) {
return false;
}

if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPending(transaction))) {
if (transactions.length > 0 && transactions.every((transaction) => isPending(transaction))) {
return false;
}

Expand All @@ -117,7 +115,7 @@ function canApprove(report: Report, currentUserAccountID: number, reportMetadata
return false;
}

return isExpense && isProcessing && !!isApprovalEnabled && reportTransactions.length > 0 && isCurrentUserManager;
return isExpense && isProcessing && !!isApprovalEnabled && transactions.length > 0 && isCurrentUserManager;
}

function canPay(
Expand Down
4 changes: 1 addition & 3 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2543,10 +2543,8 @@ function hasDuplicateTransactions(
ownerLogin: string | undefined,
policy: OnyxEntry<Policy>,
allTransactionViolations: OnyxCollection<TransactionViolation[]>,
reportTransactions: Transaction[],
): boolean {
const transactionsByIouReportID = getReportTransactions(iouReport?.reportID);
const reportTransactions = transactionsByIouReportID;

return (
reportTransactions.length > 0 &&
reportTransactions.some((transaction) =>
Expand Down
123 changes: 123 additions & 0 deletions tests/unit/hooks/useMoneyReportHeaderStatusBarTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {renderHook} from '@testing-library/react-native';

import useMoneyReportHeaderStatusBar from '@hooks/useMoneyReportHeaderStatusBar';

import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportPrimaryActionUtils from '@libs/ReportPrimaryActionUtils';
import * as TransactionUtils from '@libs/TransactionUtils';

import CONST from '@src/CONST';
import type {Report, Transaction} from '@src/types/onyx';

import createMock from '../../utils/createMock';

const REPORT_ID = 'report1';
const CHAT_REPORT_ID = 'chatReport1';

// Prefixed with `mock` so they can be referenced inside the hoisted jest.mock factory below.
const mockTransaction1 = createMock<Transaction>({transactionID: 'transaction1', reportID: REPORT_ID});
const mockTransaction2 = createMock<Transaction>({transactionID: 'transaction2', reportID: REPORT_ID});
const mockMoneyRequestReport = createMock<Report>({reportID: REPORT_ID, type: 'iou'});

jest.mock('@hooks/useNetwork', () => ({
__esModule: true,
default: () => ({isOffline: false}),
}));

jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({
__esModule: true,
default: () => ({accountID: 1, email: 'test@example.com'}),
}));

jest.mock('@hooks/usePaginatedReportActions', () => ({
__esModule: true,
default: () => ({reportActions: []}),
}));

jest.mock('@hooks/useReportTransactionsCollection', () => ({
__esModule: true,
default: () => ({}),
}));

jest.mock('@hooks/useTransactionViolations', () => ({
__esModule: true,
default: () => [],
}));

jest.mock('@hooks/useReportIsArchived', () => ({
__esModule: true,
default: () => false,
}));

jest.mock('@hooks/useTransactionsAndViolationsForReport', () => ({
__esModule: true,
default: () => ({transactions: {transaction1: mockTransaction1, transaction2: mockTransaction2}, violations: {}}),
}));

jest.mock('@hooks/useOnyx', () => ({
__esModule: true,
default: (key: string) => {
if (key === `report_${REPORT_ID}`) {
return [mockMoneyRequestReport];
}
return [undefined];
},
}));

describe('useMoneyReportHeaderStatusBar - duplicate transactions', () => {
beforeEach(() => {
// Every other status-bar condition is stubbed to false so the test can isolate the hasDuplicates branch,
// which is the line this hook's PR changed (threading `transactions` into hasDuplicateTransactions
// instead of it recomputing them via the deprecated getReportTransactions default).
jest.spyOn(ReportActionsUtils, 'getFilteredReportActionsForReportView').mockReturnValue([]);
jest.spyOn(ReportActionsUtils, 'getOneTransactionThreadReportID').mockReturnValue(undefined);
jest.spyOn(ReportActionsUtils, 'getOriginalMessage').mockReturnValue(undefined);
jest.spyOn(ReportActionsUtils, 'isMoneyRequestAction').mockReturnValue(false);
jest.spyOn(ReportPrimaryActionUtils, 'isMarkAsResolvedAction').mockReturnValue(false);
// isPaidGroupPolicy/isPaidGroupPolicyExpenseReport are billing-only and restricted from static import.
// This hook never touches them, but a namespace import can't be statically proven not to, so these
// ReportUtils spies go through require() instead (matches other tests spying on this module).
jest.spyOn(require('@libs/ReportUtils'), 'hasOnlyHeldExpenses').mockReturnValue(false);
jest.spyOn(require('@libs/ReportUtils'), 'isSettled').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'allHavePendingRTERViolation').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'hasDuplicateTransactions').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'isBrokenConnectionViolation').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'hasReceipt').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'isPayAtEndExpense').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'isPending').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'isScanning').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'shouldSuppressBrokenConnectionStatus').mockReturnValue(false);
jest.spyOn(TransactionUtils, 'shouldShowBrokenConnectionViolationForMultipleTransactions').mockReturnValue(false);
});

afterEach(() => {
jest.restoreAllMocks();
});

it('passes the report transactions through to hasDuplicateTransactions', () => {
renderHook(() => useMoneyReportHeaderStatusBar(REPORT_ID, CHAT_REPORT_ID));

expect(jest.mocked(TransactionUtils.hasDuplicateTransactions)).toHaveBeenCalledWith('test@example.com', 1, mockMoneyRequestReport, undefined, undefined, undefined, [
mockTransaction1,
mockTransaction2,
]);
});

it('shows the duplicates status bar when hasDuplicateTransactions reports duplicates on an unsettled report', () => {
jest.spyOn(TransactionUtils, 'hasDuplicateTransactions').mockReturnValue(true);

const {result} = renderHook(() => useMoneyReportHeaderStatusBar(REPORT_ID, CHAT_REPORT_ID));

expect(result.current.shouldShowStatusBar).toBe(true);
expect(result.current.statusBarType).toBe(CONST.REPORT.STATUS_BAR_TYPE.DUPLICATES);
});

it('does not show the duplicates status bar when the report is already settled, even if duplicates are found', () => {
jest.spyOn(TransactionUtils, 'hasDuplicateTransactions').mockReturnValue(true);
jest.spyOn(require('@libs/ReportUtils'), 'isSettled').mockReturnValue(true);

const {result} = renderHook(() => useMoneyReportHeaderStatusBar(REPORT_ID, CHAT_REPORT_ID));

expect(result.current.statusBarType).not.toBe(CONST.REPORT.STATUS_BAR_TYPE.DUPLICATES);
});
});
Loading