@@ -9,6 +9,8 @@ import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithA
99
1010const mockRequestMoneyAction = jest . fn ( ) ;
1111const mockTrackExpenseAction = jest . fn ( ) ;
12+ const mockSubmitPerDiemExpenseAction = jest . fn ( ) ;
13+ const mockSubmitPerDiemExpenseForSelfDMAction = jest . fn ( ) ;
1214const mockCleanupAfterExpenseCreate = jest . fn ( ) ;
1315const mockCleanupAndNavigateAfterExpenseCreate = jest . fn ( ) ;
1416const 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+
2128jest . 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+
109148function 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
298411describe ( '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