1- import { markRejectViolationAsResolved , rejectExpenseReport , rejectMoneyRequest } from '@libs/actions/IOU/RejectMoneyRequest' ;
1+ import { dismissRejectExpenseError , markRejectViolationAsResolved , rejectExpenseReport , rejectMoneyRequest } from '@libs/actions/IOU/RejectMoneyRequest' ;
22import initOnyxDerivedValues from '@libs/actions/OnyxDerived' ;
33import { WRITE_COMMANDS } from '@libs/API/types' ;
44import { getParsedComment } from '@libs/ReportUtils' ;
@@ -22,7 +22,7 @@ import createRandomPolicy from '../../utils/collections/policies';
2222import { createRandomReport } from '../../utils/collections/reports' ;
2323import createRandomTransaction from '../../utils/collections/transaction' ;
2424import getOnyxValue from '../../utils/getOnyxValue' ;
25- import { getCurrencyDecimalsLocal , getGlobalFetchMock , getOnyxData } from '../../utils/TestHelper' ;
25+ import { getCurrencyDecimalsLocal , getGlobalFetchMock , getOnyxData , translateLocal } from '../../utils/TestHelper' ;
2626import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates' ;
2727
2828jest . mock ( '@src/libs/Navigation/Navigation' , ( ) => ( {
@@ -334,6 +334,137 @@ describe('actions/IOU/RejectMoneyRequest', () => {
334334 expect ( firstRejectedTransaction ?. reportID ) . toBe ( sharedRejectedToReportID ) ;
335335 expect ( secondRejectedTransaction ?. reportID ) . toBe ( sharedRejectedToReportID ) ;
336336 } ) ;
337+
338+ describe ( 'when the expense has already moved off the report' , ( ) => {
339+ const MOVED_TO_REPORT_ID = '999' ;
340+ const ERROR_TIMESTAMP = '1770000000000000' ;
341+
342+ // The copy Web-Expensify returns for this case, reported under its own `reject` error field
343+ const SERVER_REJECT_ERROR = { [ ERROR_TIMESTAMP ] : 'The expense has already been moved or rejected.' } ;
344+
345+ /** Puts a second expense on the report so rejecting moves the expense off it instead of deleting the report. */
346+ async function addSecondExpenseToReport ( ) {
347+ const secondTransaction = {
348+ ...createRandomTransaction ( 2 ) ,
349+ reportID : iouReport ?. reportID ,
350+ amount,
351+ currency : CONST . CURRENCY . USD ,
352+ merchant : 'Second Test Merchant' ,
353+ transactionID : '2' ,
354+ } ;
355+ await Onyx . set ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ secondTransaction . transactionID } ` , secondTransaction ) ;
356+ await waitForBatchedUpdates ( ) ;
357+ }
358+
359+ it ( 'should not send a reject for an expense the server has already moved elsewhere' , async ( ) => {
360+ if ( ! transaction ?. transactionID || ! iouReport ?. reportID ) {
361+ throw new Error ( 'Required transaction or report data is missing' ) ;
362+ }
363+
364+ // Given: The expense now sits on a different report, so the copy on this report is stale
365+ await Onyx . merge ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` , { reportID : MOVED_TO_REPORT_ID } ) ;
366+ await waitForBatchedUpdates ( ) ;
367+
368+ // When: Rejecting it from the report it is no longer on
369+ const result = rejectMoneyRequest (
370+ transaction . transactionID ,
371+ iouReport . reportID ,
372+ comment ,
373+ policy ,
374+ TEST_USER_ACCOUNT_ID ,
375+ TEST_USER_EMAIL ,
376+ [ CONST . BETAS . ALL ] ,
377+ undefined ,
378+ getCurrencyDecimalsLocal ,
379+ ) ;
380+
381+ await waitForBatchedUpdates ( ) ;
382+
383+ // Then: There is nowhere to navigate back to, and nothing was applied optimistically —
384+ // the expense stays put on the report the server moved it to
385+ expect ( result ) . toBeUndefined ( ) ;
386+ const untouchedTransaction = await getOnyxValue ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` ) ;
387+ expect ( untouchedTransaction ?. reportID ) . toBe ( MOVED_TO_REPORT_ID ) ;
388+ } ) ;
389+
390+ it ( 'should put the expense back on the report with an error when the reject fails' , async ( ) => {
391+ if ( ! transaction ?. transactionID || ! iouReport ?. reportID ) {
392+ throw new Error ( 'Required transaction or report data is missing' ) ;
393+ }
394+ await addSecondExpenseToReport ( ) ;
395+
396+ // When: The server rejects the request
397+ mockFetch ?. fail ?.( ) ;
398+ rejectMoneyRequest (
399+ transaction . transactionID ,
400+ iouReport . reportID ,
401+ comment ,
402+ policy ,
403+ TEST_USER_ACCOUNT_ID ,
404+ TEST_USER_EMAIL ,
405+ [ CONST . BETAS . ALL ] ,
406+ undefined ,
407+ getCurrencyDecimalsLocal ,
408+ ) ;
409+ await waitForBatchedUpdates ( ) ;
410+
411+ // Then: The expense returns to the report carrying a translated rejection error
412+ const rejectedTransaction = await getOnyxValue ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` ) ;
413+ expect ( rejectedTransaction ?. reportID ) . toBe ( iouReport . reportID ) ;
414+ expect ( Object . values ( rejectedTransaction ?. errors ?? { } ) ) . toEqual ( [ translateLocal ( 'iou.rejectReport.couldNotRejectExpense' ) ] ) ;
415+ } ) ;
416+
417+ it ( 'should clear an earlier rejection error when the expense is rejected again' , async ( ) => {
418+ if ( ! transaction ?. transactionID || ! iouReport ?. reportID ) {
419+ throw new Error ( 'Required transaction or report data is missing' ) ;
420+ }
421+ await addSecondExpenseToReport ( ) ;
422+
423+ // Given: The expense still shows the error left by an earlier failed reject
424+ await Onyx . merge ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` , {
425+ errors : { [ ERROR_TIMESTAMP ] : translateLocal ( 'iou.rejectReport.couldNotRejectExpense' ) } ,
426+ errorFields : { reject : SERVER_REJECT_ERROR } ,
427+ } ) ;
428+ await waitForBatchedUpdates ( ) ;
429+
430+ // When: Rejecting it again
431+ rejectMoneyRequest (
432+ transaction . transactionID ,
433+ iouReport . reportID ,
434+ comment ,
435+ policy ,
436+ TEST_USER_ACCOUNT_ID ,
437+ TEST_USER_EMAIL ,
438+ [ CONST . BETAS . ALL ] ,
439+ undefined ,
440+ getCurrencyDecimalsLocal ,
441+ ) ;
442+ await waitForBatchedUpdates ( ) ;
443+
444+ // Then: The stale error is gone, so the retry does not show the previous failure
445+ const rejectedTransaction = await getOnyxValue ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` ) ;
446+ expect ( rejectedTransaction ?. errors ) . toBeFalsy ( ) ;
447+ expect ( rejectedTransaction ?. errorFields ?. reject ) . toBeFalsy ( ) ;
448+ } ) ;
449+
450+ it ( 'should drop the stale local copy of the expense when its reject error is dismissed' , async ( ) => {
451+ if ( ! transaction ?. transactionID ) {
452+ throw new Error ( 'Required transaction data is missing' ) ;
453+ }
454+
455+ // Given: An expense the server reported as already moved
456+ await Onyx . merge ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` , { errorFields : { reject : SERVER_REJECT_ERROR } } ) ;
457+ await waitForBatchedUpdates ( ) ;
458+
459+ // When: The user dismisses the error
460+ dismissRejectExpenseError ( transaction . transactionID ) ;
461+ await waitForBatchedUpdates ( ) ;
462+
463+ // Then: The expense is gone locally, so it stops showing on a report it is no longer on
464+ const dismissedTransaction = await getOnyxValue ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transaction . transactionID } ` ) ;
465+ expect ( dismissedTransaction ) . toBeFalsy ( ) ;
466+ } ) ;
467+ } ) ;
337468 } ) ;
338469
339470 describe ( 'rejectExpenseReport' , ( ) => {
0 commit comments