Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/hooks/useOptimisticNextStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
buildOptimisticNextStepForDynamicExternalWorkflowSubmitError,
buildOptimisticNextStepForStrictPolicyRuleViolations,
getReportNextStep,
shouldShowDynamicExternalWorkflowApproveErrorNextStep,
} from '@libs/NextStepUtils';
import {hasDynamicExternalWorkflow} from '@libs/PolicyUtils';
import {getFilteredReportActionsForReportView, hasPendingDEWApprove, hasPendingDEWSubmit} from '@libs/ReportActionsUtils';
Expand Down Expand Up @@ -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();
Expand Down
18 changes: 17 additions & 1 deletion src/libs/NextStepUtils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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<ReportAction>, 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',
Expand Down Expand Up @@ -875,6 +890,7 @@ export {
buildOptimisticNextStepForStrictPolicyRuleViolations,
buildOptimisticNextStepForDynamicExternalWorkflowSubmitError,
buildOptimisticNextStepForDynamicExternalWorkflowApproveError,
shouldShowDynamicExternalWorkflowApproveErrorNextStep,
buildOptimisticNextStepForDEWOffline,
buildOptimisticNextStepForPreventSelfApprovalsEnabled,
buildNextStepNew,
Expand Down
54 changes: 53 additions & 1 deletion tests/unit/NextStepUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<typeof CONST.REPORT.ACTIONS.TYPE.DEW_APPROVE_FAILED> => ({
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;
Expand Down
Loading