From df3371f6e86dfeec77d576b0b5eaa73f99aabf5c Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Thu, 16 Oct 2025 18:15:41 +0530 Subject: [PATCH 1/5] Revert changes --- src/libs/ReportUtils.ts | 45 +---------------------------------------- src/libs/actions/IOU.ts | 27 ++++++++++--------------- 2 files changed, 12 insertions(+), 60 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0e2769940ce6..6d4604a318f0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4328,12 +4328,6 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(report?.policyID); - // If the current user took control, then they are the final approver and we don't have a next approver - const bypassApprover = getBypassApproverIfTakenControl(report); - if (bypassApprover === currentUserAccountID) { - return undefined; - } - const approvalChain = getApprovalChain(policy, report); const submitToAccountID = getSubmitToAccountID(policy, report); @@ -4351,8 +4345,7 @@ function getNextApproverAccountID(report: OnyxEntry, isUnapproved = fals const nextApproverEmail = approvalChain.length === 1 ? approvalChain.at(0) : approvalChain.at(approvalChain.indexOf(currentUserEmail ?? '') + 1); if (!nextApproverEmail) { - // If there's no next approver in the chain, return undefined to indicate the current user is the final approver - return undefined; + return submitToAccountID; } return getAccountIDsByLogins([nextApproverEmail]).at(0); @@ -11482,42 +11475,6 @@ function isWorkspaceEligibleForReportChange(submitterEmail: string | undefined, return isPaidGroupPolicyPolicyUtils(newPolicy) && !!newPolicy.role; } -/** - * Checks if someone took control of the report and if that take control is still valid - * A take control is invalidated if there's a SUBMITTED action after it - */ -function getBypassApproverIfTakenControl(expenseReport: OnyxEntry): number | null { - if (!expenseReport?.reportID) { - return null; - } - - if (!isProcessingReport(expenseReport)) { - return null; - } - - const reportActions = getAllReportActions(expenseReport.reportID); - if (!reportActions) { - return null; - } - - // Sort actions by created timestamp to get chronological order - const sortedActions = getSortedReportActions(Object.values(reportActions ?? {}), true); - - // Look through actions in reverse chronological order (newest first) - // If we find a SUBMITTED action, there's no valid take control since any take control would be older - for (const action of sortedActions) { - if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED)) { - return null; - } - - if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { - return action.actorAccountID ?? null; - } - } - - return null; -} - function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; const fullApprovalChain: string[] = []; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8a43d242b84b..44a5c4f03309 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -10110,6 +10110,13 @@ function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry, full?: boolean) { if (!expenseReport) { return; @@ -10131,23 +10138,11 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated - const policy = getPolicy(expenseReport.policyID); - const nextApproverAccountID = getNextApproverAccountID(expenseReport); - const predictedNextStatus = !nextApproverAccountID ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; - const predictedNextState = !nextApproverAccountID ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; - const managerID = !nextApproverAccountID ? expenseReport.managerID : nextApproverAccountID; + const approvalChain = getApprovalChain(getPolicy(expenseReport.policyID), expenseReport); - const hasViolations = hasViolationsReportUtils(expenseReport.reportID, allTransactionViolations); - const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); - const optimisticNextStep = buildNextStepNew({ - report: expenseReport, - policy, - currentUserAccountIDParam: userAccountID, - currentUserEmailParam: currentUserEmail, - hasViolations, - isASAPSubmitBetaEnabled, - predictedNextStatus, - }); + const predictedNextStatus = isLastApprover(approvalChain) ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; + const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; + const managerID = isLastApprover(approvalChain) ? expenseReport.managerID : getNextApproverAccountID(expenseReport); const chatReport = getReportOrDraftReport(expenseReport.chatReportID); const optimisticReportActionsData: OnyxUpdate = { From a2948fad44a3886d30aa17a98e8139a946714831 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Thu, 16 Oct 2025 18:19:10 +0530 Subject: [PATCH 2/5] Revert tests --- tests/actions/IOUTest.ts | 288 --------------------------------------- 1 file changed, 288 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index d955f663ac33..857b8f6e0897 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -8736,292 +8736,4 @@ describe('actions/IOU', () => { ); }); }); - - describe('approveMoneyRequest with take control', () => { - const adminAccountID = 1; - const managerAccountID = 2; - const employeeAccountID = 3; - const adminEmail = 'admin@test.com'; - const managerEmail = 'manager@test.com'; - const employeeEmail = 'employee@test.com'; - - let expenseReport: Report; - let policy: Policy; - - beforeEach(async () => { - await Onyx.clear(); - - // Set up personal details - await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [adminAccountID]: { - accountID: adminAccountID, - login: adminEmail, - displayName: 'Admin User', - }, - [managerAccountID]: { - accountID: managerAccountID, - login: managerEmail, - displayName: 'Manager User', - }, - [employeeAccountID]: { - accountID: employeeAccountID, - login: employeeEmail, - displayName: 'Employee User', - }, - }); - - // Set up session as admin (who will approve) - await Onyx.set(ONYXKEYS.SESSION, { - email: adminEmail, - accountID: adminAccountID, - }); - - // Create policy with approval hierarchy - policy = { - id: '1', - name: 'Test Policy', - role: CONST.POLICY.ROLE.ADMIN, - owner: adminEmail, - outputCurrency: CONST.CURRENCY.USD, - isPolicyExpenseChatEnabled: true, - type: CONST.POLICY.TYPE.CORPORATE, - approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, - employeeList: { - [employeeEmail]: { - email: employeeEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: managerEmail, - }, - [managerEmail]: { - email: managerEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: adminEmail, - forwardsTo: adminEmail, - }, - [adminEmail]: { - email: adminEmail, - role: CONST.POLICY.ROLE.ADMIN, - submitsTo: '', - forwardsTo: '', - }, - }, - }; - - // Create expense report - expenseReport = { - reportID: '123', - type: CONST.REPORT.TYPE.EXPENSE, - ownerAccountID: employeeAccountID, - managerID: managerAccountID, - policyID: policy.id, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, - total: 1000, - currency: 'USD', - }; - - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); - }); - - afterEach(async () => { - await Onyx.clear(); - }); - - it('should set report to approved when admin takes control and approves', async () => { - // Admin takes control - const takeControlAction = { - reportActionID: 'takeControl1', - actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, - actorAccountID: adminAccountID, - created: '2023-01-01T10:00:00.000Z', - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { - [takeControlAction.reportActionID]: takeControlAction, - }); - - // Admin approves the report - approveMoneyRequest(expenseReport); - await waitForBatchedUpdates(); - - // Should be approved since admin took control and is the last approver - const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); - expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); - expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); - }); - - it('should invalidate take control when report is resubmitted after take control', async () => { - // Admin takes control first - const takeControlAction = { - reportActionID: 'takeControl3', - actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, - actorAccountID: adminAccountID, - created: '2023-01-01T10:00:00.000Z', - }; - - // Employee resubmits after take control (invalidates it) - const submittedAction = { - reportActionID: 'submitted1', - actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, - actorAccountID: employeeAccountID, - created: '2023-01-01T11:00:00.000Z', // After take control - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { - [takeControlAction.reportActionID]: takeControlAction, - [submittedAction.reportActionID]: submittedAction, - }); - - // Set session as manager (normal approver) - await Onyx.set(ONYXKEYS.SESSION, { - email: managerEmail, - accountID: managerAccountID, - }); - - // Manager approves the report - approveMoneyRequest(expenseReport); - await waitForBatchedUpdates(); - - // Should be submitted to admin (normal flow) since take control was invalidated - const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); - expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); - expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); - }); - }); - - describe('approveMoneyRequest with normal approval chain', () => { - const adminAccountID = 1; - const managerAccountID = 2; - const employeeAccountID = 3; - const adminEmail = 'admin@test.com'; - const managerEmail = 'manager@test.com'; - const employeeEmail = 'employee@test.com'; - - let expenseReport: Report; - let policy: Policy; - - beforeEach(async () => { - await Onyx.clear(); - - // Set up personal details - await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [adminAccountID]: { - accountID: adminAccountID, - login: adminEmail, - displayName: 'Admin User', - }, - [managerAccountID]: { - accountID: managerAccountID, - login: managerEmail, - displayName: 'Manager User', - }, - [employeeAccountID]: { - accountID: employeeAccountID, - login: employeeEmail, - displayName: 'Employee User', - }, - }); - - // Create policy with approval hierarchy - policy = { - id: '1', - name: 'Test Policy', - role: CONST.POLICY.ROLE.ADMIN, - owner: adminEmail, - outputCurrency: CONST.CURRENCY.USD, - isPolicyExpenseChatEnabled: true, - type: CONST.POLICY.TYPE.CORPORATE, - approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, - employeeList: { - [employeeEmail]: { - email: employeeEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: managerEmail, - }, - [managerEmail]: { - email: managerEmail, - role: CONST.POLICY.ROLE.USER, - submitsTo: adminEmail, - forwardsTo: adminEmail, - }, - [adminEmail]: { - email: adminEmail, - role: CONST.POLICY.ROLE.ADMIN, - submitsTo: '', - forwardsTo: '', - }, - }, - }; - - // Create expense report - expenseReport = { - reportID: '123', - type: CONST.REPORT.TYPE.EXPENSE, - ownerAccountID: employeeAccountID, - managerID: managerAccountID, - policyID: policy.id, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, - total: 1000, - currency: 'USD', - }; - - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); - }); - - afterEach(async () => { - await Onyx.clear(); - }); - - it('should follow normal approval chain when manager approves without take control', async () => { - // Set session as manager (first approver in the chain) - await Onyx.set(ONYXKEYS.SESSION, { - email: managerEmail, - accountID: managerAccountID, - }); - - // Manager approves the report (no take control actions) - approveMoneyRequest(expenseReport); - await waitForBatchedUpdates(); - - // Should be submitted to admin (next in approval chain) since manager is not the final approver - const updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); - expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); - expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); - expect(updatedReport?.managerID).toBe(adminAccountID); // Should be forwarded to admin - }); - - it('should handle multi-step approval chain correctly', async () => { - // First, manager approves - await Onyx.set(ONYXKEYS.SESSION, { - email: managerEmail, - accountID: managerAccountID, - }); - - approveMoneyRequest(expenseReport); - await waitForBatchedUpdates(); - - // Should be submitted to admin - let updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); - expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.SUBMITTED); - expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); - expect(updatedReport?.managerID).toBe(adminAccountID); - - // Then, admin approves - await Onyx.set(ONYXKEYS.SESSION, { - email: adminEmail, - accountID: adminAccountID, - }); - - approveMoneyRequest(updatedReport); - await waitForBatchedUpdates(); - - // Should be fully approved - updatedReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); - expect(updatedReport?.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); - expect(updatedReport?.statusNum).toBe(CONST.REPORT.STATUS_NUM.APPROVED); - }); - }); }); From ffd0075838f1e4669e500be102c0a03be760fc87 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Thu, 16 Oct 2025 18:21:02 +0530 Subject: [PATCH 3/5] Revert imorts --- src/libs/ReportUtils.ts | 1 - src/libs/actions/IOU.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6d4604a318f0..4ad108888fab 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -186,7 +186,6 @@ import { getReportActionMessageText, getReportActionText, getRetractedMessage, - getSortedReportActions, getTravelUpdateMessage, getWorkspaceCurrencyUpdateMessage, getWorkspaceFrequencyUpdateMessage, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 44a5c4f03309..c5752061ef04 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -68,7 +68,7 @@ import Log from '@libs/Log'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import {buildNextStep, buildNextStepNew} from '@libs/NextStepUtils'; +import {buildNextStep} from '@libs/NextStepUtils'; import * as NumberUtils from '@libs/NumberUtils'; import {getManagerMcTestParticipant, getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; From 3f332e846f69497e1ec7752c3cf5a55ab2b66c9e Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Thu, 16 Oct 2025 18:25:43 +0530 Subject: [PATCH 4/5] Small fix --- src/libs/actions/IOU.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c5752061ef04..0b75240b989f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -10143,6 +10143,8 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const predictedNextStatus = isLastApprover(approvalChain) ? CONST.REPORT.STATUS_NUM.APPROVED : CONST.REPORT.STATUS_NUM.SUBMITTED; const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; const managerID = isLastApprover(approvalChain) ? expenseReport.managerID : getNextApproverAccountID(expenseReport); + + const optimisticNextStep = buildNextStep(expenseReport, predictedNextStatus); const chatReport = getReportOrDraftReport(expenseReport.chatReportID); const optimisticReportActionsData: OnyxUpdate = { From 93fab11a673ccda13eaad9c0dc10384f16103fee Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Thu, 16 Oct 2025 20:48:48 +0530 Subject: [PATCH 5/5] Fix lint --- src/libs/actions/IOU.ts | 1 - tests/actions/IOUTest.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0b75240b989f..396f479eb4ed 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -170,7 +170,6 @@ import { hasOutstandingChildRequest, hasReportBeenReopened, hasReportBeenRetracted, - hasViolations as hasViolationsReportUtils, isArchivedReport, isClosedReport as isClosedReportUtil, isDraftReport, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 857b8f6e0897..65a72133180c 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -11,7 +11,6 @@ import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransact import type {PerDiemExpenseTransactionParams, RequestMoneyParticipantParams} from '@libs/actions/IOU'; import { addSplitExpenseField, - approveMoneyRequest, calculateDiffAmount, canApproveIOU, canCancelPayment,