Skip to content

Commit 3c96997

Browse files
authored
Merge pull request Expensify#92889 from nabi-ebrahimi/fix/per-diem-expense-self-dm-after-submit-87666
fix: remove per diem expense from self DM after submission to workspace chat
2 parents 3d76089 + 78e80c5 commit 3c96997

2 files changed

Lines changed: 119 additions & 6 deletions

File tree

src/pages/iou/request/step/confirmation/useExpenseSubmission.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ function useExpenseSubmission(params: UseExpenseSubmissionParams) {
948948
return;
949949
}
950950

951-
if (isPerDiemRequest) {
951+
if (isPerDiemRequest && action !== CONST.IOU.ACTION.SUBMIT) {
952952
submitPerDiemExpense(trimmedComment, shouldHandleNavigation, policyRecentlyUsedCategories);
953953
markSubmitExpenseEnd();
954954
return;

tests/unit/hooks/useExpenseSubmission.test.ts

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithA
99

1010
const mockRequestMoneyAction = jest.fn();
1111
const mockTrackExpenseAction = jest.fn();
12+
const mockSubmitPerDiemExpenseAction = jest.fn();
13+
const mockSubmitPerDiemExpenseForSelfDMAction = jest.fn();
1214
const mockCleanupAfterExpenseCreate = jest.fn();
1315
const mockCleanupAndNavigateAfterExpenseCreate = jest.fn();
1416
const mockResolveChatTargetForSubmitCleanup = jest.fn();
@@ -18,6 +20,11 @@ jest.mock('@userActions/IOU/TrackExpense', () => ({
1820
trackExpense: (...args: unknown[]) => mockTrackExpenseAction(...args),
1921
}));
2022

23+
jest.mock('@userActions/IOU/PerDiem', () => ({
24+
submitPerDiemExpense: (...args: unknown[]) => mockSubmitPerDiemExpenseAction(...args),
25+
submitPerDiemExpenseForSelfDM: (...args: unknown[]) => mockSubmitPerDiemExpenseForSelfDMAction(...args),
26+
}));
27+
2128
jest.mock('@libs/Navigation/helpers/cleanupAfterExpenseCreate', () => ({
2229
__esModule: true,
2330
default: (...args: unknown[]) => mockCleanupAfterExpenseCreate(...args),
@@ -106,6 +113,38 @@ function buildTransaction(overrides: Partial<Transaction> = {}): Transaction {
106113
} as Transaction;
107114
}
108115

116+
function buildReportAction(overrides: Partial<ReportAction> = {}): ReportAction {
117+
return {
118+
reportActionID: 'report-action-1',
119+
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
120+
created: '2026-04-24',
121+
...overrides,
122+
};
123+
}
124+
125+
function buildPerDiemTransaction(overrides: Partial<Transaction> = {}): Transaction {
126+
return buildTransaction({
127+
amount: 200,
128+
merchant: 'Per diem',
129+
comment: {
130+
comment: 'Trip per diem',
131+
customUnit: {
132+
customUnitID: 'per-diem-custom-unit',
133+
customUnitRateID: 'per-diem-rate',
134+
name: CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL,
135+
subRates: [{id: 'sub-rate-1', name: 'Meals', quantity: 1, rate: 200}],
136+
attributes: {
137+
dates: {
138+
start: '2026-04-24',
139+
end: '2026-04-24',
140+
},
141+
},
142+
},
143+
},
144+
...overrides,
145+
});
146+
}
147+
109148
function buildParams(overrides: Partial<Parameters<typeof useExpenseSubmission>[0]> = {}): Parameters<typeof useExpenseSubmission>[0] {
110149
const transaction = buildTransaction();
111150
return {
@@ -190,16 +229,14 @@ describe('useExpenseSubmission orchestrator-suppressed cleanup', () => {
190229
// Move-from-track SUBMIT: the action writes the transaction under the EXISTING tracked transaction id,
191230
// so cleanup must reference that same id — not a fresh rand64() optimistic one.
192231
const EXISTING_TRACKED_TRANSACTION_ID = 'tracked-transaction-99';
193-
const linkedTrackedExpenseReportAction = {
232+
const linkedTrackedExpenseReportAction = buildReportAction({
194233
reportActionID: 'linked-action-1',
195-
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
196-
created: '2026-04-24',
197234
originalMessage: {
198235
IOUTransactionID: EXISTING_TRACKED_TRANSACTION_ID,
199236
IOUReportID: 'tracked-report-1',
200237
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
201238
},
202-
} as unknown as ReportAction;
239+
});
203240
const movedTransaction = buildTransaction({
204241
linkedTrackedExpenseReportAction,
205242
linkedTrackedExpenseReportID: 'tracked-report-1',
@@ -244,6 +281,55 @@ describe('useExpenseSubmission orchestrator-suppressed cleanup', () => {
244281
expect(mockResolveChatTargetForSubmitCleanup).not.toHaveBeenCalled();
245282
expect(mockCleanupAndNavigateAfterExpenseCreate).toHaveBeenCalledWith(expect.objectContaining({optimisticChatReportID: 'iou-chat-77'}));
246283
});
284+
285+
it('routes tracked per diem SUBMIT through requestMoney so the original tracked expense is moved', async () => {
286+
const existingTrackedTransactionID = 'tracked-per-diem-transaction-1';
287+
const linkedTrackedExpenseReportAction = buildReportAction({
288+
reportActionID: 'tracked-per-diem-action-1',
289+
childReportID: 'tracked-per-diem-thread-1',
290+
originalMessage: {
291+
IOUTransactionID: existingTrackedTransactionID,
292+
IOUReportID: 'tracked-per-diem-report-1',
293+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
294+
},
295+
});
296+
const perDiemTransaction = buildPerDiemTransaction({
297+
linkedTrackedExpenseReportAction,
298+
linkedTrackedExpenseReportID: 'tracked-per-diem-report-1',
299+
});
300+
301+
const {result} = renderHook(() =>
302+
useExpenseSubmission(
303+
buildParams({
304+
action: CONST.IOU.ACTION.SUBMIT,
305+
requestType: CONST.IOU.REQUEST_TYPE.PER_DIEM,
306+
isPerDiemRequest: true,
307+
transaction: perDiemTransaction,
308+
transactions: [perDiemTransaction],
309+
}),
310+
),
311+
);
312+
await waitForBatchedUpdatesWithAct();
313+
314+
await act(async () => {
315+
result.current.createTransaction(false, true);
316+
});
317+
await waitForBatchedUpdatesWithAct();
318+
319+
expect(mockRequestMoneyAction).toHaveBeenCalledTimes(1);
320+
expect(mockRequestMoneyAction).toHaveBeenCalledWith(
321+
expect.objectContaining({
322+
action: CONST.IOU.ACTION.SUBMIT,
323+
existingTransaction: perDiemTransaction,
324+
transactionParams: expect.objectContaining({
325+
linkedTrackedExpenseReportAction,
326+
linkedTrackedExpenseReportID: 'tracked-per-diem-report-1',
327+
}),
328+
}),
329+
);
330+
expect(mockSubmitPerDiemExpenseAction).not.toHaveBeenCalled();
331+
expect(mockSubmitPerDiemExpenseForSelfDMAction).not.toHaveBeenCalled();
332+
});
247333
});
248334

249335
describe('trackExpense path', () => {
@@ -293,6 +379,33 @@ describe('useExpenseSubmission orchestrator-suppressed cleanup', () => {
293379
expect(mockTrackExpenseAction).toHaveBeenCalledWith(expect.objectContaining({existingTransaction: params.transactions.at(0)}));
294380
});
295381
});
382+
383+
describe('per diem path', () => {
384+
it('keeps initial self-DM per diem tracking on submitPerDiemExpenseForSelfDM', async () => {
385+
const perDiemTransaction = buildPerDiemTransaction();
386+
387+
const {result} = renderHook(() =>
388+
useExpenseSubmission(
389+
buildParams({
390+
iouType: CONST.IOU.TYPE.TRACK,
391+
requestType: CONST.IOU.REQUEST_TYPE.PER_DIEM,
392+
isPerDiemRequest: true,
393+
transaction: perDiemTransaction,
394+
transactions: [perDiemTransaction],
395+
}),
396+
),
397+
);
398+
await waitForBatchedUpdatesWithAct();
399+
400+
await act(async () => {
401+
result.current.createTransaction(false, true);
402+
});
403+
await waitForBatchedUpdatesWithAct();
404+
405+
expect(mockSubmitPerDiemExpenseForSelfDMAction).toHaveBeenCalledTimes(1);
406+
expect(mockRequestMoneyAction).not.toHaveBeenCalled();
407+
});
408+
});
296409
});
297410

298411
describe('useExpenseSubmission action-bailout safety', () => {
@@ -323,7 +436,7 @@ describe('useExpenseSubmission action-bailout safety', () => {
323436

324437
it('skips cleanup/nav when a multi-transaction SUBMIT batch has any iteration that bails (defense-in-depth — preserves the failed item draft)', async () => {
325438
// Cast keeps the fixture minimal — pre-validation only needs truthy presence.
326-
const linkedTracked = {linkedTrackedExpenseReportAction: {reportActionID: 'a-1'} as unknown as ReportAction, linkedTrackedExpenseReportID: 'r-1'};
439+
const linkedTracked = {linkedTrackedExpenseReportAction: buildReportAction({reportActionID: 'a-1'}), linkedTrackedExpenseReportID: 'r-1'};
327440
const transaction1 = buildTransaction({transactionID: 't-1', ...linkedTracked});
328441
const transaction2 = buildTransaction({transactionID: 't-2', ...linkedTracked});
329442
mockRequestMoneyAction.mockReturnValueOnce({iouReport: {reportID: 'iou-1'}}).mockReturnValueOnce({});

0 commit comments

Comments
 (0)