From 03709776a24f08dc992eff6784872d06ef96a4fe Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 23 Dec 2025 12:20:35 +0100 Subject: [PATCH 01/12] fix: revert revert tsxes field --- .../MoneyRequestConfirmationList.tsx | 31 ++++++++++----- .../MoneyRequestConfirmationListFooter.tsx | 22 +++++++++-- .../ReportActionItem/MoneyRequestView.tsx | 2 +- src/hooks/usePolicyForTransaction.ts | 39 +++++++++++++++++++ src/libs/PolicyUtils.ts | 4 +- src/libs/TransactionUtils/index.ts | 4 +- src/libs/actions/IOU.ts | 9 ++++- .../iou/request/step/IOURequestStepAmount.tsx | 5 ++- .../step/IOURequestStepConfirmation.tsx | 2 +- .../step/IOURequestStepParticipants.tsx | 19 ++++++++- .../step/IOURequestStepTaxAmountPage.tsx | 8 ++-- .../step/IOURequestStepTaxRatePage.tsx | 3 +- 12 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 src/hooks/usePolicyForTransaction.ts diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 3039556287397..64defbf96ce67 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -254,6 +254,8 @@ function MoneyRequestConfirmationList({ selector: mileageRateSelector, canBeMissing: true, }); + const {policyForMovingExpenses} = usePolicyForMovingExpenses(); + const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action); const [defaultMileageRateReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { selector: mileageRateSelector, canBeMissing: true, @@ -281,7 +283,6 @@ function MoneyRequestConfirmationList({ isTestDriveReceipt || isManagerMcTestReceipt, ); - const {policyForMovingExpenses} = usePolicyForMovingExpenses(); const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK; const policy = isTrackExpense ? policyForMovingExpenses : (policyReal ?? policyDraft); const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; @@ -299,7 +300,6 @@ function MoneyRequestConfirmationList({ const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const isScanRequest = useMemo(() => isScanRequestUtil(transaction), [transaction]); const isCreateExpenseFlow = !!transaction?.isFromGlobalCreate && !isPerDiemRequest; - const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action); const transactionID = transaction?.transactionID; const customUnitRateID = getRateID(transaction); @@ -343,16 +343,18 @@ function MoneyRequestConfirmationList({ const policyTagLists = useMemo(() => getTagLists(policyTags), [policyTags]); - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest); + const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat || isTrackExpense, policy, isDistanceRequest, isPerDiemRequest); // Update the tax code when the default changes (for example, because the transaction currency changed) - const defaultTaxCode = getDefaultTaxCode(policy, transaction) ?? ''; + const defaultTaxCode = getDefaultTaxCode(policy, transaction) ?? (isMovingTransactionFromTrackExpense ? (getDefaultTaxCode(policyForMovingExpenses, transaction) ?? '') : ''); + useEffect(() => { - if (!transactionID || isReadOnly || !shouldShowTax) { + if (!transactionID || isReadOnly || !shouldShowTax || isMovingTransactionFromTrackExpense) { return; } setMoneyRequestTaxRate(transactionID, defaultTaxCode); - }, [defaultTaxCode, transactionID, isReadOnly, shouldShowTax]); + // trigger this useEffect also when policyID changes - the defaultTaxCode may stay the same + }, [defaultTaxCode, isMovingTransactionFromTrackExpense, isReadOnly, transactionID, policyID, shouldShowTax]); const distance = getDistanceInMeters(transaction, unit); const prevDistance = usePrevious(distance); @@ -503,15 +505,18 @@ function MoneyRequestConfirmationList({ // Calculate and set tax amount in transaction draft const taxableAmount = isDistanceRequest ? DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance) : (transaction?.amount ?? 0); - const taxPercentage = getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? ''; + // First we'll try to get the tax value from the chosen policy and if not found, we'll try to get it from the policy for moving expenses (only if the transaction is moving from track expense) + const taxPercentage = + getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? + (isMovingTransactionFromTrackExpense ? getTaxValue(policyForMovingExpenses, transaction, transaction?.taxCode ?? defaultTaxCode) : ''); const taxAmount = calculateTaxAmount(taxPercentage, taxableAmount, transaction?.currency ?? CONST.CURRENCY.USD); const taxAmountInSmallestCurrencyUnits = convertToBackendAmount(Number.parseFloat(taxAmount.toString())); useEffect(() => { - if (!transactionID || isReadOnly || !shouldShowTax) { + if (!transactionID || isMovingTransactionFromTrackExpense || isReadOnly || !shouldShowTax) { return; } setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits); - }, [transactionID, taxAmountInSmallestCurrencyUnits, isReadOnly, shouldShowTax]); + }, [transactionID, taxAmountInSmallestCurrencyUnits, isMovingTransactionFromTrackExpense, isReadOnly, shouldShowTax]); // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { @@ -836,7 +841,7 @@ function MoneyRequestConfirmationList({ if (!transactionID || iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { return; } - setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy); + setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy, isMovingTransactionFromTrackExpense); // Keep 'transaction' out to ensure that we auto select the option only once // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [shouldShowCategories, policyCategories, isCategoryRequired, policy?.id]); @@ -920,6 +925,11 @@ function MoneyRequestConfirmationList({ return; } + if (shouldShowTax && !Object.keys(policy?.taxRates?.taxes ?? {}).some((key) => key === transaction.taxCode)) { + setFormError('violations.taxOutOfPolicy'); + return; + } + if (isPerDiemRequest && (transaction.comment?.customUnit?.subRates ?? []).length === 0) { setFormError('iou.error.invalidSubrateLength'); return; @@ -977,6 +987,7 @@ function MoneyRequestConfirmationList({ isMerchantRequired, isMerchantEmpty, shouldDisplayFieldError, + shouldShowTax, transaction, policyTags, isPerDiemRequest, diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index b49a1555b6ba0..604d609a213ca 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -16,12 +16,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getDecodedCategoryName} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import {shouldShowReceiptEmptyState} from '@libs/IOUUtils'; +import {isMovingTransactionFromTrackExpense, shouldShowReceiptEmptyState} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getDestinationForDisplay, getSubratesFields, getSubratesForDisplay, getTimeDifferenceIntervals, getTimeForDisplay} from '@libs/PerDiemRequestUtils'; import {canSendInvoice, getPerDiemCustomUnit} from '@libs/PolicyUtils'; import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; +// eslint-disable-next-line @typescript-eslint/no-deprecated import {generateReportID, getDefaultWorkspaceAvatar, getOutstandingReportsForUser, getReportName, isArchivedReport, isMoneyRequestReport, isReportOutstanding} from '@libs/ReportUtils'; import {getTagVisibility, hasEnabledTags} from '@libs/TagsOptionsListUtils'; import { @@ -274,7 +275,7 @@ function MoneyRequestConfirmationListFooter({ const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, { canBeMissing: true, }); - const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(); + const {policyForMovingExpensesID, policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(); const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector, canBeMissing: true}); const isUnreported = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; @@ -346,6 +347,7 @@ function MoneyRequestConfirmationListFooter({ }, [allReports, shouldUseTransactionReport, transaction?.reportID, outstandingReportID]); const reportName = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-deprecated const name = getReportName(selectedReport, selectedPolicy); if (!name) { return isUnreported ? translate('common.none') : translate('iou.newReport'); @@ -355,11 +357,13 @@ function MoneyRequestConfirmationListFooter({ const shouldReportBeEditableFromFAB = isUnreported ? allOutstandingReports.length >= 1 : allOutstandingReports.length > 1; + const isMovingCurrentTransactionFromTrackExpense = isMovingTransactionFromTrackExpense(action); + // When creating an expense in an individual report, the report field becomes read-only // since the destination is already determined and there's no need to show a selectable list. const shouldReportBeEditable = (isFromGlobalCreate ? shouldReportBeEditableFromFAB : availableOutstandingReports.length > 1) && !isMoneyRequestReport(reportID, allReports); - const taxRates = policy?.taxRates ?? null; + const taxRates = policy?.taxRates ?? (isMovingCurrentTransactionFromTrackExpense ? policyForMovingExpenses?.taxRates : null); // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest; // Determines whether the tax fields can be modified. @@ -374,11 +378,19 @@ function MoneyRequestConfirmationListFooter({ const taxAmount = getTaxAmount(transaction, false); const formattedTaxAmount = convertToDisplayString(taxAmount, iouCurrencyCode); // Get the tax rate title based on the policy and transaction - const taxRateTitle = getTaxName(policy, transaction); + let taxRateTitle; + if (getTaxName(policy, transaction)) { + taxRateTitle = getTaxName(policy, transaction); + } else if (isMovingCurrentTransactionFromTrackExpense) { + taxRateTitle = getTaxName(policyForMovingExpenses, transaction); + } else { + taxRateTitle = ''; + } // Determine if the merchant error should be displayed const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty; const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate'; const shouldDisplayTagError = formError === 'violations.tagOutOfPolicy'; + const shouldDisplayTaxRateError = formError === 'violations.taxOutOfPolicy'; const shouldDisplayCategoryError = formError === 'violations.categoryOutOfPolicy'; const showReceiptEmptyState = shouldShowReceiptEmptyState(iouType, action, policy, isPerDiemRequest); @@ -697,6 +709,8 @@ function MoneyRequestConfirmationListFooter({ }} disabled={didConfirm} interactive={canModifyTaxFields} + brickRoadIndicator={shouldDisplayTaxRateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + errorText={shouldDisplayTaxRateError ? translate(formError) : ''} /> ), shouldShow: shouldShowTax, diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index f98e50a039a55..e1ec0205934c0 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -354,7 +354,7 @@ function MoneyRequestView({ const canEditReimbursable = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE, undefined, isChatReportArchived); const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]); - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest); + const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat || isExpenseUnreported, policy, isDistanceRequest, isPerDiemRequest); const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; diff --git a/src/hooks/usePolicyForTransaction.ts b/src/hooks/usePolicyForTransaction.ts new file mode 100644 index 0000000000000..31f33c9b5c811 --- /dev/null +++ b/src/hooks/usePolicyForTransaction.ts @@ -0,0 +1,39 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isExpenseUnreported} from '@libs/TransactionUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, Transaction} from '@src/types/onyx'; +import useOnyx from './useOnyx'; +import usePolicyForMovingExpenses from './usePolicyForMovingExpenses'; + +type UsePolicyForTransactionParams = { + /** The transaction to determine the policy for */ + transaction: OnyxEntry; + + /** The report associated with the transaction */ + report: OnyxEntry; + + /** The current action being performed */ + action: string; + + /** The type of IOU (split, track, submit, etc.) */ + iouType: string; +}; + +type UsePolicyForTransactionResult = { + /** The policy to use for the transaction */ + policy: OnyxEntry; +}; + +function usePolicyForTransaction({transaction, report, action, iouType}: UsePolicyForTransactionParams): UsePolicyForTransactionResult { + const {policyForMovingExpenses} = usePolicyForMovingExpenses(); + const isUnreportedExpense = isExpenseUnreported(transaction); + const isCreatingTrackExpense = action === CONST.IOU.ACTION.CREATE && iouType === CONST.IOU.TYPE.TRACK; + + const [reportPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); + const policy = isUnreportedExpense || isCreatingTrackExpense ? policyForMovingExpenses : reportPolicy; + + return {policy}; +} + +export default usePolicyForTransaction; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 90acb7a26f444..09183761571f4 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -666,13 +666,13 @@ function isCollectPolicy(policy: OnyxEntry): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM; } -function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry, isDistanceRequest: boolean, isPerDiemRequest = false): boolean { +function isTaxTrackingEnabled(isPolicyExpenseChatOrUnreportedExpense: boolean, policy: OnyxEntry, isDistanceRequest: boolean, isPerDiemRequest = false): boolean { if (isPerDiemRequest) { return false; } const distanceUnit = getDistanceRateCustomUnit(policy); const customUnitID = distanceUnit?.customUnitID ?? CONST.DEFAULT_NUMBER_ID; - const isPolicyTaxTrackingEnabled = isPolicyExpenseChat && policy?.tax?.trackingEnabled; + const isPolicyTaxTrackingEnabled = isPolicyExpenseChatOrUnreportedExpense && policy?.tax?.trackingEnabled; const isTaxEnabledForDistance = isPolicyTaxTrackingEnabled && !!customUnitID && policy?.customUnits?.[customUnitID]?.attributes?.taxEnabled; return !!(isDistanceRequest ? isTaxEnabledForDistance : isPolicyTaxTrackingEnabled); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 57fffc9200b72..78616e428c78e 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1809,7 +1809,9 @@ function getWorkspaceTaxesSettingsName(policy: OnyxEntry, taxCode: strin */ function getTaxName(policy: OnyxEntry, transaction: OnyxEntry) { const defaultTaxCode = getDefaultTaxCode(policy, transaction); - return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === (transaction?.taxCode ?? defaultTaxCode))?.modifiedName; + // transaction?.taxCode may be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === (transaction?.taxCode || defaultTaxCode))?.modifiedName; } type FieldsToCompare = Record>; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bef8fc4e6f69b..a7f1f7e1d506b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1268,9 +1268,13 @@ function setMoneyRequestPendingFields(transactionID: string, pendingFields: Onyx * @param transactionID - The transaction ID * @param category - The category name * @param policy - The policy object, or undefined for P2P transactions where tax info should be cleared + * @param isMovingFromTrackExpense - If the expense is moved from Track Expense */ -function setMoneyRequestCategory(transactionID: string, category: string, policy: OnyxEntry) { +function setMoneyRequestCategory(transactionID: string, category: string, policy: OnyxEntry, isMovingFromTrackExpense?: boolean) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {category}); + if (isMovingFromTrackExpense) { + return; + } if (!policy) { setMoneyRequestTaxRate(transactionID, ''); setMoneyRequestTaxAmount(transactionID, null); @@ -6469,7 +6473,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep category, tag, taxCode, - taxAmount, + taxAmount: Math.abs(taxAmount), billable, policyID: chatReport.policyID, waypoints: sanitizedWaypoints, @@ -12150,6 +12154,7 @@ function completePaymentOnboarding( shouldSkipTestDriveModal: true, }); } + function payMoneyRequest( paymentType: PaymentMethodType, chatReport: OnyxTypes.Report, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 461f71fc02396..581afb316156b 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -11,6 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePersonalPolicy from '@hooks/usePersonalPolicy'; +import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import {setTransactionReport} from '@libs/actions/Transaction'; @@ -79,7 +80,9 @@ function IOURequestStepAmount({ const focusTimeoutRef = useRef(null); const isSaveButtonPressed = useRef(false); const iouRequestType = getRequestType(transaction); - const policyID = report?.policyID; + const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK; + const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); + const policyID = isTrackExpense ? policyForMovingExpensesID : report?.policyID; const isReportArchived = useReportIsArchived(report?.reportID); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 62d76da12531e..6f01ccfb92833 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -398,7 +398,7 @@ function IOURequestStepConfirmation({ if (!isDistanceRequest || !!item?.category) { continue; } - setMoneyRequestCategory(item.transactionID, defaultCategory, policy); + setMoneyRequestCategory(item.transactionID, defaultCategory, policy, isMovingTransactionFromTrackExpense); } // Prevent resetting to default when unselect category // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 60a15a275e8f4..8302e287b17a6 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -7,6 +7,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useThemeStyles from '@hooks/useThemeStyles'; import {setTransactionReport} from '@libs/actions/Transaction'; import {READ_COMMANDS} from '@libs/API/types'; @@ -77,6 +78,7 @@ function IOURequestStepParticipants({ const {translate} = useLocalize(); const styles = useThemeStyles(); const isFocused = useIsFocused(); + const {policyForMovingExpenses} = usePolicyForMovingExpenses(); const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true}); const [optimisticTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, { selector: transactionDraftValuesSelector, @@ -327,7 +329,7 @@ function IOURequestStepParticipants({ const tag = isMovingTransactionFromTrackExpense && transaction?.tag ? transaction?.tag : ''; setMoneyRequestTag(transaction.transactionID, tag); const category = isMovingTransactionFromTrackExpense && transaction?.category ? transaction?.category : ''; - setMoneyRequestCategory(transaction.transactionID, category, undefined); + setMoneyRequestCategory(transaction.transactionID, category, isMovingTransactionFromTrackExpense ? policyForMovingExpenses : undefined, isMovingTransactionFromTrackExpense); if (shouldUpdateTransactionReportID) { setTransactionReport(transaction.transactionID, {reportID: transactionReportID}, true); } @@ -382,7 +384,20 @@ function IOURequestStepParticipants({ }); } }); - }, [action, participants, iouType, initialTransaction, transactions, initialTransactionID, reportID, waitForKeyboardDismiss, isMovingTransactionFromTrackExpense, backTo, introSelected]); + }, [ + action, + participants, + iouType, + initialTransaction, + transactions, + initialTransactionID, + reportID, + waitForKeyboardDismiss, + isMovingTransactionFromTrackExpense, + policyForMovingExpenses, + introSelected, + backTo, + ]); const navigateBack = useCallback(() => { if (backTo) { diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index fb08838265f7d..615a39d5c4693 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -6,6 +6,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useRestartOnReceiptFailure from '@hooks/useRestartOnReceiptFailure'; import {setDraftSplitTransaction, setMoneyRequestCurrency, setMoneyRequestParticipantsFromReport, setMoneyRequestTaxAmount, updateMoneyRequestTaxAmount} from '@libs/actions/IOU'; import {convertToBackendAmount, isValidCurrencyCode} from '@libs/CurrencyUtils'; @@ -51,9 +52,10 @@ function IOURequestStepTaxAmountPage({ transaction, report, }: IOURequestStepTaxAmountPageProps) { - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`, {canBeMissing: true}); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); + const {policy} = usePolicyForTransaction({transaction, report, action, iouType}); + + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true}); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); const {translate} = useLocalize(); const textInput = useRef(null); diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 9ff0831ad9c85..9c636238f2601 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -5,6 +5,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useRestartOnReceiptFailure from '@hooks/useRestartOnReceiptFailure'; import {convertToBackendAmount} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -40,8 +41,8 @@ function IOURequestStepTaxRatePage({ report, }: IOURequestStepTaxRatePageProps) { const {translate} = useLocalize(); + const {policy} = usePolicyForTransaction({transaction, report, action, iouType}); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true}); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); From 358c9569ed6ae19304aee72a27979676b0f3c89f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 23 Dec 2025 21:08:41 +0100 Subject: [PATCH 02/12] fix: recalculate the taxAmount when currency changes --- src/components/MoneyRequestConfirmationList.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 64defbf96ce67..453a7fcd728ea 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -518,6 +518,15 @@ function MoneyRequestConfirmationList({ setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits); }, [transactionID, taxAmountInSmallestCurrencyUnits, isMovingTransactionFromTrackExpense, isReadOnly, shouldShowTax]); + useEffect(() => { + if (!transactionID || !isMovingTransactionFromTrackExpense || isReadOnly || !shouldShowTax) { + return; + } + setMoneyRequestTaxRate(transactionID, defaultTaxCode); + setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits); + // we want this effect to run when the transaction currency changes when moving from track expense + }, [transactionID, transaction?.currency, defaultTaxCode, taxAmountInSmallestCurrencyUnits, isMovingTransactionFromTrackExpense, isReadOnly, shouldShowTax]); + // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); From 4c331ea20a081f28d4098a76163506cbbf944cf6 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 7 Jan 2026 17:02:35 +0100 Subject: [PATCH 03/12] fix: remove the fix for the currency --- src/components/MoneyRequestConfirmationList.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 453a7fcd728ea..64defbf96ce67 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -518,15 +518,6 @@ function MoneyRequestConfirmationList({ setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits); }, [transactionID, taxAmountInSmallestCurrencyUnits, isMovingTransactionFromTrackExpense, isReadOnly, shouldShowTax]); - useEffect(() => { - if (!transactionID || !isMovingTransactionFromTrackExpense || isReadOnly || !shouldShowTax) { - return; - } - setMoneyRequestTaxRate(transactionID, defaultTaxCode); - setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits); - // we want this effect to run when the transaction currency changes when moving from track expense - }, [transactionID, transaction?.currency, defaultTaxCode, taxAmountInSmallestCurrencyUnits, isMovingTransactionFromTrackExpense, isReadOnly, shouldShowTax]); - // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); From e05ff2467b82bc277323f97e838a3a92d52eaefb Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 7 Jan 2026 17:53:31 +0100 Subject: [PATCH 04/12] fix: recalculate tax amount --- src/components/MoneyRequestConfirmationList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 2de7af47d9652..71bc38b632196 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -513,11 +513,11 @@ function MoneyRequestConfirmationList({ const taxAmount = calculateTaxAmount(taxPercentage, taxableAmount, transaction?.currency ?? CONST.CURRENCY.USD); const taxAmountInSmallestCurrencyUnits = convertToBackendAmount(Number.parseFloat(taxAmount.toString())); useEffect(() => { - if (!transactionID || isMovingTransactionFromTrackExpense || isReadOnly || !shouldShowTax) { + if (!transactionID || isReadOnly || !shouldShowTax) { return; } setMoneyRequestTaxAmount(transactionID, taxAmountInSmallestCurrencyUnits); - }, [transactionID, taxAmountInSmallestCurrencyUnits, isMovingTransactionFromTrackExpense, isReadOnly, shouldShowTax]); + }, [transactionID, taxAmountInSmallestCurrencyUnits, isReadOnly, shouldShowTax]); // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { From 398f0a189cef7b9dadefcdadb7a391d2c880c757 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 12 Jan 2026 16:25:12 +0100 Subject: [PATCH 05/12] fix: change and recalculate tax rate on currency change --- .../iou/request/step/IOURequestStepAmount.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index b818278dd8dd8..012f56331eadc 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -16,7 +16,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 {isMovingTransactionFromTrackExpense, navigateToParticipantPage} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; @@ -31,6 +31,8 @@ import { setDraftSplitTransaction, setMoneyRequestAmount, setMoneyRequestParticipantsFromReport, + setMoneyRequestTaxAmount, + setMoneyRequestTaxRate, setSplitShares, trackExpense, updateMoneyRequestAmountAndCurrency, @@ -120,6 +122,8 @@ function IOURequestStepAmount({ const currentUserAccountIDParam = currentUserPersonalDetails.accountID; const currentUserEmailParam = currentUserPersonalDetails.login ?? ''; + console.log({policy}); + // For quick button actions, we'll skip the confirmation page unless the report is archived or this is a workspace request, as // the user will have to add a merchant. const shouldSkipConfirmation: boolean = useMemo(() => { @@ -174,6 +178,16 @@ function IOURequestStepAmount({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing setMoneyRequestAmount(transactionID, amountInSmallestCurrencyUnits, selectedCurrency || CONST.CURRENCY.USD, shouldKeepUserInput); + if (isMovingTransactionFromTrackExpense(action)) { + const taxCode = selectedCurrency !== policy?.outputCurrency ? policy?.taxRates?.foreignTaxDefault : policy?.taxRates?.defaultExternalID; + if (taxCode) { + setMoneyRequestTaxRate(transactionID, taxCode); + const taxPercentage = getTaxValue(policy, transaction, taxCode) ?? ''; + const taxAmount = convertToBackendAmount(calculateTaxAmount(taxPercentage, amountInSmallestCurrencyUnits, selectedCurrency || CONST.CURRENCY.USD)); + setMoneyRequestTaxAmount(transactionID, taxAmount); + } + } + if (backTo) { Navigation.goBack(backTo); return; @@ -322,6 +336,7 @@ function IOURequestStepAmount({ } if (!isEditing) { + console.log('here1'); navigateToNextPage({amount, paymentMethod}); return; } @@ -329,6 +344,7 @@ function IOURequestStepAmount({ // If the value hasn't changed, don't request to save changes on the server and just close the modal const transactionCurrency = getCurrency(currentTransaction); if (newAmount === getAmount(currentTransaction, false, false, allowNegative, disableOppositeConversion) && selectedCurrency === transactionCurrency) { + console.log('here'); navigateBack(); return; } @@ -346,6 +362,8 @@ function IOURequestStepAmount({ return; } + console.log(taxAmount, taxCode); + updateMoneyRequestAmountAndCurrency({ transactionID, transactionThreadReportID: reportID, From ab592f0c4f9e63898f353b06de1e22ae299f4872 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 19 Jan 2026 13:53:18 +0100 Subject: [PATCH 06/12] fix: add policyID to track expense params --- src/libs/API/parameters/TrackExpenseParams.ts | 1 + src/libs/actions/IOU/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 7cf42fd2e36eb..42f8b878c6783 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -19,6 +19,7 @@ type TrackExpenseParams = { reportPreviewReportActionID?: string; optimisticReportID?: string; optimisticReportActionID?: string; + policyID?: string; receipt?: Receipt; receiptState?: ValueOf; category?: string; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index ff3cd055c01f9..33200e3692032 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -6740,6 +6740,7 @@ function trackExpense(params: CreateTrackExpenseParams) { reportPreviewReportActionID: reportPreviewAction?.reportActionID, optimisticReportID, optimisticReportActionID, + policyID: policy?.id, receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined, receiptState: trackedReceipt?.state, reimbursable, From 297f468e082b4cf34f80af175f63335b8ff2195f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 19 Jan 2026 13:58:18 +0100 Subject: [PATCH 07/12] fix: prettier --- src/libs/PolicyUtils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index aa34d3163892f..f63da49eff702 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -666,7 +666,13 @@ function isCollectPolicy(policy: OnyxEntry): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM; } -function isTaxTrackingEnabled(isPolicyExpenseChatOrUnreportedExpense: boolean, policy: OnyxEntry, isDistanceRequest: boolean, isPerDiemRequest = false, isTimeRequest = false): boolean { +function isTaxTrackingEnabled( + isPolicyExpenseChatOrUnreportedExpense: boolean, + policy: OnyxEntry, + isDistanceRequest: boolean, + isPerDiemRequest = false, + isTimeRequest = false, +): boolean { if (isPerDiemRequest || isTimeRequest) { return false; } From 3c662055ca5b6f1fa0a09bae3538927c798ffd8f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 19 Jan 2026 14:29:02 +0100 Subject: [PATCH 08/12] fix: fallback to tax value when names not match --- src/components/ReportActionItem/MoneyRequestView.tsx | 4 ++-- src/libs/TransactionUtils/index.ts | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 438d548080483..3a2137e635fad 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -307,10 +307,10 @@ function MoneyRequestView({ : convertToDisplayString(Math.abs(transactionTaxAmount ?? 0), transactionCurrency); const taxRatesDescription = taxRates?.name; - const taxRateTitle = updatedTransaction ? getTaxName(policy, updatedTransaction) : getTaxName(policy, transaction); + const fallbackTaxRateTitle = transaction?.taxValue; + const taxRateTitle = updatedTransaction ? getTaxName(policy, updatedTransaction, isExpenseUnreported) : getTaxName(policy, transaction, isExpenseUnreported); const actualTransactionDate = isFromMergeTransaction && updatedTransaction ? getFormattedCreated(updatedTransaction) : transactionDate; - const fallbackTaxRateTitle = transaction?.taxValue; const isSettled = isSettledReportUtils(moneyRequestReport); const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 21dcf552ba21d..6fcbb8de4e4b6 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -2044,11 +2044,18 @@ function getWorkspaceTaxesSettingsName(policy: OnyxEntry, taxCode: strin /** * Gets the name corresponding to the taxCode that is displayed to the user */ -function getTaxName(policy: OnyxEntry, transaction: OnyxEntry) { +function getTaxName(policy: OnyxEntry, transaction: OnyxEntry, shouldFallbackToValue = false) { const defaultTaxCode = getDefaultTaxCode(policy, transaction); + // transaction?.taxCode may be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return Object.values(transformedTaxRates(policy, transaction)).find((taxRate) => taxRate.code === (transaction?.taxCode || defaultTaxCode))?.modifiedName; + const taxRate = Object.values(transformedTaxRates(policy, transaction)).find((rate) => rate.code === (transaction?.taxCode || defaultTaxCode)); + + if (shouldFallbackToValue && taxRate?.value !== transaction?.taxValue) { + return transaction?.taxValue; + } + + return taxRate?.modifiedName; } type FieldsToCompare = Record>; From 1c4c39b51c9c1b5c4d8367eb9cec1100def31c3e Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 20 Jan 2026 18:34:54 +0100 Subject: [PATCH 09/12] fix: update taxValue according to new taxRate --- src/components/ReportActionItem/MoneyRequestView.tsx | 1 - src/libs/TransactionUtils/index.ts | 4 ++++ src/libs/actions/IOU/index.ts | 6 ++++++ src/pages/iou/request/step/IOURequestStepAmount.tsx | 3 ++- src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx | 2 ++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e13554ca8fd28..2765031f0cdf2 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -312,7 +312,6 @@ function MoneyRequestView({ const actualTransactionDate = isFromMergeTransaction && updatedTransaction ? getFormattedCreated(updatedTransaction, preferredLocale) : transactionDate; const fallbackTaxRateTitle = transaction?.taxValue; - const isSettled = isSettledReportUtils(moneyRequestReport); const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU; const isChatReportArchived = useReportIsArchived(moneyRequestReport?.chatReportID); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index e87536298a68e..11033c773fe9d 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -729,6 +729,10 @@ function getUpdatedTransaction({ updatedTransaction.taxCode = transactionChanges.taxCode; } + if (Object.hasOwn(transactionChanges, 'taxValue') && typeof transactionChanges.taxCode === 'string') { + updatedTransaction.taxValue = transactionChanges.taxValue; + } + if (Object.hasOwn(transactionChanges, 'reimbursable') && typeof transactionChanges.reimbursable === 'boolean') { updatedTransaction.reimbursable = transactionChanges.reimbursable; } diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 70e6cfb9e4025..a7fa1e22d3ea7 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -5198,6 +5198,7 @@ type UpdateMoneyRequestTaxRateParams = { parentReport: OnyxEntry; taxCode: string; taxAmount: number; + taxValue: string; policy: OnyxEntry; policyTagList: OnyxEntry; policyCategories: OnyxEntry; @@ -5214,6 +5215,7 @@ function updateMoneyRequestTaxRate({ parentReport, taxCode, taxAmount, + taxValue, policy, policyTagList, policyCategories, @@ -5225,6 +5227,7 @@ function updateMoneyRequestTaxRate({ const transactionChanges = { taxCode, taxAmount, + taxValue, }; const {params, onyxData} = getUpdateMoneyRequestParams({ transactionID, @@ -8535,6 +8538,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = { policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; taxCode: string; + taxValue: string; allowNegative?: boolean; transactions: OnyxCollection; transactionViolations: OnyxCollection; @@ -8557,6 +8561,7 @@ function updateMoneyRequestAmountAndCurrency({ policyTagList, policyCategories, taxCode, + taxValue, allowNegative = false, transactions, transactionViolations, @@ -8571,6 +8576,7 @@ function updateMoneyRequestAmountAndCurrency({ currency, taxCode, taxAmount, + taxValue, }; let data: UpdateMoneyRequestData; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index cbbc9b4119ebb..2049e518ef147 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -11,8 +11,8 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePersonalPolicy from '@hooks/usePersonalPolicy'; -import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; +import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import {setTransactionReport} from '@libs/actions/Transaction'; @@ -383,6 +383,7 @@ function IOURequestStepAmount({ taxAmount, policy, taxCode, + taxValue: taxPercentage, policyCategories, currentUserAccountIDParam, currentUserEmailParam, diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 3812bb86483ea..c6a6970e54a72 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -75,6 +75,7 @@ function IOURequestStepTaxRatePage({ } const taxAmount = getTaxAmount(policy, currentTransaction, taxes.code, getAmount(currentTransaction, false, true)); + const taxValue = getTaxValue(policy, currentTransaction, taxes.code) ?? ''; if (isEditingSplitBill) { setDraftSplitTransaction(currentTransaction.transactionID, splitDraftTransaction, { @@ -92,6 +93,7 @@ function IOURequestStepTaxRatePage({ transactionThreadReport: report, parentReport, taxCode: newTaxCode, + taxValue, taxAmount: convertToBackendAmount(taxAmount ?? 0), policy, policyTagList: policyTags, From 1f6fac49991bba1a592e79a8dba21923039fe701 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 20 Jan 2026 18:55:12 +0100 Subject: [PATCH 10/12] fix: show tax rate name when created offline --- src/libs/TransactionUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 11033c773fe9d..b92dc60825981 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -2053,7 +2053,7 @@ function getTaxName(policy: OnyxEntry, transaction: OnyxEntry rate.code === (transaction?.taxCode || defaultTaxCode)); - if (shouldFallbackToValue && taxRate?.value !== transaction?.taxValue) { + if (shouldFallbackToValue && transaction?.taxValue !== undefined && taxRate?.value !== transaction?.taxValue) { return transaction?.taxValue; } From 8575e03e75484b7a824d652a6ae10a54c3e73768 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 21 Jan 2026 10:16:03 +0100 Subject: [PATCH 11/12] fix: tests --- tests/actions/IOUTest.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 01b1bbd6ba1fc..569de7dfde50c 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -5311,6 +5311,7 @@ describe('actions/IOU', () => { currency: CONST.CURRENCY.USD, taxAmount: 0, taxCode: '', + taxValue: '', policy: { id: '123', role: CONST.POLICY.ROLE.USER, @@ -7967,6 +7968,7 @@ describe('actions/IOU', () => { currency: CONST.CURRENCY.USD, taxAmount: 0, taxCode: '', + taxValue: '', policy: { id: '123', role: CONST.POLICY.ROLE.USER, @@ -8036,6 +8038,7 @@ describe('actions/IOU', () => { currency: CONST.CURRENCY.USD, taxAmount: 0, taxCode: '', + taxValue: '', policy: { id: '123', role: CONST.POLICY.ROLE.USER, @@ -8134,6 +8137,7 @@ describe('actions/IOU', () => { currency: CONST.CURRENCY.USD, taxAmount: 0, taxCode: '', + taxValue: '', policy, policyTagList: {}, policyCategories: {}, From 0778490b62856da73a10ea42e86a7d20d25d4e0f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 21 Jan 2026 10:24:53 +0100 Subject: [PATCH 12/12] fix: only check if tax rate matches when taxCode on the transaction object --- .../MoneyRequestConfirmationList.tsx | 90 +++++++------------ 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b0c673e10673d..673920f47437a 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,12 +1,12 @@ -import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import {deepEqual} from 'fast-equals'; -import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import { useFocusEffect, useIsFocused } from '@react-navigation/native'; +import { deepEqual } from 'fast-equals'; +import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { InteractionManager, View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; -import {MouseProvider} from '@hooks/useMouseContext'; +import { MouseProvider } from '@hooks/useMouseContext'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; @@ -14,76 +14,50 @@ import usePreferredPolicy from '@hooks/usePreferredPolicy'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; -import { - adjustRemainingSplitShares, - computePerDiemExpenseAmount, - isValidPerDiemExpenseAmount, - resetSplitShares, - setCustomUnitRateID, - setIndividualShare, - setMoneyRequestAmount, - setMoneyRequestCategory, - setMoneyRequestMerchant, - setMoneyRequestPendingFields, - setMoneyRequestTag, - setMoneyRequestTaxAmount, - setMoneyRequestTaxRate, - setSplitShares, -} from '@libs/actions/IOU'; -import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; -import {isCategoryDescriptionRequired} from '@libs/CategoryUtils'; -import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils'; +import { adjustRemainingSplitShares, computePerDiemExpenseAmount, isValidPerDiemExpenseAmount, resetSplitShares, setCustomUnitRateID, setIndividualShare, setMoneyRequestAmount, setMoneyRequestCategory, setMoneyRequestMerchant, setMoneyRequestPendingFields, setMoneyRequestTag, setMoneyRequestTaxAmount, setMoneyRequestTaxRate, setSplitShares } from '@libs/actions/IOU'; +import { getIsMissingAttendeesViolation } from '@libs/AttendeeUtils'; +import { isCategoryDescriptionRequired } from '@libs/CategoryUtils'; +import { convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals } from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import {calculateAmount, insertTagIntoTransactionTagsString, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils'; +import { calculateAmount, insertTagIntoTransactionTagsString, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil } from '@libs/IOUUtils'; import Log from '@libs/Log'; -import {validateAmount} from '@libs/MoneyRequestUtils'; +import { validateAmount } from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getIOUConfirmationOptionsFromPayeePersonalDetail, hasEnabledOptions} from '@libs/OptionsListUtils'; -import {getTagLists, isTaxTrackingEnabled} from '@libs/PolicyUtils'; -import {isSelectedManagerMcTest} from '@libs/ReportUtils'; -import type {OptionData} from '@libs/ReportUtils'; -import {hasEnabledTags, hasMatchingTag} from '@libs/TagsOptionsListUtils'; -import {isValidTimeExpenseAmount} from '@libs/TimeTrackingUtils'; -import { - areRequiredFieldsEmpty, - calculateTaxAmount, - getDefaultTaxCode, - getDistanceInMeters, - getRateID, - getTag, - getTaxValue, - hasMissingSmartscanFields, - hasRoute as hasRouteUtil, - isMerchantMissing, - isScanRequest as isScanRequestUtil, -} from '@libs/TransactionUtils'; -import {hasInvoicingDetails} from '@userActions/Policy/Policy'; -import type {IOUAction, IOUType} from '@src/CONST'; +import { getIOUConfirmationOptionsFromPayeePersonalDetail, hasEnabledOptions } from '@libs/OptionsListUtils'; +import { getTagLists, isTaxTrackingEnabled } from '@libs/PolicyUtils'; +import { isSelectedManagerMcTest } from '@libs/ReportUtils'; +import type { OptionData } from '@libs/ReportUtils'; +import { hasEnabledTags, hasMatchingTag } from '@libs/TagsOptionsListUtils'; +import { isValidTimeExpenseAmount } from '@libs/TimeTrackingUtils'; +import { areRequiredFieldsEmpty, calculateTaxAmount, getDefaultTaxCode, getDistanceInMeters, getRateID, getTag, getTaxValue, hasMissingSmartscanFields, hasRoute as hasRouteUtil, isMerchantMissing, isScanRequest as isScanRequestUtil } from '@libs/TransactionUtils'; +import { hasInvoicingDetails } from '@userActions/Policy/Policy'; +import type { IOUAction, IOUType } from '@src/CONST'; import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; +import type { TranslationPaths } from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {Attendee, Participant} from '@src/types/onyx/IOU'; -import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import type {SplitShares} from '@src/types/onyx/Transaction'; +import type { Attendee, Participant } from '@src/types/onyx/IOU'; +import type { PaymentMethodType } from '@src/types/onyx/OriginalMessage'; +import type { SplitShares } from '@src/types/onyx/Transaction'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import type {DropdownOption} from './ButtonWithDropdownMenu/types'; -import {DelegateNoAccessContext} from './DelegateNoAccessModalProvider'; +import type { DropdownOption } from './ButtonWithDropdownMenu/types'; +import { DelegateNoAccessContext } from './DelegateNoAccessModalProvider'; import FormHelpMessage from './FormHelpMessage'; import MoneyRequestAmountInput from './MoneyRequestAmountInput'; import MoneyRequestConfirmationListFooter from './MoneyRequestConfirmationListFooter'; -import {PressableWithFeedback} from './Pressable'; -import {useProductTrainingContext} from './ProductTrainingContext'; +import { PressableWithFeedback } from './Pressable'; +import { useProductTrainingContext } from './ProductTrainingContext'; // eslint-disable-next-line no-restricted-imports import SelectionList from './SelectionListWithSections'; -import type {SectionListDataType} from './SelectionListWithSections/types'; +import type { SectionListDataType } from './SelectionListWithSections/types'; import UserListItem from './SelectionListWithSections/UserListItem'; import SettlementButton from './SettlementButton'; import Text from './Text'; import EducationalTooltip from './Tooltip/EducationalTooltip'; + type MoneyRequestConfirmationListProps = { /** Callback to inform parent modal of success */ onConfirm?: (selectedParticipants: Participant[]) => void; @@ -965,7 +939,7 @@ function MoneyRequestConfirmationList({ return; } - if (shouldShowTax && !Object.keys(policy?.taxRates?.taxes ?? {}).some((key) => key === transaction.taxCode)) { + if (shouldShowTax && !!transaction.taxCode && !Object.keys(policy?.taxRates?.taxes ?? {}).some((key) => key === transaction.taxCode)) { setFormError('violations.taxOutOfPolicy'); return; }