diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 1978be33d565..9d42d25c11f1 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -63,6 +63,7 @@ import { isScanning, isScanningTransaction, } from '@libs/TransactionUtils'; +import {isValidAccountRoute} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -1311,6 +1312,7 @@ function submitReport({ const adminAccountID = policy?.role === CONST.POLICY.ROLE.ADMIN ? currentUserAccountIDParam : undefined; const parentReport = getReportOrDraftReport(expenseReport.parentReportID); const managerID = getSubmitReportManagerAccountID(policy, expenseReport); + const optimisticNextStepApproverID = !isSubmitAndClosePolicy && managerID !== undefined && isValidAccountRoute(managerID) ? managerID : undefined; const isCurrentUserManager = currentUserAccountIDParam === managerID; const optimisticSubmittedReportAction = buildOptimisticSubmittedReportAction( expenseReport?.total ?? 0, @@ -1336,6 +1338,7 @@ function submitReport({ hasViolations, isASAPSubmitBetaEnabled, isUnapprove: true, + bypassNextApproverID: optimisticNextStepApproverID, }); const optimisticNextStep = isDEWPolicy ? null @@ -1348,6 +1351,7 @@ function submitReport({ hasViolations, isASAPSubmitBetaEnabled, isUnapprove: true, + bypassNextApproverID: optimisticNextStepApproverID, }); const optimisticData: Array< OnyxUpdate diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index 3f180737bd73..e341a28368ea 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -30,7 +30,7 @@ import * as API from '@src/libs/API'; import DateUtils from '@src/libs/DateUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportNameValuePairs} from '@src/types/onyx'; +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'; @@ -1381,6 +1381,100 @@ describe('actions/IOU/ReportWorkflow', () => { const optimisticReportUpdate = onyxData.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); expect((optimisticReportUpdate?.value as Report | undefined)?.managerID).toBe(adminAccountID); }); + + it('uses the rule approver in the optimistic next step when the existing report manager is stale', async () => { + // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting API.write calls to verify submit payload and optimistic data. + const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve()); + const policyID = '1'; + const submitterAccountID = 100; + const defaultApproverAccountID = 101; + const ruleApproverAccountID = 102; + const submitterEmail = 'submitter@example.com'; + const defaultApproverEmail = 'default-approver@example.com'; + const ruleApproverEmail = 'rule-approver@example.com'; + + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [submitterAccountID]: {accountID: submitterAccountID, login: submitterEmail}, + [defaultApproverAccountID]: {accountID: defaultApproverAccountID, login: defaultApproverEmail}, + [ruleApproverAccountID]: {accountID: ruleApproverAccountID, login: ruleApproverEmail}, + }); + + const policy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.CORPORATE, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + approver: defaultApproverEmail, + owner: defaultApproverEmail, + employeeList: { + [submitterEmail]: { + email: submitterEmail, + submitsTo: defaultApproverEmail, + }, + }, + rules: { + approvalRules: [ + { + id: 'travel-rule', + applyWhen: [ + { + field: CONST.POLICY.FIELDS.CATEGORY, + condition: CONST.POLICY.RULE_CONDITIONS.MATCHES, + value: 'Travel', + }, + ], + approver: ruleApproverEmail, + }, + ], + }, + }; + const expenseReport: Report = { + ...createRandomReport(Number(policyID), undefined), + reportID: '1', + policyID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: submitterAccountID, + managerID: defaultApproverAccountID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + total: 1000, + currency: CONST.CURRENCY.USD, + }; + const transaction: Transaction = { + ...createRandomTransaction(1), + reportID: expenseReport.reportID, + category: 'Travel', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await waitForBatchedUpdates(); + + submitReport({ + expenseReport, + policy, + currentUserAccountIDParam: submitterAccountID, + currentUserEmailParam: submitterEmail, + hasViolations: false, + isASAPSubmitBetaEnabled: false, + expenseReportCurrentNextStepDeprecated: undefined, + userBillingGracePeriodEnds: undefined, + amountOwed: 0, + ownerBillingGracePeriodEnd: undefined, + delegateEmail: undefined, + }); + + const [, parameters, onyxData] = apiWriteSpy.mock.calls.at(-1) as [unknown, {managerAccountID?: number}, OnyxData]; + expect(parameters.managerAccountID).toBe(ruleApproverAccountID); + + const optimisticReportUpdate = onyxData.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`); + expect((optimisticReportUpdate?.value as Report | undefined)?.managerID).toBe(ruleApproverAccountID); + expect((optimisticReportUpdate?.value as Report | undefined)?.nextStep?.actorAccountID).toBe(ruleApproverAccountID); + + const optimisticDeprecatedNextStepUpdate = onyxData.optimisticData?.find((update) => update.key === `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`); + const optimisticDeprecatedNextStep = optimisticDeprecatedNextStepUpdate?.value as ReportNextStepDeprecated | undefined; + expect(optimisticDeprecatedNextStep?.message?.find((message) => message.type === 'strong')?.text).toBe(ruleApproverEmail); + }); + it('keeps the workspace chat outstanding when an admin submits after approver changes', async () => { // eslint-disable-next-line rulesdir/no-multiple-api-calls -- Inspecting optimistic parent chat data after submit from workspace chat. const apiWriteSpy = jest.spyOn(API, 'write').mockImplementation(() => Promise.resolve());