Skip to content

Commit d612af2

Browse files
authored
Merge pull request #98281 from fedirjh/fix/block-invalid-distance-report-move
[Commuter Mileage Exclusion] Block invalid distance report moves
2 parents 754b6e6 + 71e7957 commit d612af2

5 files changed

Lines changed: 132 additions & 7 deletions

File tree

‎src/pages/Search/SearchTransactionsChangeReport.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav
2020
import Navigation from '@libs/Navigation/Navigation';
2121
import {generateReportID, getPersonalDetailsForAccountID, getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
2222
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
23-
import {isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';
23+
import {isManualDistanceRequest as isManualDistanceRequestUtil, isOdometerDistanceRequest as isOdometerDistanceRequestUtil, isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';
2424

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

@@ -309,6 +309,8 @@ function SearchTransactionsChangeReport() {
309309
<IOURequestEditReportCommon
310310
backTo={undefined}
311311
transactionIDs={selectedTransactionsKeys}
312+
isManualDistanceRequest={transactions.some(isManualDistanceRequestUtil)}
313+
isOdometerDistanceRequest={transactions.some(isOdometerDistanceRequestUtil)}
312314
selectedReportID={selectedReportID}
313315
selectReport={selectReport}
314316
removeFromReport={removeFromReport}

‎src/pages/iou/request/step/DynamicIOURequestEditReport.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav
2323
import Navigation from '@libs/Navigation/Navigation';
2424
import {getPersonalDetailsForAccountID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
2525
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
26-
import {isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';
26+
import {isManualDistanceRequest as isManualDistanceRequestUtil, isOdometerDistanceRequest as isOdometerDistanceRequestUtil, isUnreportedManagedCardTransaction} from '@libs/TransactionUtils';
2727

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

@@ -223,6 +223,8 @@ function DynamicIOURequestEditReport({route}: DynamicIOURequestEditReportProps)
223223
backTo={backPath}
224224
selectedReportID={reportID}
225225
transactionIDs={transactionIDs}
226+
isManualDistanceRequest={transactions.some(isManualDistanceRequestUtil)}
227+
isOdometerDistanceRequest={transactions.some(isOdometerDistanceRequestUtil)}
226228
selectReport={selectReport}
227229
removeFromReport={removeFromReport}
228230
isEditing={action === CONST.IOU.ACTION.EDIT}

‎src/pages/iou/request/step/DynamicIOURequestStepReport.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea
1919
import Navigation from '@libs/Navigation/Navigation';
2020
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
2121
import {getPersonalDetailsForAccountID, getReportOrDraftReport, isPolicyExpenseChat, isReportOutstanding} from '@libs/ReportUtils';
22-
import {isPerDiemRequest, isTimeRequest as isTimeRequestUtil, isUnreportedManagedCardTransaction as isUnreportedManagedCardTransactionUtil} from '@libs/TransactionUtils';
22+
import {
23+
isManualDistanceRequest as isManualDistanceRequestUtil,
24+
isOdometerDistanceRequest as isOdometerDistanceRequestUtil,
25+
isPerDiemRequest,
26+
isTimeRequest as isTimeRequestUtil,
27+
isUnreportedManagedCardTransaction as isUnreportedManagedCardTransactionUtil,
28+
} from '@libs/TransactionUtils';
2329

2430
import CONST from '@src/CONST';
2531
import ONYXKEYS from '@src/ONYXKEYS';
@@ -222,6 +228,8 @@ function DynamicIOURequestStepReport({route, transaction}: DynamicIOURequestStep
222228
backTo={backPath}
223229
selectReport={selectReport}
224230
transactionIDs={transaction ? [transaction.transactionID] : []}
231+
isManualDistanceRequest={transactions.some(isManualDistanceRequestUtil)}
232+
isOdometerDistanceRequest={transactions.some(isOdometerDistanceRequestUtil)}
225233
selectedReportID={selectedReportID}
226234
selectedPolicyID={selectedPolicyID}
227235
transactionPolicyID={targetExpensePolicyID}

‎src/pages/iou/request/step/IOURequestEditReportCommon.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {ListItem} from '@components/SelectionList/types';
77
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
88

99
import useAutoFocusInput from '@hooks/useAutoFocusInput';
10+
import useCommuterExclusionGuard from '@hooks/useCommuterExclusionGuard';
1011
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
1112
import useDebouncedState from '@hooks/useDebouncedState';
1213
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
@@ -44,6 +45,8 @@ type TransactionGroupListItem = ListItem & {
4445
type Props = {
4546
backTo: Route | undefined;
4647
transactionIDs?: string[];
48+
isManualDistanceRequest: boolean;
49+
isOdometerDistanceRequest: boolean;
4750
selectedReportID?: string;
4851
selectedPolicyID?: string;
4952
transactionPolicyID?: string;
@@ -62,6 +65,8 @@ type Props = {
6265
function IOURequestEditReportCommon({
6366
backTo,
6467
transactionIDs,
68+
isManualDistanceRequest,
69+
isOdometerDistanceRequest,
6570
selectReport,
6671
selectedReportID,
6772
selectedPolicyID,
@@ -105,6 +110,10 @@ function IOURequestEditReportCommon({
105110
const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, transactionPolicyID, isUnreportedManagedCardTransaction);
106111

107112
const [perDiemWarningModalVisible, setPerDiemWarningModalVisible] = useState(false);
113+
const blockManualOrOdometerDistanceRequestIfNeeded = useCommuterExclusionGuard({
114+
isManualDistanceRequest,
115+
isOdometerDistanceRequest,
116+
});
108117

109118
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
110119
const isSelectedReportUnreported = useMemo(() => !!(isUnreported ?? selectedReportID === CONST.REPORT.UNREPORTED_REPORT_ID), [isUnreported, selectedReportID]);
@@ -247,6 +256,9 @@ function IOURequestEditReportCommon({
247256
navigateBack();
248257
return;
249258
}
259+
if (blockManualOrOdometerDistanceRequestIfNeeded(item.policyID)) {
260+
return;
261+
}
250262
const itemPolicy = item.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] : undefined;
251263
if (
252264
item?.policyID &&
@@ -265,11 +277,14 @@ function IOURequestEditReportCommon({
265277
};
266278

267279
const handleCreateReport = useCallback(() => {
280+
if (blockManualOrOdometerDistanceRequestIfNeeded(policyForMovingExpenses?.id)) {
281+
return;
282+
}
268283
if (!validatePerDiemMove(policyForMovingExpenses?.id)) {
269284
return;
270285
}
271286
createReport?.();
272-
}, [validatePerDiemMove, policyForMovingExpenses?.id, createReport]);
287+
}, [blockManualOrOdometerDistanceRequestIfNeeded, validatePerDiemMove, policyForMovingExpenses?.id, createReport]);
273288

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

‎tests/ui/IOURequestEditReportCommonTest.tsx‎

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {act, render, screen} from '@testing-library/react-native';
1+
import {act, fireEvent, render, screen} from '@testing-library/react-native';
22

33
import ComposeProviders from '@components/ComposeProviders';
44
import {LocaleContextProvider} from '@components/LocaleContextProvider';
@@ -24,19 +24,47 @@ const FAKE_TRANSACTION_ID = '2';
2424
const FAKE_EMAIL = 'fake@gmail.com';
2525
const FAKE_ACCOUNT_ID = 1;
2626
const FAKE_SECOND_ACCOUNT_ID = 2;
27+
const mockShowConfirmModal = jest.fn();
28+
29+
jest.mock('@hooks/useConfirmModal', () => () => ({
30+
showConfirmModal: mockShowConfirmModal,
31+
}));
2732

2833
/**
2934
* Helper function to render the IOURequestEditReportCommon component with required providers.
3035
* This encapsulates the component setup and makes tests more readable.
3136
*/
32-
const renderIOURequestEditReportCommon = ({selectedReportID = '', selectedPolicyID}: {selectedReportID: string; selectedPolicyID?: string}) =>
37+
const renderIOURequestEditReportCommon = ({
38+
selectedReportID = '',
39+
selectedPolicyID,
40+
transactionPolicyID,
41+
transactionIDs,
42+
isManualDistanceRequest = false,
43+
isOdometerDistanceRequest = false,
44+
selectReport = jest.fn(),
45+
createReport,
46+
}: {
47+
selectedReportID: string;
48+
selectedPolicyID?: string;
49+
transactionPolicyID?: string;
50+
transactionIDs?: string[];
51+
isManualDistanceRequest?: boolean;
52+
isOdometerDistanceRequest?: boolean;
53+
selectReport?: jest.Mock;
54+
createReport?: jest.Mock;
55+
}) =>
3356
render(
3457
<NavigationContainer>
3558
<ComposeProviders components={[OnyxListItemProvider, LocaleContextProvider]}>
3659
<IOURequestEditReportCommon
3760
selectedReportID={selectedReportID}
3861
selectedPolicyID={selectedPolicyID}
39-
selectReport={jest.fn()}
62+
transactionPolicyID={transactionPolicyID}
63+
transactionIDs={transactionIDs}
64+
isManualDistanceRequest={isManualDistanceRequest}
65+
isOdometerDistanceRequest={isOdometerDistanceRequest}
66+
selectReport={selectReport}
67+
createReport={createReport}
4068
backTo=""
4169
isPerDiemRequest={false}
4270
/>
@@ -110,6 +138,76 @@ describe('IOURequestEditReportCommon', () => {
110138
const dotIndicators = screen.queryAllByTestId(CONST.DOT_INDICATOR_TEST_ID);
111139
expect(dotIndicators).toHaveLength(0);
112140
});
141+
142+
const setUpCommuterExclusionTest = async () => {
143+
const currentReport: Report = {
144+
reportID: 'currentReport',
145+
reportName: 'Current Report',
146+
ownerAccountID: FAKE_ACCOUNT_ID,
147+
policyID: 'currentPolicy',
148+
};
149+
await act(async () => {
150+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${currentReport.reportID}`, currentReport);
151+
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}`, {
152+
...createRandomPolicy(Number(FAKE_POLICY_ID), CONST.POLICY.TYPE.TEAM),
153+
role: CONST.POLICY.ROLE.ADMIN,
154+
pendingAction: undefined,
155+
commuterExclusions: {
156+
method: CONST.POLICY.COMMUTER_EXCLUSION_METHOD.FIXED_DISTANCE,
157+
fixedDistance: 1,
158+
fixedDistanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
159+
},
160+
});
161+
});
162+
await waitForBatchedUpdatesWithAct();
163+
164+
return currentReport;
165+
};
166+
167+
it.each([
168+
['a manual', {isManualDistanceRequest: true}],
169+
['an odometer', {isOdometerDistanceRequest: true}],
170+
])('blocks moving %s distance expense to a report with commuter exclusions', async (_distanceType, requestTypeProps) => {
171+
const currentReport = await setUpCommuterExclusionTest();
172+
const selectReport = jest.fn();
173+
174+
renderIOURequestEditReportCommon({selectedReportID: currentReport.reportID, transactionIDs: [FAKE_TRANSACTION_ID], selectReport, ...requestTypeProps});
175+
await waitForBatchedUpdatesWithAct();
176+
fireEvent.press(screen.getByText('Expense Report'));
177+
178+
expect(mockShowConfirmModal).toHaveBeenCalledTimes(1);
179+
expect(selectReport).not.toHaveBeenCalled();
180+
});
181+
182+
it('allows moving a GPS distance expense to a report with commuter exclusions', async () => {
183+
const currentReport = await setUpCommuterExclusionTest();
184+
const selectReport = jest.fn();
185+
186+
renderIOURequestEditReportCommon({selectedReportID: currentReport.reportID, transactionIDs: [FAKE_TRANSACTION_ID], selectReport});
187+
await waitForBatchedUpdatesWithAct();
188+
fireEvent.press(screen.getByText('Expense Report'));
189+
190+
expect(mockShowConfirmModal).not.toHaveBeenCalled();
191+
expect(selectReport).toHaveBeenCalledTimes(1);
192+
});
193+
194+
it('blocks creating a report for a manual distance expense with commuter exclusions', async () => {
195+
const currentReport = await setUpCommuterExclusionTest();
196+
const createReport = jest.fn();
197+
198+
renderIOURequestEditReportCommon({
199+
selectedReportID: currentReport.reportID,
200+
transactionPolicyID: FAKE_POLICY_ID,
201+
transactionIDs: [FAKE_TRANSACTION_ID],
202+
isManualDistanceRequest: true,
203+
createReport,
204+
});
205+
await waitForBatchedUpdatesWithAct();
206+
fireEvent.press(screen.getByText('Create report'), {});
207+
208+
expect(createReport).not.toHaveBeenCalled();
209+
expect(mockShowConfirmModal).toHaveBeenCalledTimes(1);
210+
});
113211
});
114212

115213
describe('NotFound', () => {

0 commit comments

Comments
 (0)