diff --git a/src/hooks/useOptimisticNextStep.ts b/src/hooks/useOptimisticNextStep.ts index 5a80fae71dab..0a81fdee2367 100644 --- a/src/hooks/useOptimisticNextStep.ts +++ b/src/hooks/useOptimisticNextStep.ts @@ -5,6 +5,7 @@ import { buildOptimisticNextStepForDynamicExternalWorkflowSubmitError, buildOptimisticNextStepForStrictPolicyRuleViolations, getReportNextStep, + shouldShowDynamicExternalWorkflowApproveErrorNextStep, } from '@libs/NextStepUtils'; import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; import {getFilteredReportActionsForReportView, hasPendingDEWApprove, hasPendingDEWSubmit} from '@libs/ReportActionsUtils'; @@ -96,7 +97,7 @@ function useOptimisticNextStep(reportID: string | undefined) { const gbrResult = getReasonAndReportActionThatRequiresAttention(moneyRequestReport, currentUserLogin ?? '', accountID, undefined, isArchivedReport); const hasDEWApproveFailed = gbrResult?.reason === CONST.REQUIRES_ATTENTION_REASONS.HAS_DEW_APPROVE_FAILED; const isCurrentUserTheApprover = moneyRequestReport?.managerID === accountID; - if (hasDEWApproveFailed && isCurrentUserTheApprover) { + if (shouldShowDynamicExternalWorkflowApproveErrorNextStep(gbrResult?.reportAction, hasDEWApproveFailed, isCurrentUserTheApprover)) { optimisticNextStep = buildOptimisticNextStepForDynamicExternalWorkflowApproveError(theme.danger); } else if (isOffline && hasPendingDEWApprove(reportMetadata, isDEWPolicy)) { optimisticNextStep = buildOptimisticNextStepForDEWOffline(); diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index d046cefc29fd..1c0da837aca4 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,7 +1,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import type {Policy, Report, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; import type {ReportNextStep} from '@src/types/onyx/Report'; import type {Message} from '@src/types/onyx/ReportNextStepDeprecated'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -16,6 +16,7 @@ import EmailUtils from './EmailUtils'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from './LocalePhoneNumber'; import {deprecatedGetLoginsByAccountIDs, deprecatedGetPersonalDetailsByIDs} from './PersonalDetailsUtils'; import {getApprovalWorkflow, getCorrectedAutoReportingFrequency, getReimburserAccountID} from './PolicyUtils'; +import {getOriginalMessage, isDynamicExternalWorkflowApproveFailedAction} from './ReportActionsUtils'; import { getDisplayNameForParticipant, getMoneyRequestSpendBreakdown, @@ -461,6 +462,20 @@ function buildOptimisticNextStepForDynamicExternalWorkflowApproveError(iconFill? return optimisticNextStep; } +/** + * Whether to show the DEW approve-error next step. + * Only manual approve failures (`automaticAction` false/absent) for the current approver should show it. + * Auto-approval blocks keep the normal workflow next step. + */ +function shouldShowDynamicExternalWorkflowApproveErrorNextStep(reportAction: OnyxEntry, hasDEWApproveFailed: boolean, isCurrentUserTheApprover: boolean): boolean { + if (!hasDEWApproveFailed || !isCurrentUserTheApprover || !isDynamicExternalWorkflowApproveFailedAction(reportAction)) { + return false; + } + + const {automaticAction} = getOriginalMessage(reportAction) ?? {}; + return !automaticAction; +} + function buildOptimisticNextStepForDEWOffline() { const optimisticNextStep: ReportNextStepDeprecated = { type: 'neutral', @@ -875,6 +890,7 @@ export { buildOptimisticNextStepForStrictPolicyRuleViolations, buildOptimisticNextStepForDynamicExternalWorkflowSubmitError, buildOptimisticNextStepForDynamicExternalWorkflowApproveError, + shouldShowDynamicExternalWorkflowApproveErrorNextStep, buildOptimisticNextStepForDEWOffline, buildOptimisticNextStepForPreventSelfApprovalsEnabled, buildNextStepNew, diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 6163aa8173bd..8dc756ddf617 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -3,17 +3,19 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import { buildNextStepMessage, buildNextStepNew, + buildOptimisticNextStepForDynamicExternalWorkflowApproveError, buildOptimisticNextStepForDynamicExternalWorkflowSubmitError, buildOptimisticNextStepForPreventSelfApprovalsEnabled, buildOptimisticNextStepForStrictPolicyRuleViolations, getReportNextStep, buildOptimisticNextStep, + shouldShowDynamicExternalWorkflowApproveErrorNextStep, } from '@libs/NextStepUtils'; import {buildOptimisticEmptyReport, buildOptimisticExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, Report, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; import type {ReportNextStep} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; @@ -1123,6 +1125,56 @@ describe('libs/NextStepUtils', () => { }); }); + describe('buildOptimisticNextStepForDynamicExternalWorkflowApproveError', () => { + test('should return alert next step with error message when DEW approve fails', () => { + const result = buildOptimisticNextStepForDynamicExternalWorkflowApproveError(); + + expect(result).toEqual({ + type: 'alert', + icon: CONST.NEXT_STEP.ICONS.DOT_INDICATOR, + message: [ + { + text: "This report can't be approved. Please review the comments to resolve.", + type: 'alert-text', + }, + ], + }); + }); + }); + + describe('shouldShowDynamicExternalWorkflowApproveErrorNextStep', () => { + const createDEWApproveFailedAction = (automaticAction?: boolean): ReportAction => ({ + actionName: CONST.REPORT.ACTIONS.TYPE.DEW_APPROVE_FAILED, + reportActionID: '1', + created: '2026-01-01 00:00:00.000', + message: [], + originalMessage: { + message: 'DEW blocked approval', + automaticAction, + }, + }); + + it('returns true for manual approve failures when the current user is the approver', () => { + expect(shouldShowDynamicExternalWorkflowApproveErrorNextStep(createDEWApproveFailedAction(false), true, true)).toBe(true); + }); + + it('returns true when automaticAction is absent (treated as manual failure)', () => { + expect(shouldShowDynamicExternalWorkflowApproveErrorNextStep(createDEWApproveFailedAction(), true, true)).toBe(true); + }); + + it('returns false for auto-approve failures so the normal workflow next step is kept', () => { + expect(shouldShowDynamicExternalWorkflowApproveErrorNextStep(createDEWApproveFailedAction(true), true, true)).toBe(false); + }); + + it('returns false when the current user is not the approver', () => { + expect(shouldShowDynamicExternalWorkflowApproveErrorNextStep(createDEWApproveFailedAction(false), true, false)).toBe(false); + }); + + it('returns false when there is no active DEW approve failure', () => { + expect(shouldShowDynamicExternalWorkflowApproveErrorNextStep(createDEWApproveFailedAction(false), false, true)).toBe(false); + }); + }); + describe('getReportNextStep', () => { const currentUserEmail = 'current-user@expensify.com'; const currentUserAccountID = 37;