From 85c27e3a970a27f1c72d3de7f68c463edb154160 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:36:05 +0100 Subject: [PATCH 01/14] add return type for usePolicyForMovingExpenses --- src/hooks/usePolicyForMovingExpenses.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts index 74f3e8384158..6344b3cc6619 100644 --- a/src/hooks/usePolicyForMovingExpenses.ts +++ b/src/hooks/usePolicyForMovingExpenses.ts @@ -77,7 +77,13 @@ function getPolicyQualificationResult( return {singlePolicyID, isMemberOfMoreThanOnePolicy, validExpensePolicyID}; } -function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string) { +type PolicyForMovingExpenses = { + policyForMovingExpensesID: string | undefined; + policyForMovingExpenses: OnyxEntry; + shouldSelectPolicy: boolean; +}; + +function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string): PolicyForMovingExpenses { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { selector: activePolicySelector, From e389dd2dabb4ddd36911e81c30d60df391eae8a9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:38:28 +0100 Subject: [PATCH 02/14] add shouldNavigateToUpgradePath return field --- src/hooks/usePolicyForMovingExpenses.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts index 6344b3cc6619..7afd4bbfa710 100644 --- a/src/hooks/usePolicyForMovingExpenses.ts +++ b/src/hooks/usePolicyForMovingExpenses.ts @@ -81,6 +81,7 @@ type PolicyForMovingExpenses = { policyForMovingExpensesID: string | undefined; policyForMovingExpenses: OnyxEntry; shouldSelectPolicy: boolean; + shouldNavigateToUpgradePath: boolean; }; function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string): PolicyForMovingExpenses { @@ -107,22 +108,22 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: // If an expense policy ID is provided and valid, prefer it over the active policy if (validExpensePolicyID) { - return {policyForMovingExpensesID: validExpensePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false}; + return {policyForMovingExpensesID: validExpensePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; } if (activePolicy && (!isPerDiemRequest || canSubmitPerDiemExpenseFromWorkspace(activePolicy)) && (!isTimeRequest || isTimeTrackingEnabled(activePolicy))) { - return {policyForMovingExpensesID: activePolicyID, policyForMovingExpenses: activePolicy, shouldSelectPolicy: false}; + return {policyForMovingExpensesID: activePolicyID, policyForMovingExpenses: activePolicy, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; } if (singlePolicyID && !isMemberOfMoreThanOnePolicy) { - return {policyForMovingExpensesID: singlePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false}; + return {policyForMovingExpensesID: singlePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; } if (isMemberOfMoreThanOnePolicy) { - return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: true}; + return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: true, shouldNavigateToUpgradePath: false}; } - return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false}; + return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false, shouldNavigateToUpgradePath: true}; } export default usePolicyForMovingExpenses; From 2045a323ae134096a4c70f1b03edec8d333a4628 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:47:13 +0100 Subject: [PATCH 03/14] use returned shouldNavigateToUpgradePath from usePolicyForMovingExpenses as source of truth --- .../hooks/useFooterDerivedFlags.ts | 3 +-- src/components/MoneyRequestHeaderSecondaryActions.tsx | 3 +-- src/components/Navigation/QuickCreationActionsBar/index.tsx | 3 +-- src/components/ReportActionItem/MoneyRequestView.tsx | 3 +-- .../SearchPageHeader/SearchActionsBarCreateButton.tsx | 3 +-- src/pages/Search/SearchTransactionsChangeReport.tsx | 6 +++--- src/pages/iou/SplitExpenseEditPage.tsx | 3 +-- 7 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts b/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts index 86f04e47438d..32116ef76be1 100644 --- a/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts +++ b/src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts @@ -62,7 +62,7 @@ function useFooterDerivedFlags({ isTypeInvoice, shouldShowSmartScanFields, }: UseFooterDerivedFlagsParams) { - const {policyForMovingExpensesID, policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(); + const {policyForMovingExpenses, shouldSelectPolicy, shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(); const transaction = useTransactionSelector(transactionID, derivedFlagsSliceSelector); @@ -90,7 +90,6 @@ function useFooterDerivedFlags({ const shouldShowBillable = isBillableEnabledOnPolicy(policy); const shouldShowReimbursable = (isPolicyExpenseChat || isTrackExpense) && !!policy && policy?.disabledFields?.reimbursable !== true && !isManagedCardTransaction(transaction) && !isTypeInvoice; - const shouldNavigateToUpgradePath = !policyForMovingExpensesID && !shouldSelectPolicy; const shouldShowTimeRequestFields = isTimeRequest && action === CONST.IOU.ACTION.CREATE; return { diff --git a/src/components/MoneyRequestHeaderSecondaryActions.tsx b/src/components/MoneyRequestHeaderSecondaryActions.tsx index 4344a67f75f4..c68855d88dfa 100644 --- a/src/components/MoneyRequestHeaderSecondaryActions.tsx +++ b/src/components/MoneyRequestHeaderSecondaryActions.tsx @@ -158,8 +158,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money const defaultExpensePolicy = useDefaultExpensePolicy(); const [defaultPolicyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${getNonEmptyStringOnyxID(defaultExpensePolicy?.id)}`); const [defaultPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(defaultExpensePolicy?.id)}`); - const {policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiemRequest(transaction)); - const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy; + const {shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(isPerDiemRequest(transaction)); const {deleteTransactions, shouldOpenSplitExpenseEditFlowOnDelete} = useDeleteTransactions({report: parentReport, reportActions: parentReportAction ? [parentReportAction] : [], policy}); const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(parentReportAction); const isParentReportArchived = useReportIsArchived(report?.parentReportID); diff --git a/src/components/Navigation/QuickCreationActionsBar/index.tsx b/src/components/Navigation/QuickCreationActionsBar/index.tsx index 7b57025a2b28..9b3ee1a13013 100644 --- a/src/components/Navigation/QuickCreationActionsBar/index.tsx +++ b/src/components/Navigation/QuickCreationActionsBar/index.tsx @@ -54,8 +54,7 @@ function QuickCreationActionsBar() { const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(); - const shouldNavigateToUpgradePath = !policyForMovingExpensesID && !shouldSelectPolicy; + const {shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(); const isSubmit2026BetaEnabled = isBetaEnabled(CONST.BETAS.SUBMIT_2026); const groupPoliciesWithChatEnabledSelector = (policies: OnyxCollection) => getGroupPoliciesWhereReportCanBeCreated(policies, isSubmit2026BetaEnabled, email); const [groupPoliciesWithChatEnabled = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPoliciesWithChatEnabledSelector}, [email, isSubmit2026BetaEnabled]); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 7dbfc194e101..1a08fec1e822 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -219,7 +219,7 @@ function MoneyRequestView({ const transaction = updatedTransaction ?? onyxTransaction; const isExpenseUnreported = isExpenseUnreportedTransactionUtils(transaction); const personalPolicy = usePersonalPolicy(); - const {policyForMovingExpensesID, policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(); + const {policyForMovingExpensesID, policyForMovingExpenses, shouldSelectPolicy, shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(); const isTimeRequest = isTimeRequestTransactionUtils(transaction); const [policiesWithPerDiem] = useOnyx(ONYXKEYS.COLLECTION.POLICY, { @@ -558,7 +558,6 @@ function MoneyRequestView({ amountTitle = translate('iou.receiptStatusTitle'); } - const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy; const updatedTransactionDescription = getDescription(updatedTransaction) || undefined; const shouldHideEmptyDescription = (isFromReviewDuplicates || isFromMergeTransaction) && !(updatedTransactionDescription ?? transactionDescription); const isEmptyUpdatedMerchant = isInvalidMerchantValue(updatedTransaction?.modifiedMerchant); diff --git a/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx index 13ccdacd59db..403ce9902067 100644 --- a/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx +++ b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx @@ -58,8 +58,7 @@ function SearchActionsBarCreateButton() { const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED); - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(); - const shouldNavigateToUpgradePath = !policyForMovingExpensesID && !shouldSelectPolicy; + const {shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(); const defaultChatEnabledPolicy = useMemo( () => getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled as Array>, activePolicy), [activePolicy, groupPoliciesWithChatEnabled], diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 1dd04647975b..ec6d0342bc95 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -63,7 +63,7 @@ function SearchTransactionsChangeReport() { // Get the policyID from the selected transactions' report to pass to usePolicyForMovingExpenses // This ensures the "Create report" button shows the correct workspace instead of the user's default const selectedReportPolicyID = selectedReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`]?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, selectedReportPolicyID); + const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, selectedReportPolicyID); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; const areAllTransactionsUnreported = selectedTransactionsKeys.length > 0 && selectedTransactionsKeys.every((transactionKey) => selectedTransactions[transactionKey]?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID); @@ -170,7 +170,7 @@ function SearchTransactionsChangeReport() { }); const createReport = () => { - if (!policyForMovingExpensesID && !shouldSelectPolicy && selectedTransactionsKeys.length > 0) { + if (shouldNavigateToUpgradePath && selectedTransactionsKeys.length > 0) { const firstTransactionID = selectedTransactionsKeys.at(0); if (firstTransactionID) { Navigation.navigate( @@ -190,7 +190,7 @@ function SearchTransactionsChangeReport() { Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute(true))); return; } - if (!policyForMovingExpensesID) { + if (shouldNavigateToUpgradePath) { Navigation.navigate( ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ action: CONST.IOU.ACTION.CREATE, diff --git a/src/pages/iou/SplitExpenseEditPage.tsx b/src/pages/iou/SplitExpenseEditPage.tsx index f9fa600f70ee..bdc0cafcabea 100644 --- a/src/pages/iou/SplitExpenseEditPage.tsx +++ b/src/pages/iou/SplitExpenseEditPage.tsx @@ -82,8 +82,7 @@ function SplitExpenseEditPage({route}: SplitExpensePageProps) { // Detect selfDM splits whose source workspace is gone: nothing for the Rate step to render. const hasAnyPaidWorkspace = hasAnyPaidPolicy(allPolicies ?? {}); - const {policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(); - const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy; + const {shouldSelectPolicy, shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(); const effectivePolicyID = effectivePolicy?.id; From 35af664861db65317d3db51fa549bd9d0291df3f Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:59:26 +0100 Subject: [PATCH 04/14] add isNonOwnedManagedCardTransaction param to be able to send an empty policyID to backend --- src/hooks/usePolicyForMovingExpenses.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts index 7afd4bbfa710..25fa3324cbc6 100644 --- a/src/hooks/usePolicyForMovingExpenses.ts +++ b/src/hooks/usePolicyForMovingExpenses.ts @@ -84,7 +84,7 @@ type PolicyForMovingExpenses = { shouldNavigateToUpgradePath: boolean; }; -function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string): PolicyForMovingExpenses { +function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string, isNonOwnedManagedCardTransaction?: boolean): PolicyForMovingExpenses { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { selector: activePolicySelector, @@ -93,6 +93,12 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: const session = useSession(); const login = session?.email ?? ''; + // If this is an employee's card transaction that we manage, then we should report it to their default policy + // which we don't know. Sending an empty `policyID` instructs the backend to auto-select the preferred policy. + if (isNonOwnedManagedCardTransaction) { + return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; + } + // Contextual selector — captures login/flags from closure. // Returns only IDs + flags (stable output) to prevent re-renders when unrelated policies change. const policyQualificationSelector = (policies: OnyxCollection) => getPolicyQualificationResult(policies, login, isPerDiemRequest, isTimeRequest, expensePolicyID); From 09d8fae36fde95c61a252babda8d71f5871dff74 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:39:14 +0100 Subject: [PATCH 05/14] get empty policyID when reporting unreported managed card transactions --- src/hooks/usePolicyForMovingExpenses.ts | 4 ++-- src/pages/Search/SearchTransactionsChangeReport.tsx | 10 +++++++++- src/pages/iou/request/step/IOURequestEditReport.tsx | 3 +++ .../iou/request/step/IOURequestEditReportCommon.tsx | 4 +++- src/pages/iou/request/step/IOURequestStepReport.tsx | 3 ++- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts index 25fa3324cbc6..c575b2858c76 100644 --- a/src/hooks/usePolicyForMovingExpenses.ts +++ b/src/hooks/usePolicyForMovingExpenses.ts @@ -84,7 +84,7 @@ type PolicyForMovingExpenses = { shouldNavigateToUpgradePath: boolean; }; -function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string, isNonOwnedManagedCardTransaction?: boolean): PolicyForMovingExpenses { +function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: boolean, expensePolicyID?: string, isUnreportedManagedCardTransaction?: boolean): PolicyForMovingExpenses { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { selector: activePolicySelector, @@ -95,7 +95,7 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: // If this is an employee's card transaction that we manage, then we should report it to their default policy // which we don't know. Sending an empty `policyID` instructs the backend to auto-select the preferred policy. - if (isNonOwnedManagedCardTransaction) { + if (isUnreportedManagedCardTransaction) { return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; } diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index ec6d0342bc95..9cc3b0feb724 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -17,6 +17,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav import Navigation from '@libs/Navigation/Navigation'; import {generateReportID, getPersonalDetailsForAccountID, getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {isExpenseUnreported, isManagedCardTransaction} from '@libs/TransactionUtils'; import IOURequestEditReportCommon from '@pages/iou/request/step/IOURequestEditReportCommon'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -48,6 +49,7 @@ function SearchTransactionsChangeReport() { const [betas] = useOnyx(ONYXKEYS.BETAS); const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const hasPerDiemTransactions = useHasPerDiemTransactions(selectedTransactionsKeys); + const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isExpenseUnreported(transaction) && isManagedCardTransaction(transaction)); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); @@ -63,7 +65,12 @@ function SearchTransactionsChangeReport() { // Get the policyID from the selected transactions' report to pass to usePolicyForMovingExpenses // This ensures the "Create report" button shows the correct workspace instead of the user's default const selectedReportPolicyID = selectedReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`]?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, selectedReportPolicyID); + const {policyForMovingExpensesID, shouldSelectPolicy, shouldNavigateToUpgradePath} = usePolicyForMovingExpenses( + hasPerDiemTransactions, + undefined, + selectedReportPolicyID, + hasUnreportedManagedCardTransactions, + ); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; const areAllTransactionsUnreported = selectedTransactionsKeys.length > 0 && selectedTransactionsKeys.every((transactionKey) => selectedTransactions[transactionKey]?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID); @@ -275,6 +282,7 @@ function SearchTransactionsChangeReport() { targetOwnerAccountID={targetOwnerAccountID} transactionPolicyID={selectedReportPolicyID} isPerDiemRequest={hasPerDiemTransactions} + isUnreportedManagedCardTransactions={hasUnreportedManagedCardTransactions} /> ); } diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index a72aff3eaa4b..249a786eff30 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -17,6 +17,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailsForAccountID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {isExpenseUnreported, isManagedCardTransaction} from '@libs/TransactionUtils'; import {createNewReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -72,6 +73,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; const [betas] = useOnyx(ONYXKEYS.BETAS); const [transactions] = useTransactionsByID(transactionIDs); + const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isExpenseUnreported(transaction) && isManagedCardTransaction(transaction)); const selectReport = (item: TransactionGroupListItem, report?: OnyxEntry) => { if (transactionIDs.length === 0 || item.value === reportID) { Navigation.dismissToSuperWideRHP(); @@ -182,6 +184,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { isEditing={action === CONST.IOU.ACTION.EDIT} createReport={createReport} isPerDiemRequest={hasPerDiemTransactions} + isUnreportedManagedCardTransactions={hasUnreportedManagedCardTransactions} transactionPolicyID={targetExpensePolicyID} /> ); diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index ea83654af8f6..0c6a924fc1bd 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -50,6 +50,7 @@ type Props = { createReport?: () => void; isPerDiemRequest: boolean; isTimeRequest?: boolean; + isUnreportedManagedCardTransactions?: boolean; }; function IOURequestEditReportCommon({ @@ -67,6 +68,7 @@ function IOURequestEditReportCommon({ createReport, isPerDiemRequest, isTimeRequest = false, + isUnreportedManagedCardTransactions = false, }: Props) { const icons = useMemoizedLazyExpensifyIcons(['Close', 'Document']); const {inputCallbackRef} = useAutoFocusInput(); @@ -94,7 +96,7 @@ function IOURequestEditReportCommon({ // Use the caller-provided transactionPolicyID so that the "Create report" button shows the correct workspace. // Each caller is responsible for passing the appropriate policyID (e.g., selectedReport?.policyID ?? transactionPolicyID). // When no transactionPolicyID is provided (e.g., from IOURequestEditReport), the hook falls back to the user's default workspace. - const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, transactionPolicyID); + const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, transactionPolicyID, isUnreportedManagedCardTransactions); const [perDiemWarningModalVisible, setPerDiemWarningModalVisible] = useState(false); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 5b68a0dfe733..e741c53e129c 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -16,7 +16,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getPersonalDetailsForAccountID, getReportOrDraftReport, isPolicyExpenseChat, isReportOutstanding} from '@libs/ReportUtils'; -import {isPerDiemRequest, isTimeRequest as isTimeRequestUtil} from '@libs/TransactionUtils'; +import {isExpenseUnreported, isManagedCardTransaction, isPerDiemRequest, isTimeRequest as isTimeRequestUtil} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -198,6 +198,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { shouldShowNotFoundPage={shouldShowNotFoundPage} isPerDiemRequest={transaction ? isPerDiemTransaction : false} isTimeRequest={transaction ? isTimeRequestUtil(transaction) : false} + isUnreportedManagedCardTransactions={transaction ? isExpenseUnreported(transaction) && isManagedCardTransaction(transaction) : false} createReport={policyForMovingExpensesID || shouldSelectPolicy || isPerDiemTransaction ? createReport : undefined} targetOwnerAccountID={ownerAccountID} /> From f0527f985f585fb616ad00f7d04bf3489a346253 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:14:46 +0100 Subject: [PATCH 06/14] pass missed isUnreportedManagedCardTransaction param --- src/pages/iou/request/step/IOURequestStepReport.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index e741c53e129c..8f3d9e9cfab5 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -69,6 +69,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { : selectedReport?.ownerAccountID; const ownerPersonalDetails = getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails; const isPerDiemTransaction = isPerDiemRequest(transaction); + const isUnreportedManagedCardTransactions = isExpenseUnreported(transaction) && isManagedCardTransaction(transaction); const transactionPolicyID = transaction?.participants?.at(0)?.isPolicyExpenseChat ? transaction?.participants.at(0)?.policyID : undefined; // When moving an expense that belongs to another user, or when the selection includes per diem @@ -79,7 +80,12 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { // we need to fall back to transactionPolicyID because for a new workspace there is no report created yet // and if we choose this workspace as participant we want to create a new report in the chosen workspace - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiemTransaction, isTimeRequestUtil(transaction), targetExpensePolicyID); + const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses( + isPerDiemTransaction, + isTimeRequestUtil(transaction), + targetExpensePolicyID, + isUnreportedManagedCardTransactions, + ); // No violations exist for a report that hasn't been created yet — kept as a literal to avoid subscribing to the entire TRANSACTION_VIOLATIONS collection. const hasViolations = false; @@ -198,7 +204,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { shouldShowNotFoundPage={shouldShowNotFoundPage} isPerDiemRequest={transaction ? isPerDiemTransaction : false} isTimeRequest={transaction ? isTimeRequestUtil(transaction) : false} - isUnreportedManagedCardTransactions={transaction ? isExpenseUnreported(transaction) && isManagedCardTransaction(transaction) : false} + isUnreportedManagedCardTransactions={transaction ? isUnreportedManagedCardTransactions : false} createReport={policyForMovingExpensesID || shouldSelectPolicy || isPerDiemTransaction ? createReport : undefined} targetOwnerAccountID={ownerAccountID} /> From 44a36e4c007406b8d2eea97bcba972537412fa49 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:15:48 +0100 Subject: [PATCH 07/14] fix variable name --- src/pages/Search/SearchTransactionsChangeReport.tsx | 2 +- src/pages/iou/request/step/IOURequestEditReport.tsx | 2 +- src/pages/iou/request/step/IOURequestEditReportCommon.tsx | 6 +++--- src/pages/iou/request/step/IOURequestStepReport.tsx | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 9cc3b0feb724..fb13c3162675 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -282,7 +282,7 @@ function SearchTransactionsChangeReport() { targetOwnerAccountID={targetOwnerAccountID} transactionPolicyID={selectedReportPolicyID} isPerDiemRequest={hasPerDiemTransactions} - isUnreportedManagedCardTransactions={hasUnreportedManagedCardTransactions} + isUnreportedManagedCardTransaction={hasUnreportedManagedCardTransactions} /> ); } diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 249a786eff30..ccbb91a18e78 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -184,7 +184,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { isEditing={action === CONST.IOU.ACTION.EDIT} createReport={createReport} isPerDiemRequest={hasPerDiemTransactions} - isUnreportedManagedCardTransactions={hasUnreportedManagedCardTransactions} + isUnreportedManagedCardTransaction={hasUnreportedManagedCardTransactions} transactionPolicyID={targetExpensePolicyID} /> ); diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 0c6a924fc1bd..859747cb5e91 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -50,7 +50,7 @@ type Props = { createReport?: () => void; isPerDiemRequest: boolean; isTimeRequest?: boolean; - isUnreportedManagedCardTransactions?: boolean; + isUnreportedManagedCardTransaction?: boolean; }; function IOURequestEditReportCommon({ @@ -68,7 +68,7 @@ function IOURequestEditReportCommon({ createReport, isPerDiemRequest, isTimeRequest = false, - isUnreportedManagedCardTransactions = false, + isUnreportedManagedCardTransaction = false, }: Props) { const icons = useMemoizedLazyExpensifyIcons(['Close', 'Document']); const {inputCallbackRef} = useAutoFocusInput(); @@ -96,7 +96,7 @@ function IOURequestEditReportCommon({ // Use the caller-provided transactionPolicyID so that the "Create report" button shows the correct workspace. // Each caller is responsible for passing the appropriate policyID (e.g., selectedReport?.policyID ?? transactionPolicyID). // When no transactionPolicyID is provided (e.g., from IOURequestEditReport), the hook falls back to the user's default workspace. - const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, transactionPolicyID, isUnreportedManagedCardTransactions); + const {policyForMovingExpenses} = usePolicyForMovingExpenses(isPerDiemRequest, isTimeRequest, transactionPolicyID, isUnreportedManagedCardTransaction); const [perDiemWarningModalVisible, setPerDiemWarningModalVisible] = useState(false); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 8f3d9e9cfab5..46577a7fe7aa 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -69,7 +69,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { : selectedReport?.ownerAccountID; const ownerPersonalDetails = getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails; const isPerDiemTransaction = isPerDiemRequest(transaction); - const isUnreportedManagedCardTransactions = isExpenseUnreported(transaction) && isManagedCardTransaction(transaction); + const isUnreportedManagedCardTransaction = isExpenseUnreported(transaction) && isManagedCardTransaction(transaction); const transactionPolicyID = transaction?.participants?.at(0)?.isPolicyExpenseChat ? transaction?.participants.at(0)?.policyID : undefined; // When moving an expense that belongs to another user, or when the selection includes per diem @@ -84,7 +84,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { isPerDiemTransaction, isTimeRequestUtil(transaction), targetExpensePolicyID, - isUnreportedManagedCardTransactions, + isUnreportedManagedCardTransaction, ); // No violations exist for a report that hasn't been created yet — kept as a literal to avoid subscribing to the entire TRANSACTION_VIOLATIONS collection. @@ -204,7 +204,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { shouldShowNotFoundPage={shouldShowNotFoundPage} isPerDiemRequest={transaction ? isPerDiemTransaction : false} isTimeRequest={transaction ? isTimeRequestUtil(transaction) : false} - isUnreportedManagedCardTransactions={transaction ? isUnreportedManagedCardTransactions : false} + isUnreportedManagedCardTransaction={transaction ? isUnreportedManagedCardTransaction : false} createReport={policyForMovingExpensesID || shouldSelectPolicy || isPerDiemTransaction ? createReport : undefined} targetOwnerAccountID={ownerAccountID} /> From fc533b315de697f289938c21d0565cf41ee1cb59 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:30:58 +0100 Subject: [PATCH 08/14] display create report option for unreported managed card transactions --- src/pages/iou/request/step/IOURequestStepReport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 46577a7fe7aa..8ce6a0a07975 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -205,7 +205,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { isPerDiemRequest={transaction ? isPerDiemTransaction : false} isTimeRequest={transaction ? isTimeRequestUtil(transaction) : false} isUnreportedManagedCardTransaction={transaction ? isUnreportedManagedCardTransaction : false} - createReport={policyForMovingExpensesID || shouldSelectPolicy || isPerDiemTransaction ? createReport : undefined} + createReport={policyForMovingExpensesID || shouldSelectPolicy || isPerDiemTransaction || isUnreportedManagedCardTransaction ? createReport : undefined} targetOwnerAccountID={ownerAccountID} /> ); From 3c851376674fce8e508ff7f9e11b66f8fdc55990 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:40:06 +0100 Subject: [PATCH 09/14] fix createReport early return --- src/pages/iou/request/step/IOURequestStepReport.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 8ce6a0a07975..2511c69d2ba6 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -146,7 +146,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const shouldShowNotFoundPage = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, reportOrDraftReport, transaction); const createReportForPolicy = (shouldDismissEmptyReportsConfirmation?: boolean) => { - if (!isPerDiemTransaction && !policyForMovingExpenses?.id) { + if (!isPerDiemTransaction && !isUnreportedManagedCardTransaction && !policyForMovingExpenses?.id) { return; } @@ -172,7 +172,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { handleCreateReport(); return; } - if (!isPerDiemTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!isPerDiemTransaction && !isUnreportedManagedCardTransaction && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } if (shouldSelectPolicy) { From 007a93946cf0c1efb47374d0438397bb5fbcfb26 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:41:41 +0100 Subject: [PATCH 10/14] move return after hooks --- src/hooks/usePolicyForMovingExpenses.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hooks/usePolicyForMovingExpenses.ts b/src/hooks/usePolicyForMovingExpenses.ts index c575b2858c76..5275c7adbab4 100644 --- a/src/hooks/usePolicyForMovingExpenses.ts +++ b/src/hooks/usePolicyForMovingExpenses.ts @@ -93,12 +93,6 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: const session = useSession(); const login = session?.email ?? ''; - // If this is an employee's card transaction that we manage, then we should report it to their default policy - // which we don't know. Sending an empty `policyID` instructs the backend to auto-select the preferred policy. - if (isUnreportedManagedCardTransaction) { - return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; - } - // Contextual selector — captures login/flags from closure. // Returns only IDs + flags (stable output) to prevent re-renders when unrelated policies change. const policyQualificationSelector = (policies: OnyxCollection) => getPolicyQualificationResult(policies, login, isPerDiemRequest, isTimeRequest, expensePolicyID); @@ -112,6 +106,12 @@ function usePolicyForMovingExpenses(isPerDiemRequest?: boolean, isTimeRequest?: const resolvedPolicyID = validExpensePolicyID ?? singlePolicyID; const [resolvedPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${resolvedPolicyID}`); + // If this is an employee's card transaction that we manage, then we should report it to their default policy + // which we don't know. Sending an empty `policyID` instructs the backend to auto-select the preferred policy. + if (isUnreportedManagedCardTransaction) { + return {policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; + } + // If an expense policy ID is provided and valid, prefer it over the active policy if (validExpensePolicyID) { return {policyForMovingExpensesID: validExpensePolicyID, policyForMovingExpenses: resolvedPolicy, shouldSelectPolicy: false, shouldNavigateToUpgradePath: false}; From e280d034aa98caaa9bf5ec6c27ada33bb7b4e4a9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:45:12 +0100 Subject: [PATCH 11/14] fix test and ts --- tests/ui/components/SearchActionsBarCreateButtonTest.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/ui/components/SearchActionsBarCreateButtonTest.tsx b/tests/ui/components/SearchActionsBarCreateButtonTest.tsx index a9e51da5c853..e60505e11360 100644 --- a/tests/ui/components/SearchActionsBarCreateButtonTest.tsx +++ b/tests/ui/components/SearchActionsBarCreateButtonTest.tsx @@ -107,6 +107,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: undefined, policyForMovingExpenses: undefined, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: true, }); await act(async () => { @@ -167,6 +168,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: 'some-policy', policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); // Set up multiple policies with chat enabled @@ -204,6 +206,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: MOCK_POLICY_ID, policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); await act(async () => { @@ -270,6 +273,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: MOCK_POLICY_ID, policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); await act(async () => { @@ -299,6 +303,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: MOCK_POLICY_ID, policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); await act(async () => { @@ -339,6 +344,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: MOCK_POLICY_ID, policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); await act(async () => { @@ -377,6 +383,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: MOCK_POLICY_ID, policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); await act(async () => { @@ -418,6 +425,7 @@ describe('SearchActionsBarCreateButton', () => { policyForMovingExpensesID: MOCK_POLICY_ID, policyForMovingExpenses: MOCK_POLICY, shouldSelectPolicy: false, + shouldNavigateToUpgradePath: false, }); await act(async () => { From 04ac11861936f92f2ffecfad015b0e752af199bf Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:43:48 +0100 Subject: [PATCH 12/14] add isUnreportedManagedCardTransaction util --- src/libs/TransactionUtils/index.ts | 5 +++++ src/pages/Search/SearchTransactionsChangeReport.tsx | 4 ++-- src/pages/iou/request/step/IOURequestEditReport.tsx | 4 ++-- src/pages/iou/request/step/IOURequestStepReport.tsx | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 450ee8c91cec..76f1d77c61b9 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -2950,6 +2950,10 @@ function isExpenseUnreported(transaction?: Transaction): transaction is Unreport return transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; } +function isUnreportedManagedCardTransaction(transaction?: Transaction): boolean { + return isExpenseUnreported(transaction) && isManagedCardTransaction(transaction); +} + /** * Returns true if the violation should block report submission. */ @@ -3215,4 +3219,5 @@ export { hasSmartScanFailedWithMissingFields, isDeletedTransaction, getDistanceRequestType, + isUnreportedManagedCardTransaction, }; diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index fb13c3162675..2ccb98eec00a 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -17,7 +17,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav import Navigation from '@libs/Navigation/Navigation'; import {generateReportID, getPersonalDetailsForAccountID, getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {isExpenseUnreported, isManagedCardTransaction} from '@libs/TransactionUtils'; +import {isUnreportedManagedCardTransaction} from '@libs/TransactionUtils'; import IOURequestEditReportCommon from '@pages/iou/request/step/IOURequestEditReportCommon'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -49,7 +49,7 @@ function SearchTransactionsChangeReport() { const [betas] = useOnyx(ONYXKEYS.BETAS); const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const hasPerDiemTransactions = useHasPerDiemTransactions(selectedTransactionsKeys); - const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isExpenseUnreported(transaction) && isManagedCardTransaction(transaction)); + const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isUnreportedManagedCardTransaction(transaction)); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index ccbb91a18e78..9b0420c65c2a 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -17,7 +17,7 @@ import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNav import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailsForAccountID, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {isExpenseUnreported, isManagedCardTransaction} from '@libs/TransactionUtils'; +import {isUnreportedManagedCardTransaction} from '@libs/TransactionUtils'; import {createNewReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -73,7 +73,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; const [betas] = useOnyx(ONYXKEYS.BETAS); const [transactions] = useTransactionsByID(transactionIDs); - const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isExpenseUnreported(transaction) && isManagedCardTransaction(transaction)); + const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isUnreportedManagedCardTransaction(transaction)); const selectReport = (item: TransactionGroupListItem, report?: OnyxEntry) => { if (transactionIDs.length === 0 || item.value === reportID) { Navigation.dismissToSuperWideRHP(); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 2511c69d2ba6..def512b95975 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -16,7 +16,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getPersonalDetailsForAccountID, getReportOrDraftReport, isPolicyExpenseChat, isReportOutstanding} from '@libs/ReportUtils'; -import {isExpenseUnreported, isManagedCardTransaction, isPerDiemRequest, isTimeRequest as isTimeRequestUtil} from '@libs/TransactionUtils'; +import {isPerDiemRequest, isTimeRequest as isTimeRequestUtil, isUnreportedManagedCardTransaction as isUnreportedManagedCardTransactionUtil} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; @@ -69,7 +69,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { : selectedReport?.ownerAccountID; const ownerPersonalDetails = getPersonalDetailsForAccountID(ownerAccountID, personalDetails) as PersonalDetails; const isPerDiemTransaction = isPerDiemRequest(transaction); - const isUnreportedManagedCardTransaction = isExpenseUnreported(transaction) && isManagedCardTransaction(transaction); + const isUnreportedManagedCardTransaction = isUnreportedManagedCardTransactionUtil(transaction); const transactionPolicyID = transaction?.participants?.at(0)?.isPolicyExpenseChat ? transaction?.participants.at(0)?.policyID : undefined; // When moving an expense that belongs to another user, or when the selection includes per diem From b83f1fd40bf56d6019174c8c9c8c7e4a47b47dac Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:58:07 +0100 Subject: [PATCH 13/14] pass isUnreportedManagedCardTransaction in IOURequestEditReport usePolicyForMovingExpenses --- src/pages/iou/request/step/IOURequestEditReport.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 9b0420c65c2a..c5fa7c752d7b 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -58,6 +58,8 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { () => getPersonalDetailsForAccountID(selectedReport?.ownerAccountID, personalDetails) as PersonalDetails, [personalDetails, selectedReport?.ownerAccountID], ); + const [transactions] = useTransactionsByID(transactionIDs); + const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isUnreportedManagedCardTransaction(transaction)); const hasPerDiemTransactions = useHasPerDiemTransactions(transactionIDs); // When moving an expense that belongs to another user, or when the selection includes per diem @@ -67,13 +69,11 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { const isOwnedByOther = selectedReport?.ownerAccountID !== currentUserPersonalDetails.accountID; const isOwnedByOtherOrHasPerDiem = isOwnedByOther || hasPerDiemTransactions; const targetExpensePolicyID = isOwnedByOtherOrHasPerDiem ? selectedReport?.policyID : undefined; - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, targetExpensePolicyID); + const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(hasPerDiemTransactions, undefined, targetExpensePolicyID, hasUnreportedManagedCardTransactions); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, currentUserPersonalDetails.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails.email ?? ''); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; const [betas] = useOnyx(ONYXKEYS.BETAS); - const [transactions] = useTransactionsByID(transactionIDs); - const hasUnreportedManagedCardTransactions = transactions.some((transaction) => isUnreportedManagedCardTransaction(transaction)); const selectReport = (item: TransactionGroupListItem, report?: OnyxEntry) => { if (transactionIDs.length === 0 || item.value === reportID) { Navigation.dismissToSuperWideRHP(); @@ -129,7 +129,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { }; const createReportForPolicy = (shouldDismissEmptyReportsConfirmation?: boolean) => { - if (!hasPerDiemTransactions && !policyForMovingExpenses?.id) { + if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpenses?.id) { return; } @@ -164,7 +164,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { handleCreateReport(); return; } - if (!hasPerDiemTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { + if (!hasPerDiemTransactions && !hasUnreportedManagedCardTransactions && !policyForMovingExpensesID && !shouldSelectPolicy) { return; } if (shouldSelectPolicy) { From f6ee561a0b4bab44721506bad6ce4a0ba48a1a5e Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:11:56 +0100 Subject: [PATCH 14/14] set parentReport optimistic data only if report exists --- src/libs/actions/Report/index.ts | 53 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 58f3b3afabb2..cfec44891798 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -4090,28 +4090,30 @@ function buildNewReportOptimisticData( value: {[reportActionID]: optimisticCreateAction}, }, { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`, + value: optimisticNextStepDeprecated, + }, + ]; + if (parentReport) { + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, value: {[reportPreviewReportActionID]: optimisticReportPreview}, - }, - { + }); + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, value: {iouReportID: reportID, ...outstandingChildRequest}, - }, - { + }); + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${parentReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${parentReport.reportID}`, value: { hasOnceLoadedReportActions: true, }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`, - value: optimisticNextStepDeprecated, - }, - ]; + }); + } optimisticData.push(...updateTitleFieldToMatchPolicy(reportID, policy)); @@ -4158,13 +4160,14 @@ function buildNewReportOptimisticData( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[reportActionID]: {errors: {createReport: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')}}}, }, - - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`, - value: {hasOutstandingChildRequest: parentReport?.hasOutstandingChildRequest}, - }, ]; + if (parentReport) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, + value: {hasOutstandingChildRequest: parentReport.hasOutstandingChildRequest}, + }); + } const successData: Array> = [ { @@ -4189,17 +4192,19 @@ function buildNewReportOptimisticData( }, }, }, - { + ]; + if (parentReport) { + successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, value: { [reportPreviewReportActionID]: { pendingAction: null, errors: null, }, }, - }, - ]; + }); + } return { optimisticReportName: optimisticReportData.reportName,