From f2f746cfbae59a893889661a0911dd3f711dc586 Mon Sep 17 00:00:00 2001 From: "Youssef Lourayad (via MelvinBot)" Date: Wed, 8 Jul 2026 17:03:14 +0000 Subject: [PATCH 1/2] Clear expense report hasOutstandingChildRequest optimistically on approve Co-authored-by: Youssef Lourayad --- src/libs/actions/IOU/ReportWorkflow.ts | 5 ++ tests/actions/IOUTest/ReportWorkflowTest.ts | 55 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 0040b3045d56..de020b6dbb8c 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -541,6 +541,10 @@ function approveMoneyRequest(params: ApproveMoneyRequestFunctionParams) { statusNum: predictedNextStatus, managerID, nextStep: optimisticNextStep ?? undefined, + // The current user just approved this report, so it no longer awaits their action. + // Clear the expense report's own outstanding-child flag so the LHN green dot disappears + // optimistically instead of lingering until OpenReport refetches the server's false value. + hasOutstandingChildRequest: false, pendingFields: { partial: full ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -659,6 +663,7 @@ function approveMoneyRequest(params: ApproveMoneyRequestFunctionParams) { statusNum: expenseReport.statusNum, stateNum: expenseReport.stateNum, nextStep: expenseReport.nextStep ?? null, + hasOutstandingChildRequest: expenseReport.hasOutstandingChildRequest, pendingFields: { partial: null, nextStep: null, diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index c25b61ac4dbc..45dc4119aae0 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -36,7 +36,7 @@ import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportNameValuePairs, ReportNextStepDeprecated} from '@src/types/onyx'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {ReportActions} from '@src/types/onyx/ReportAction'; -import type {OnyxData} from '@src/types/onyx/Request'; +import type {AnyOnyxData, OnyxData} from '@src/types/onyx/Request'; import type Transaction from '@src/types/onyx/Transaction'; import type {OnyxEntry, OnyxMultiSetInput} from 'react-native-onyx'; @@ -2294,6 +2294,59 @@ describe('actions/IOU/ReportWorkflow', () => { }); }); + describe('approveMoneyRequest outstanding child request', () => { + beforeEach(() => { + jest.clearAllMocks(); + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify optimistic/failure data. + jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + }); + + it('clears hasOutstandingChildRequest optimistically and restores it if the approve request fails', () => { + const expenseReport: Report = { + ...createRandomReport(1, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + total: 10000, + currency: CONST.CURRENCY.USD, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + // The report shows the LHN green dot before approval. + hasOutstandingChildRequest: true, + }; + const expenseReportPolicy: Policy = { + ...createRandomPolicy(Number(expenseReport.policyID), CONST.POLICY.TYPE.TEAM), + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}` as const; + + approveMoneyRequest({ + expenseReport, + expenseReportPolicy, + currentUserAccountIDParam: CARLOS_ACCOUNT_ID, + currentUserEmailParam: CARLOS_EMAIL, + hasViolations: false, + isASAPSubmitBetaEnabled: false, + expenseReportCurrentNextStepDeprecated: undefined, + betas: [CONST.BETAS.ALL], + userBillingGracePeriodEnds: undefined, + amountOwed: 0, + ownerBillingGracePeriodEnd: undefined, + delegateEmail: undefined, + ownerLogin: undefined, + }); + + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting mock call args to verify optimistic/failure data structure + const onyxData: AnyOnyxData | undefined = jest.mocked(API.write).mock.calls.at(0)?.[2]; + + // The green dot clears optimistically because the expense report's own flag is set to false. + const optimisticReport: OnyxEntry = onyxData?.optimisticData?.find((update) => update.key === reportKey)?.value; + expect(optimisticReport?.hasOutstandingChildRequest).toBe(false); + + // If the approve request fails, the flag is restored so the green dot reappears. + const failureReport: OnyxEntry = onyxData?.failureData?.find((update) => update.key === reportKey)?.value; + expect(failureReport?.hasOutstandingChildRequest).toBe(true); + }); + }); + describe('canIOUBePaid', () => { it('For invoices from archived workspaces', async () => { const {policy, convertedInvoiceChat: chatReport}: InvoiceTestData = InvoiceData; From 57f3a884ce5a8ace48cbea7fa5fbf9700d67efc3 Mon Sep 17 00:00:00 2001 From: "Youssef Lourayad (via MelvinBot)" Date: Wed, 8 Jul 2026 17:22:37 +0000 Subject: [PATCH 2/2] Fix spellcheck: reword comment to avoid unknown word 'refetches' Co-authored-by: Youssef Lourayad --- src/libs/actions/IOU/ReportWorkflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index de020b6dbb8c..d2645e32775b 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -543,7 +543,7 @@ function approveMoneyRequest(params: ApproveMoneyRequestFunctionParams) { nextStep: optimisticNextStep ?? undefined, // The current user just approved this report, so it no longer awaits their action. // Clear the expense report's own outstanding-child flag so the LHN green dot disappears - // optimistically instead of lingering until OpenReport refetches the server's false value. + // optimistically instead of lingering until OpenReport re-fetches the server's false value. hasOutstandingChildRequest: false, pendingFields: { partial: full ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,