diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index b4639e4fe93a..b93c07bbe82a 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -4,14 +4,15 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {OnyxInputOrEntry, PersonalDetails, Policy, Report} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; +import type Transaction from '@src/types/onyx/Transaction'; import SafeString from '@src/utils/SafeString'; import type {IOURequestType} from './actions/IOU'; import {getCurrencyUnit} from './CurrencyUtils'; import Navigation from './Navigation/Navigation'; import Performance from './Performance'; import {isPaidGroupPolicy} from './PolicyUtils'; -import {getReportTransactions} from './ReportUtils'; -import {getCurrency, getTagArrayFromName} from './TransactionUtils'; +import {getReportTransactions, isExpenseRequest, isPolicyExpenseChat} from './ReportUtils'; +import {getCurrency, getTagArrayFromName, isMerchantMissing, isScanRequest} from './TransactionUtils'; function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: IOUType, transactionID: string, reportID: string, iouAction?: IOUAction): void { if (iouAction === CONST.IOU.ACTION.CATEGORIZE || iouAction === CONST.IOU.ACTION.SUBMIT || iouAction === CONST.IOU.ACTION.SHARE) { @@ -239,6 +240,32 @@ function formatCurrentUserToAttendee(currentUser?: PersonalDetails, reportID?: s return [initialAttendee]; } +/** + * Checks if merchant is required and missing for a transaction. + * Merchant is required for policy expense chats, expense requests, or when any participant is a policy expense chat. + * For scan requests, merchant is not required unless it's a split bill being edited. + * + * @param transaction - The transaction to check + * @param report - The report associated with the transaction + * @param isEditingSplitBill - Whether this is editing a split bill + * @returns true if merchant is required and missing, false otherwise + */ +function shouldRequireMerchant(transaction: OnyxInputOrEntry | undefined, report: OnyxInputOrEntry | undefined, isEditingSplitBill = false): boolean { + if (!transaction) { + return false; + } + + // Check if merchant is required based on report type and participants + const isMerchantRequired = !!(isPolicyExpenseChat(report) || isExpenseRequest(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat)); + + // For scan requests, merchant is not required unless it's a split bill being edited + if (isScanRequest(transaction) && !isEditingSplitBill) { + return false; + } + + return isMerchantRequired && isMerchantMissing(transaction); +} + export { calculateAmount, insertTagIntoTransactionTagsString, @@ -251,4 +278,5 @@ export { formatCurrentUserToAttendee, navigateToParticipantPage, shouldShowReceiptEmptyState, + shouldRequireMerchant, }; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index cd6bd7ea5d74..c6a18d5e9e4f 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -14,7 +14,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import {setTransactionReport} from '@libs/actions/Transaction'; import {convertToBackendAmount} from '@libs/CurrencyUtils'; -import {navigateToParticipantPage} from '@libs/IOUUtils'; +import {navigateToParticipantPage, shouldRequireMerchant} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; @@ -240,8 +240,15 @@ function IOURequestStepAmount({ setSplitShares(transaction, amountInSmallestCurrencyUnits, selectedCurrency || CONST.CURRENCY.USD, participantAccountIDs); } setMoneyRequestParticipantsFromReport(transactionID, report, currentUserPersonalDetails.accountID).then(() => { + // Check if merchant is required and missing before proceeding + // If so, navigate to merchant step first + if (shouldRequireMerchant(transaction, report, isEditingSplitBill)) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID, undefined, reportActionID)); + return; + } navigateToConfirmationPage(); }); + return; } @@ -261,7 +268,9 @@ function IOURequestStepAmount({ const resetToDefaultWorkspace = () => { setTransactionReport(transactionID, {reportID: transactionReportID}, true); setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat, currentUserPersonalDetails.accountID).then(() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.SUBMIT, transactionID, activePolicyExpenseChat?.reportID)); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, activePolicyExpenseChat?.reportID, undefined, reportActionID), + ); }); }; @@ -283,6 +292,10 @@ function IOURequestStepAmount({ const chatReportID = selectedReport?.chatReportID ?? iouReportID; Navigation.setNavigationActionToMicrotaskQueue(() => { + if (shouldRequireMerchant(transaction, selectedReport, isEditingSplitBill)) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.CREATE, navigationIOUType, transactionID, chatReportID, undefined, reportActionID)); + return; + } Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, navigationIOUType, transactionID, chatReportID)); }); } else { diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index e5e7666490cd..9c4d753e331a 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -19,6 +19,7 @@ import {isValidInputLength} from '@libs/ValidationUtils'; import {setDraftSplitTransaction, setMoneyRequestMerchant, updateMoneyRequestMerchant} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestMerchantForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -129,7 +130,15 @@ function IOURequestStepMerchant({ currentUserEmailParam, isASAPSubmitBetaEnabled, ); + navigateBack(); + return; + } + + if (!backTo) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, reportID)); + return; } + navigateBack(); }; diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 5a0abd341183..89157e2f78d5 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -21,7 +21,7 @@ import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {findSelfDMReportID, generateReportID, isInvoiceRoomWithID} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {endSpan} from '@libs/telemetry/activeSpans'; -import {getRequestType, isCorporateCardTransaction, isPerDiemRequest} from '@libs/TransactionUtils'; +import {getRequestType, isCorporateCardTransaction, isMerchantMissing, isPerDiemRequest} from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector'; import { navigateToStartStepIfScanFileCannotBeRead, @@ -95,6 +95,7 @@ function IOURequestStepParticipants({ // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? (participants.at(0)?.reportID ?? reportID) : reportID); + const selectedParticipants = useRef(participants); // We can assume that shouldAutoReport is true as the initial value is not used. shouldAutoReport is only used after the selectedReportID changes in addParticipant where we'd update shouldAutoReport too const shouldAutoReport = useRef(true); const numberOfParticipants = useRef(participants?.length ?? 0); @@ -231,6 +232,7 @@ function IOURequestStepParticipants({ (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); + selectedParticipants.current = val; const firstParticipant = val.at(0); if (firstParticipant?.isSelfDM && !isSplitRequest) { @@ -354,6 +356,9 @@ function IOURequestStepParticipants({ return; } + const firstParticipant = selectedParticipants.current?.at(0); + const isMerchantRequired = !!firstParticipant?.isPolicyExpenseChat && isMerchantMissing(initialTransaction); + const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( action, iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.TRACK ? CONST.IOU.TYPE.SUBMIT : iouType, @@ -364,9 +369,18 @@ function IOURequestStepParticipants({ action === CONST.IOU.ACTION.SHARE ? Navigation.getActiveRoute() : undefined, ); + // eslint-disable-next-line no-nested-ternary const route = isCategorizing ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, initialTransactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) - : iouConfirmationPageRoute; + : isMerchantRequired + ? ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( + action, + iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.TRACK ? CONST.IOU.TYPE.SUBMIT : iouType, + initialTransactionID, + newReportID, + undefined, + ) + : iouConfirmationPageRoute; Performance.markStart(CONST.TIMING.OPEN_CREATE_EXPENSE_APPROVE); waitForKeyboardDismiss(() => {