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
4 changes: 3 additions & 1 deletion src/pages/Search/SearchTransactionsChangeReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav
import Navigation from '@libs/Navigation/Navigation';
import {generateReportID, getPersonalDetailsForAccountID, getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';
import {isManualDistanceRequest as isManualDistanceRequestUtil, isOdometerDistanceRequest as isOdometerDistanceRequestUtil, isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';

import IOURequestEditReportCommon from '@pages/iou/request/step/IOURequestEditReportCommon';

Expand Down Expand Up @@ -304,6 +304,8 @@ function SearchTransactionsChangeReport() {
<IOURequestEditReportCommon
backTo={undefined}
transactionIDs={selectedTransactionsKeys}
isManualDistanceRequest={transactions.some(isManualDistanceRequestUtil)}
isOdometerDistanceRequest={transactions.some(isOdometerDistanceRequestUtil)}
selectedReportID={selectedReportID}
selectReport={selectReport}
removeFromReport={removeFromReport}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav
import Navigation from '@libs/Navigation/Navigation';
import {getPersonalDetailsForAccountID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';
import {isManualDistanceRequest as isManualDistanceRequestUtil, isOdometerDistanceRequest as isOdometerDistanceRequestUtil, isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';

import {createNewReport} from '@userActions/Report';

Expand Down Expand Up @@ -219,6 +219,8 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps)
backTo={backPath}
selectedReportID={reportID}
transactionIDs={transactionIDs}
isManualDistanceRequest={transactions.some(isManualDistanceRequestUtil)}
isOdometerDistanceRequest={transactions.some(isOdometerDistanceRequestUtil)}
selectReport={selectReport}
removeFromReport={removeFromReport}
isEditing={action === CONST.IOU.ACTION.EDIT}
Expand Down
10 changes: 9 additions & 1 deletion src/pages/iou/request/step/DynamicIOURequestStepReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea
import Navigation from '@libs/Navigation/Navigation';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getPersonalDetailsForAccountID, getReportOrDraftReport, isPolicyExpenseChat, isReportOutstanding} from '@libs/ReportUtils';
import {isPerDiemRequest, isTimeRequest as isTimeRequestUtil, isUnreportedManagedCardTransaction as isUnreportedManagedCardTransactionUtil} from '@libs/TransactionUtils';
import {
isManualDistanceRequest as isManualDistanceRequestUtil,
isOdometerDistanceRequest as isOdometerDistanceRequestUtil,
isPerDiemRequest,
isTimeRequest as isTimeRequestUtil,
isUnreportedManagedCardTransaction as isUnreportedManagedCardTransactionUtil,
} from '@libs/TransactionUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -220,6 +226,8 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep
backTo={backPath}
selectReport={selectReport}
transactionIDs={transaction ? [transaction.transactionID] : []}
isManualDistanceRequest={transactions.some(isManualDistanceRequestUtil)}
isOdometerDistanceRequest={transactions.some(isOdometerDistanceRequestUtil)}
selectedReportID={selectedReportID}
selectedPolicyID={selectedPolicyID}
transactionPolicyID={targetExpensePolicyID}
Expand Down
17 changes: 16 additions & 1 deletion src/pages/iou/request/step/IOURequestEditReportCommon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {ListItem} from '@components/SelectionList/types';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';

import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useCommuterExclusionGuard from '@hooks/useCommuterExclusionGuard';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
Expand Down Expand Up @@ -44,6 +45,8 @@ type TransactionGroupListItem = ListItem & {
type Props = {
backTo: Route | undefined;
transactionIDs?: string[];
isManualDistanceRequest: boolean;
isOdometerDistanceRequest: boolean;
selectedReportID?: string;
selectedPolicyID?: string;
transactionPolicyID?: string;
Expand All @@ -62,6 +65,8 @@ type Props = {
function IOURequestEditReportCommon({
backTo,
transactionIDs,
isManualDistanceRequest,
isOdometerDistanceRequest,
selectReport,
selectedReportID,
selectedPolicyID,
Expand Down Expand Up @@ -105,6 +110,10 @@ function IOURequestEditReportCommon({
const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, transactionPolicyID, isUnreportedManagedCardTransaction);

const [perDiemWarningModalVisible, setPerDiemWarningModalVisible] = useState(false);
const blockManualOrOdometerDistanceRequestIfNeeded = useCommuterExclusionGuard({
isManualDistanceRequest,
isOdometerDistanceRequest,
});

const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
const isSelectedReportUnreported = useMemo(() => !!(isUnreported ?? selectedReportID === CONST.REPORT.UNREPORTED_REPORT_ID), [isUnreported, selectedReportID]);
Expand Down Expand Up @@ -246,6 +255,9 @@ function IOURequestEditReportCommon({
navigateBack();
return;
}
if (blockManualOrOdometerDistanceRequestIfNeeded(item.policyID)) {
return;
}
const itemPolicy = item.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] : undefined;
if (
item?.policyID &&
Expand All @@ -264,11 +276,14 @@ function IOURequestEditReportCommon({
};

const handleCreateReport = useCallback(() => {
if (blockManualOrOdometerDistanceRequestIfNeeded(policyForMovingExpenses?.id)) {
return;
}
if (!validatePerDiemMove(policyForMovingExpenses?.id)) {
return;
}
createReport?.();
}, [validatePerDiemMove, policyForMovingExpenses?.id, createReport]);
}, [blockManualOrOdometerDistanceRequestIfNeeded, validatePerDiemMove, policyForMovingExpenses?.id, createReport]);

const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions.length, translate]);

Expand Down
104 changes: 101 additions & 3 deletions tests/ui/IOURequestEditReportCommonTest.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {act, render, screen} from '@testing-library/react-native';
import {act, fireEvent, render, screen} from '@testing-library/react-native';

import ComposeProviders from '@components/ComposeProviders';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
Expand All @@ -24,19 +24,47 @@ const FAKE_TRANSACTION_ID = '2';
const FAKE_EMAIL = 'fake@gmail.com';
const FAKE_ACCOUNT_ID = 1;
const FAKE_SECOND_ACCOUNT_ID = 2;
const mockShowConfirmModal = jest.fn();

jest.mock('@hooks/useConfirmModal', () => () => ({
showConfirmModal: mockShowConfirmModal,
}));

/**
* Helper function to render the IOURequestEditReportCommon component with required providers.
* This encapsulates the component setup and makes tests more readable.
*/
const renderIOURequestEditReportCommon = ({selectedReportID = '', selectedPolicyID}: {selectedReportID: string; selectedPolicyID?: string}) =>
const renderIOURequestEditReportCommon = ({
selectedReportID = '',
selectedPolicyID,
transactionPolicyID,
transactionIDs,
isManualDistanceRequest = false,
isOdometerDistanceRequest = false,
selectReport = jest.fn(),
createReport,
}: {
selectedReportID: string;
selectedPolicyID?: string;
transactionPolicyID?: string;
transactionIDs?: string[];
isManualDistanceRequest?: boolean;
isOdometerDistanceRequest?: boolean;
selectReport?: jest.Mock;
createReport?: jest.Mock;
}) =>
render(
<NavigationContainer>
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
<IOURequestEditReportCommon
selectedReportID={selectedReportID}
selectedPolicyID={selectedPolicyID}
selectReport={jest.fn()}
transactionPolicyID={transactionPolicyID}
transactionIDs={transactionIDs}
isManualDistanceRequest={isManualDistanceRequest}
isOdometerDistanceRequest={isOdometerDistanceRequest}
selectReport={selectReport}
createReport={createReport}
backTo=""
isPerDiemRequest={false}
/>
Expand Down Expand Up @@ -110,6 +138,76 @@ describe('IOURequestEditReportCommon', () => {
const dotIndicators = screen.queryAllByTestId(CONST.DOT_INDICATOR_TEST_ID);
expect(dotIndicators).toHaveLength(0);
});

const setUpCommuterExclusionTest = async () => {
const currentReport: Report = {
reportID: 'currentReport',
reportName: 'Current Report',
ownerAccountID: FAKE_ACCOUNT_ID,
policyID: 'currentPolicy',
};
await act(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${currentReport.reportID}`, currentReport);
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}`, {
...createRandomPolicy(Number(FAKE_POLICY_ID), CONST.POLICY.TYPE.TEAM),
role: CONST.POLICY.ROLE.ADMIN,
pendingAction: undefined,
commuterExclusions: {
method: CONST.POLICY.COMMUTER_EXCLUSION_METHOD.FIXED_DISTANCE,
fixedDistance: 1,
fixedDistanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
},
});
});
await waitForBatchedUpdatesWithAct();

return currentReport;
};

it.each([
['a manual', {isManualDistanceRequest: true}],
['an odometer', {isOdometerDistanceRequest: true}],
])('blocks moving %s distance expense to a report with commuter exclusions', async (_distanceType, requestTypeProps) => {
const currentReport = await setUpCommuterExclusionTest();
const selectReport = jest.fn();

renderIOURequestEditReportCommon({selectedReportID: currentReport.reportID, transactionIDs: [FAKE_TRANSACTION_ID], selectReport, ...requestTypeProps});
await waitForBatchedUpdatesWithAct();
fireEvent.press(screen.getByText('Expense Report'));

expect(mockShowConfirmModal).toHaveBeenCalledTimes(1);
expect(selectReport).not.toHaveBeenCalled();
});

it('allows moving a GPS distance expense to a report with commuter exclusions', async () => {
const currentReport = await setUpCommuterExclusionTest();
const selectReport = jest.fn();

renderIOURequestEditReportCommon({selectedReportID: currentReport.reportID, transactionIDs: [FAKE_TRANSACTION_ID], selectReport});
await waitForBatchedUpdatesWithAct();
fireEvent.press(screen.getByText('Expense Report'));

expect(mockShowConfirmModal).not.toHaveBeenCalled();
expect(selectReport).toHaveBeenCalledTimes(1);
});

it('blocks creating a report for a manual distance expense with commuter exclusions', async () => {
const currentReport = await setUpCommuterExclusionTest();
const createReport = jest.fn();

renderIOURequestEditReportCommon({
selectedReportID: currentReport.reportID,
transactionPolicyID: FAKE_POLICY_ID,
transactionIDs: [FAKE_TRANSACTION_ID],
isManualDistanceRequest: true,
createReport,
});
await waitForBatchedUpdatesWithAct();
fireEvent.press(screen.getByText('Create report'), {});

expect(createReport).not.toHaveBeenCalled();
expect(mockShowConfirmModal).toHaveBeenCalledTimes(1);
});
});

describe('NotFound', () => {
Expand Down
Loading