From bbe1844404dfb60aabf420b856a2239d3e99a7fe Mon Sep 17 00:00:00 2001 From: "thelullabyy (via MelvinBot)" Date: Mon, 5 Oct 2026 18:16:38 +0000 Subject: [PATCH] Keep duplicated expenses on the source report when it can accept them Co-authored-by: thelullabyy --- .../MoneyRequestHeaderSecondaryActions.tsx | 38 +++++-- src/hooks/useExpenseActions.ts | 34 ++++-- src/libs/ReportUtils.ts | 35 ++++++ tests/actions/IOUTest/DuplicateTest.ts | 67 ++++++++++++ tests/unit/ReportUtilsTest.ts | 100 ++++++++++++++++++ 5 files changed, 254 insertions(+), 20 deletions(-) diff --git a/src/components/MoneyRequestHeaderSecondaryActions.tsx b/src/components/MoneyRequestHeaderSecondaryActions.tsx index aa3527947c50..b4fdff1a9e6d 100644 --- a/src/components/MoneyRequestHeaderSecondaryActions.tsx +++ b/src/components/MoneyRequestHeaderSecondaryActions.tsx @@ -50,6 +50,7 @@ import {getOriginalMessage, getTrackExpenseActionableWhisper, isMoneyRequestActi import {getTransactionThreadPrimaryAction} from '@libs/ReportPrimaryActionUtils'; import {getSecondaryTransactionThreadActions} from '@libs/ReportSecondaryActionUtils'; import { + canDuplicateExpenseIntoSourceReport, changeMoneyRequestHoldStatus, generateReportID, getPolicyExpenseChat, @@ -200,8 +201,6 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money // Custom hooks const defaultExpensePolicy = useDefaultExpensePolicy(); - const [defaultPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(defaultExpensePolicy?.id)}`); - const [defaultPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(defaultExpensePolicy?.id)}`); const {shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(isPerDiemRequest(transaction)); const {deleteTransactions, shouldOpenSplitExpenseEditFlowOnDelete} = useDeleteTransactions({report: parentReport, reportActions: parentReportAction ? [parentReportAction] : [], policy}); const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(parentReportAction); @@ -238,7 +237,24 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money getTransactionThreadPrimaryAction(currentUserLogin ?? '', accountID, report, parentReport, parentOwnerLogin, transaction, transactionViolations, policy, false) ); const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id); - const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && defaultExpensePolicy?.id !== policy?.id; + + // Duplicate expense target: the expense's report when it can accept the copy, otherwise the default workspace chat + const canDuplicateIntoSourceReport = canDuplicateExpenseIntoSourceReport({ + sourceReport: iouReport, + chatReport: chatIOUReport, + policy: iouPolicy, + currentUserLogin: currentUserLogin ?? '', + isSourceReportArchived: isParentReportArchived, + isChatReportArchived: isChatIOUReportArchived, + isASAPSubmitBetaEnabled, + rules, + }); + const duplicateTargetPolicy = canDuplicateIntoSourceReport ? iouPolicy : defaultExpensePolicy; + const duplicateTargetReport = canDuplicateIntoSourceReport ? iouReport : activePolicyExpenseChat; + const [duplicateTargetPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(duplicateTargetPolicy?.id)}`); + const [duplicateTargetPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(duplicateTargetPolicy?.id)}`); + + const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && duplicateTargetPolicy?.id !== policy?.id; const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations); const isParentChatReportDM = isDM(chatIOUReport); const isDistanceExpenseUnsupportedForDuplicating = !!( @@ -252,10 +268,10 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money const shouldShowSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen); const shouldShowEditSplitOnDeleteAction = !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]); const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport); - const targetPolicyTags = defaultPolicyTags ?? {}; + const targetPolicyTags = duplicateTargetPolicyTags ?? {}; - const policyTagList = useMoneyRequestPolicyTagsForReport({report: activePolicyExpenseChat, currentUserAccountID: accountID}); - const participants = getMoneyRequestParticipantsFromReport(activePolicyExpenseChat, accountID); + const policyTagList = useMoneyRequestPolicyTagsForReport({report: duplicateTargetReport, currentUserAccountID: accountID}); + const participants = getMoneyRequestParticipantsFromReport(duplicateTargetReport, accountID); const participantsPolicyTags = useParticipantsPolicyTags(participants); // Duplicate action throttle @@ -274,7 +290,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money const optimisticChatReportID = generateReportID(); const optimisticIOUReportID = generateReportID(); - const activePolicyCategoriesMap = defaultPolicyCategories ?? {}; + const activePolicyCategoriesMap = duplicateTargetPolicyCategories ?? {}; for (const item of transactions) { const existingTransactionID = getExistingTransactionID(item.linkedTrackedExpenseReportAction); @@ -293,9 +309,9 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], isSelfTourViewed, customUnitPolicyID: policy?.id, - targetPolicy: defaultExpensePolicy ?? undefined, + targetPolicy: duplicateTargetPolicy ?? undefined, targetPolicyCategories: activePolicyCategoriesMap, - targetReport: activePolicyExpenseChat, + targetReport: duplicateTargetReport, existingTransactionDraft, personalDetails, recentWaypoints, @@ -494,9 +510,9 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money iconFill: isDuplicateActive ? undefined : theme.icon, value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DUPLICATE, onSelected: () => { - if (defaultExpensePolicy && shouldRestrictUserBillableActions(defaultExpensePolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID)) { + if (duplicateTargetPolicy && shouldRestrictUserBillableActions(duplicateTargetPolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID)) { dropdownMenuRef.current?.setIsMenuVisible(false); - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(defaultExpensePolicy.id)); + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(duplicateTargetPolicy.id)); return; } diff --git a/src/hooks/useExpenseActions.ts b/src/hooks/useExpenseActions.ts index fde6f28bd86e..e7805514157a 100644 --- a/src/hooks/useExpenseActions.ts +++ b/src/hooks/useExpenseActions.ts @@ -18,6 +18,7 @@ import {getDistanceExpenseTypeForPolicy} from '@libs/PolicyDistanceRatesUtils'; import {isPolicyAccessible} from '@libs/PolicyUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import { + canDuplicateExpenseIntoSourceReport, canEditFieldOfMoneyRequest, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, generateReportID, @@ -177,6 +178,20 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic const defaultExpensePolicy = useDefaultExpensePolicy(); const activePolicyExpenseChat = getPolicyExpenseChat(accountID, defaultExpensePolicy?.id); + // Duplicate expense target: the viewed report when it can accept the copy, otherwise the default workspace chat + const canDuplicateIntoSourceReport = canDuplicateExpenseIntoSourceReport({ + sourceReport: moneyRequestReport, + chatReport, + policy, + currentUserLogin: currentUserLogin ?? '', + isSourceReportArchived: isArchivedReport, + isChatReportArchived, + isASAPSubmitBetaEnabled, + rules, + }); + const duplicateTargetPolicy = canDuplicateIntoSourceReport ? policy : defaultExpensePolicy; + const duplicateTargetReport = canDuplicateIntoSourceReport ? moneyRequestReport : activePolicyExpenseChat; + // Duplicate detection const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID)); @@ -227,7 +242,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic // Duplicate expense: unsupported / shouldClose flags const transactionViolations = useTransactionViolations(transaction?.transactionID); const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations); - const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && defaultExpensePolicy?.id !== policy?.id; + const isPerDiemRequestOnNonDefaultWorkspace = isPerDiemRequest(transaction) && duplicateTargetPolicy?.id !== policy?.id; const isDistanceExpenseUnsupportedForDuplicating = !!( isDistanceRequest(transaction) && (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport)))) @@ -236,6 +251,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic isDistanceExpenseUnsupportedForDuplicating || isPerDiemRequestOnNonDefaultWorkspace || hasCustomUnitOutOfPolicyViolation || + canDuplicateIntoSourceReport || activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID; const handleDuplicateReset = () => { @@ -246,10 +262,10 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic }; const [isDuplicateActive, temporarilyDisableDuplicateAction] = useThrottledButtonState(handleDuplicateReset); - const targetPolicyTags = defaultExpensePolicy ? (allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${defaultExpensePolicy.id}`] ?? {}) : {}; + const targetPolicyTags = duplicateTargetPolicy ? (allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${duplicateTargetPolicy.id}`] ?? {}) : {}; - const policyTagList = useMoneyRequestPolicyTagsForReport({report: activePolicyExpenseChat, currentUserAccountID: accountID}); - const participants = getMoneyRequestParticipantsFromReport(activePolicyExpenseChat, accountID); + const policyTagList = useMoneyRequestPolicyTagsForReport({report: duplicateTargetReport, currentUserAccountID: accountID}); + const participants = getMoneyRequestParticipantsFromReport(duplicateTargetReport, accountID); const participantsPolicyTags = useParticipantsPolicyTags(participants); const duplicateExpenseTransaction = (transactionList: OnyxTypes.Transaction[]) => { @@ -258,7 +274,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic } const optimisticChatReportID = generateReportID(); const optimisticIOUReportID = generateReportID(); - const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${defaultExpensePolicy?.id}`] ?? {}; + const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${duplicateTargetPolicy?.id}`] ?? {}; for (const item of transactionList) { const existingTransactionID = getExistingTransactionID(item.linkedTrackedExpenseReportAction); @@ -277,9 +293,9 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], isSelfTourViewed, customUnitPolicyID: policy?.id, - targetPolicy: defaultExpensePolicy ?? undefined, + targetPolicy: duplicateTargetPolicy ?? undefined, targetPolicyCategories: activePolicyCategories, - targetReport: activePolicyExpenseChat, + targetReport: duplicateTargetReport, existingTransactionDraft, personalDetails, recentWaypoints, @@ -373,9 +389,9 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic iconFill: isDuplicateActive ? undefined : theme.icon, value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_EXPENSE, onSelected: () => { - if (defaultExpensePolicy && shouldRestrictUserBillableActions(defaultExpensePolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID)) { + if (duplicateTargetPolicy && shouldRestrictUserBillableActions(duplicateTargetPolicy, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, accountID)) { onDuplicateReset?.(); - Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(defaultExpensePolicy.id)); + Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(duplicateTargetPolicy.id)); return; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9467bbfaca2..ef3149ec411e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -162,6 +162,7 @@ import { isInstantSubmitEnabled, isPendingDeletePolicy, isPerDiemEnabled, + isPolicyAccessible, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyAuditor, isPolicyOwner, @@ -12256,6 +12257,39 @@ function shouldCreateNewMoneyRequestReport( ); } +type CanDuplicateExpenseIntoSourceReportParams = { + sourceReport: OnyxEntry; + chatReport: OnyxEntry; + policy: OnyxEntry; + currentUserLogin: string; + isSourceReportArchived: boolean; + isChatReportArchived: boolean; + isASAPSubmitBetaEnabled: boolean; + rules: OnyxCollection; +}; + +/** + * Whether a duplicated expense can be added to the expense report its source lives on, instead of the default workspace chat. + * Reuses `shouldCreateNewMoneyRequestReport` so this always agrees with the report the money request builder will pick. + * Duplicates are never scan requests because the receipt is stripped, so `isScanRequest` is always false here. + */ +function canDuplicateExpenseIntoSourceReport({ + sourceReport, + chatReport, + policy, + currentUserLogin, + isSourceReportArchived, + isChatReportArchived, + isASAPSubmitBetaEnabled, + rules, +}: CanDuplicateExpenseIntoSourceReportParams): boolean { + if (!isExpenseReport(sourceReport) || isSourceReportArchived || !chatReport || isChatReportArchived || !isPolicyAccessible(policy, currentUserLogin)) { + return false; + } + + return !shouldCreateNewMoneyRequestReport(sourceReport, chatReport, false, isASAPSubmitBetaEnabled, rules, CONST.IOU.ACTION.CREATE, true); +} + /** Precomputed report-action error state used to make per-transaction RBR checks O(1). */ type ActionErrorsByTransaction = { /** A non-money-request action (or money-request action without an IOUTransactionID) has errors, flagging every transaction. */ @@ -14598,6 +14632,7 @@ export { canAccessReport, isReportNotFound, canAddTransaction, + canDuplicateExpenseIntoSourceReport, canDeleteTransaction, canBeAutoReimbursed, canCreateRequest, diff --git a/tests/actions/IOUTest/DuplicateTest.ts b/tests/actions/IOUTest/DuplicateTest.ts index f98a16e351a2..c7699cdf62cd 100644 --- a/tests/actions/IOUTest/DuplicateTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -1886,6 +1886,73 @@ describe('actions/Duplicate', () => { expect(duplicatedTransaction?.transactionID).not.toBe(mockCashExpenseTransaction.transactionID); }); + it('should add the duplicate to the open expense report passed as targetReport', async () => { + // Given an open expense report on a workspace, while the workspace chat points at a different report + const sourcePolicy: Policy = {...createRandomPolicy(42), type: CONST.POLICY.TYPE.TEAM, role: CONST.POLICY.ROLE.USER, pendingAction: null, autoReporting: false}; + const sourceChatReport: Report = { + ...createRandomReport(42, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + policyID: sourcePolicy.id, + ownerAccountID: RORY_ACCOUNT_ID, + iouReportID: 'another-report', + hasOutstandingChildRequest: false, + }; + const sourceExpenseReport: Report = { + ...createRandomReport(43, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + policyID: sourcePolicy.id, + chatReportID: sourceChatReport.reportID, + ownerAccountID: RORY_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + errorFields: undefined, + pendingFields: undefined, + }; + const {waypoints, ...restOfComment} = mockTransaction.comment ?? {}; + const sourceTransaction: Transaction = {...mockTransaction, reportID: sourceExpenseReport.reportID, amount: mockTransaction.amount * -1, comment: restOfComment}; + + await Onyx.set(ONYXKEYS.SESSION, {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL}); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${sourcePolicy.id}`, sourcePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${sourceChatReport.reportID}`, sourceChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${sourceExpenseReport.reportID}`, sourceExpenseReport); + await waitForBatchedUpdates(); + + // When the expense is duplicated with that expense report as the target + duplicateExpenseTransaction({ + isVendorMatchingBetaEnabled: false, + dateFnsLocale: undefined, + conciergeChat: undefined, + transaction: sourceTransaction, + optimisticChatReportID: mockOptimisticChatReportID, + optimisticIOUReportID: mockOptimisticIOUReportID, + isASAPSubmitBetaEnabled: mockIsASAPSubmitBetaEnabled, + introSelected: undefined, + quickAction: undefined, + policyRecentlyUsedCurrencies: [], + isSelfTourViewed: false, + customUnitPolicyID: sourcePolicy.id, + targetPolicy: sourcePolicy, + targetPolicyCategories: fakePolicyCategories, + targetReport: sourceExpenseReport, + existingTransactionDraft: undefined, + personalDetails: mockPersonalDetails, + recentWaypoints, + targetPolicyTags: {}, + policyTagList: {}, + currentUser: {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL}, + currentUserLocalCurrency: undefined, + delegateAccountID: undefined, + isTrackIntentUser: false, + formatPhoneNumber, + getCurrencyDecimals: getCurrencyDecimalsLocal, + participantsPolicyTags: {}, + rules: undefined, + }); + await waitForBatchedUpdates(); + + // Then the duplicate is requested on the source expense report, not on the report the workspace chat points at + expect(writeSpy).toHaveBeenCalledWith(WRITE_COMMANDS.REQUEST_MONEY, expect.objectContaining({iouReportID: sourceExpenseReport.reportID}), expect.objectContaining({})); + }); + it('should create a duplicate time expense successfully', async () => { const transactionID = 'time-1'; const HOURLY_RATE = 9.99; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index aad60ecbfaeb..9630d9eec3de 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -59,6 +59,7 @@ import { buildParticipantsFromAccountIDs, buildTransactionThread, canAddTransaction, + canDuplicateExpenseIntoSourceReport, canBeAutoReimbursed, canCreateRequest, canDeleteMoneyRequestReport, @@ -17561,6 +17562,105 @@ describe('ReportUtils', () => { }); }); + describe('canDuplicateExpenseIntoSourceReport', () => { + const sourcePolicy: Policy = { + ...createRandomPolicy(30001), + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.USER, + pendingAction: null, + autoReporting: false, + }; + const sourceChatReport: Report = { + ...createRandomReport(30002, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + policyID: sourcePolicy.id, + ownerAccountID: currentUserAccountID, + hasOutstandingChildRequest: false, + }; + + // An open expense report the current user submitted on an accessible workspace + const buildOpenSourceReport = async (overrides: Partial = {}) => { + const report: Report = { + ...createRandomReport(30003, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + policyID: sourcePolicy.id, + chatReportID: sourceChatReport.reportID, + ownerAccountID: currentUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + errorFields: undefined, + ...overrides, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${sourcePolicy.id}`, sourcePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + return report; + }; + + const buildParams = (sourceReport: Report, overrides: Partial[0]> = {}) => ({ + sourceReport, + chatReport: sourceChatReport, + policy: sourcePolicy, + currentUserLogin: currentUserEmail, + isSourceReportArchived: false, + isChatReportArchived: false, + isASAPSubmitBetaEnabled: false, + rules: undefined, + ...overrides, + }); + + it('returns true for an open expense report the current user can add to', async () => { + // Given an open expense report the current user submitted on an accessible workspace + const sourceReport = await buildOpenSourceReport(); + + // When it's checked whether a duplicate can stay on that report + const result = canDuplicateExpenseIntoSourceReport(buildParams(sourceReport)); + + // Then the duplicate targets the source report + expect(result).toBe(true); + }); + + it('returns false for an approved expense report so a locked report is not reopened', async () => { + // Given an approved expense report + const sourceReport = await buildOpenSourceReport({stateNum: CONST.REPORT.STATE_NUM.APPROVED, statusNum: CONST.REPORT.STATUS_NUM.APPROVED}); + + // When it's checked whether a duplicate can stay on that report + const result = canDuplicateExpenseIntoSourceReport(buildParams(sourceReport)); + + // Then the duplicate falls back to the default workspace routing + expect(result).toBe(false); + }); + + it('returns false when the source report is archived', async () => { + // Given an open expense report that is archived + const sourceReport = await buildOpenSourceReport(); + + // When it's checked whether a duplicate can stay on that report + const result = canDuplicateExpenseIntoSourceReport(buildParams(sourceReport, {isSourceReportArchived: true})); + + // Then the duplicate falls back to the default workspace routing + expect(result).toBe(false); + }); + + it('returns false when the source report workspace is not accessible', async () => { + // Given an open expense report whose workspace is pending delete + const sourceReport = await buildOpenSourceReport(); + + // When it's checked whether a duplicate can stay on that report + const result = canDuplicateExpenseIntoSourceReport(buildParams(sourceReport, {policy: {...sourcePolicy, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}})); + + // Then the duplicate falls back to the default workspace routing + expect(result).toBe(false); + }); + + it('returns false when the source is not an expense report', () => { + // Given a source that is a chat rather than an expense report, like a self-DM tracked expense + // When it's checked whether a duplicate can stay on that source + const result = canDuplicateExpenseIntoSourceReport(buildParams(sourceChatReport)); + + // Then the duplicate keeps its current routing + expect(result).toBe(false); + }); + }); + describe('hasEmptyReportsForPolicy', () => { const policyID = 'workspace-001'; const otherPolicyID = 'workspace-002';