diff --git a/src/libs/API/parameters/ChangeTransactionsReportParams.ts b/src/libs/API/parameters/ChangeTransactionsReportParams.ts index deae34bec13a..b67f284a549a 100644 --- a/src/libs/API/parameters/ChangeTransactionsReportParams.ts +++ b/src/libs/API/parameters/ChangeTransactionsReportParams.ts @@ -19,7 +19,6 @@ type ChangeTransactionsReportParams = { transactionList: string; reportID: string; transactionIDToReportActionAndThreadData: string; // A map of transactionID to TransactionThreadInfo - transactionIDToUpdatedCustomUnitRateID?: string; // A JSON map of transactionID to the new customUnitRateID (for distance expenses moving to a workspace with an invalid rate) }; export type {ChangeTransactionsReportParams, TransactionThreadInfo}; diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 1c71a896636a..ca762b38ab55 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -467,6 +467,7 @@ const ViolationsUtils = { hasDependentTags, isInvoiceTransaction, isSelfDM, + isTransactionOnPolicyExpenseChat, iouReport, isFromExpenseReport, shouldRemoveRejectedExpenseViolation, @@ -481,6 +482,7 @@ const ViolationsUtils = { hasDependentTags: boolean; isInvoiceTransaction: boolean; isSelfDM?: boolean; + isTransactionOnPolicyExpenseChat?: boolean; iouReport?: OnyxEntry; isFromExpenseReport?: boolean; shouldRemoveRejectedExpenseViolation?: boolean; @@ -652,8 +654,8 @@ const ViolationsUtils = { // arrives). We must NOT clear it when the transaction is still bound to a policy expense chat, because a // track expense moved onto a workspace intentionally keeps FAKE_P2P_ID until the user picks a workspace // rate, and the violation is what prompts them to do so — so that case falls through to the rate check below. - const isTransactionOnPolicyExpenseChat = updatedTransaction.participants?.some((participant) => participant?.isPolicyExpenseChat); - if (TransactionUtils.isCustomUnitRateIDForP2P(updatedTransaction) && !isTransactionOnPolicyExpenseChat) { + const isOnPolicyExpenseChat = isTransactionOnPolicyExpenseChat ?? updatedTransaction.participants?.some((participant) => participant?.isPolicyExpenseChat); + if (TransactionUtils.isCustomUnitRateIDForP2P(updatedTransaction) && !isOnPolicyExpenseChat) { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY}); } else { const isPerDiem = TransactionUtils.isPerDiemRequest(updatedTransaction); @@ -664,7 +666,8 @@ const ViolationsUtils = { } const customRate = isPerDiem ? getPerDiemRateCustomUnitRate(policy, customUnitRateID) : getDistanceRateCustomUnitRate(policyForCustomUnitRate, customUnitRateID); - if (customRate && customRate.enabled !== false) { + const isRatePendingDeletion = customRate?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + if (customRate && !isRatePendingDeletion) { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY}); newTransactionViolations = syncCustomUnitRateOutOfDateRangeViolation(newTransactionViolations, updatedTransaction, policyForCustomUnitRate); } else if (isSelfDM && isDistanceRequestForCustomUnit) { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 085a12a4140d..ce44aaedc22b 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -10,15 +10,12 @@ import type { TransactionThreadInfo, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; -import {getCurrencySymbol} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import {toLocaleDigit} from '@libs/LocaleDigitUtils'; -import {translateLocal} from '@libs/Localize'; import {buildOptimisticNextStep} from '@libs/NextStepUtils'; import * as NumberUtils from '@libs/NumberUtils'; import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils'; -import {getDistanceRateCustomUnitRate, hasDependentTags, isGroupPolicy} from '@libs/PolicyUtils'; +import {hasDependentTags, isGroupPolicy} from '@libs/PolicyUtils'; import { getAllReportActions, getIOUActionForReportID, @@ -48,15 +45,10 @@ import { shouldEnableNegative, } from '@libs/ReportUtils'; import { - getDistanceInMeters, hasPendingRTERViolation, hasSubmissionBlockingViolationInList, isDeletedTransaction, - isDistanceRequest, - isFetchingWaypointsFromServer, isManagedCardTransaction, - isManualDistanceRequest, - isOdometerDistanceRequest, isOnHold, isSplitContainerTransaction, shouldClearConvertedAmount, @@ -65,7 +57,6 @@ import { import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; -import IntlStore from '@src/languages/IntlStore'; import ONYXKEYS from '@src/ONYXKEYS'; import type { PersonalDetails, @@ -876,7 +867,6 @@ function getChangeTransactionsReportOnyxData({ reports, skippedReportIDs, isTrackIntentUser, - personalPolicyOutputCurrency, selfDMReportActions, delegateAccountID, getCurrencyDecimals, @@ -1091,7 +1081,6 @@ function getChangeTransactionsReportOnyxData({ }; // Track distance rate updates so we can send them to the backend - const transactionIDToUpdatedCustomUnitRateID: Record = {}; for (const transaction of transactions) { const isDeletedExpense = isDeletedTransaction(transaction); @@ -1257,118 +1246,18 @@ function getChangeTransactionsReportOnyxData({ }); } - // Auto-select a valid default distance rate when moving to a workspace where the current rate is invalid, - // and recalculate derived fields (amount, merchant, currency) to match the new rate. - let transactionForViolations = transaction; - if (isGroupPolicy(policy) && policy?.id && isDistanceRequest(transaction)) { - const currentRateID = transaction.comment?.customUnit?.customUnitRateID; - const currentRate = currentRateID ? getDistanceRateCustomUnitRate(policy, currentRateID) : undefined; - if (!currentRateID || !currentRate || currentRate.enabled === false) { - const defaultRate = DistanceRequestUtils.getDefaultMileageRate(policy); - if (defaultRate?.customUnitRateID) { - transactionIDToUpdatedCustomUnitRateID[transaction.transactionID] = defaultRate.customUnitRateID; - // Build an updated transaction with the new rate so we can derive fields from it - const updatedTransaction: typeof transaction = { - ...transaction, - comment: { - ...transaction.comment, - customUnit: { - ...transaction.comment?.customUnit, - customUnitRateID: defaultRate.customUnitRateID, - defaultP2PRate: undefined, - }, - }, - }; - - // Update distanceUnit if the new rate has a different unit, and convert distance if needed - const existingDistanceUnit = transaction.comment?.customUnit?.distanceUnit; - const newDistanceUnit = DistanceRequestUtils.getUpdatedDistanceUnit({transaction: updatedTransaction, policy}); - if (updatedTransaction.comment?.customUnit) { - updatedTransaction.comment.customUnit.distanceUnit = newDistanceUnit; - } - if (existingDistanceUnit && newDistanceUnit !== existingDistanceUnit && !isOdometerDistanceRequest(transaction)) { - const conversionFactor = - existingDistanceUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? CONST.CUSTOM_UNITS.MILES_TO_KILOMETERS : CONST.CUSTOM_UNITS.KILOMETERS_TO_MILES; - const distance = roundToTwoDecimalPlaces((transaction.comment?.customUnit?.quantity ?? 0) * conversionFactor); - if (updatedTransaction.comment?.customUnit) { - updatedTransaction.comment.customUnit.quantity = distance; - } - } - - // Recalculate amount, merchant, and currency from the new rate - const optimisticValue: Partial = { - comment: updatedTransaction.comment, - }; - - if (!isFetchingWaypointsFromServer(transaction)) { - const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false, personalPolicyOutputCurrency}); - const {unit, rate} = updatedMileageRate; - const distanceInMeters = getDistanceInMeters(updatedTransaction, unit); - const calculatedAmount = DistanceRequestUtils.getDistanceRequestAmount(distanceInMeters, unit, rate ?? 0); - const shouldNegateAmount = isExpenseReport(newReport); - const updatedAmount = shouldNegateAmount ? -calculatedAmount : calculatedAmount; - const updatedCurrency = updatedMileageRate.currency ?? CONST.CURRENCY.USD; - const updatedMerchant = DistanceRequestUtils.getDistanceMerchant( - true, - distanceInMeters, - unit, - rate, - updatedCurrency, - // eslint-disable-next-line @typescript-eslint/no-deprecated - translateLocal, - (digit) => toLocaleDigit(IntlStore.getCurrentLocale(), digit), - getCurrencySymbol, - isManualDistanceRequest(transaction), - ); - - optimisticValue.amount = updatedAmount; - optimisticValue.modifiedAmount = updatedAmount; - optimisticValue.modifiedMerchant = updatedMerchant; - optimisticValue.modifiedCurrency = updatedCurrency; - } - - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - value: optimisticValue, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - value: { - comment: { - customUnit: { - customUnitRateID: currentRateID ?? null, - defaultP2PRate: transaction.comment?.customUnit?.defaultP2PRate, - distanceUnit: existingDistanceUnit, - quantity: transaction.comment?.customUnit?.quantity, - }, - }, - amount: transaction.amount, - modifiedAmount: transaction.modifiedAmount, - modifiedMerchant: transaction.modifiedMerchant, - modifiedCurrency: transaction.modifiedCurrency, - }, - }); - transactionForViolations = { - ...updatedTransaction, - ...optimisticValue, - }; - } - } - } - let transactionReimbursable = transaction.reimbursable; // 2. Calculate transaction violations if moving transaction to a workspace if (isGroupPolicy(policy) && policy?.id) { const violationData = ViolationsUtils.getViolationsOnyxData({ - updatedTransaction: transactionForViolations, + updatedTransaction: transaction, transactionViolations: currentTransactionViolations[transaction.transactionID] ?? [], policy, policyTagList: policyTagList ?? {}, policyCategories: policyCategories ?? {}, hasDependentTags: policyHasDependentTags, isInvoiceTransaction: false, + isTransactionOnPolicyExpenseChat: !isUnreported && isExpenseReport(newReport), shouldRemoveRejectedExpenseViolation: true, ownerLogin: undefined, }); @@ -1403,12 +1292,9 @@ function getChangeTransactionsReportOnyxData({ const allowNegative = shouldEnableNegative(newReport); // 3. Keep track of the new report totals - // Source report uses original transaction details (expense is being removed at its original amount) - // Target report uses transactionForViolations (expense arrives with the updated rate/amount after auto-selecting workspace rate) const targetReportID = isUnreported ? selfDMReportID : reportID; - const {amount: sourceTransactionAmount = 0, currency: sourceTransactionCurrency} = getTransactionDetails(transaction, undefined, undefined, allowNegative) ?? {}; - const {amount: targetTransactionAmount = 0, currency: targetTransactionCurrency} = getTransactionDetails(transactionForViolations, undefined, undefined, allowNegative) ?? {}; - const resolvedTargetTransactionCurrency = targetTransactionCurrency ?? transaction.currency; + const {amount: transactionAmount = 0, currency: transactionCurrency} = getTransactionDetails(transaction, undefined, undefined, allowNegative) ?? {}; + const resolvedTransactionCurrency = transactionCurrency ?? transaction.currency; const oldReportTotal = oldReport?.total ?? 0; if (oldReport) { @@ -1426,20 +1312,19 @@ function getChangeTransactionsReportOnyxData({ updatedReportStatusNums[oldReportID] = CONST.REPORT.STATUS_NUM.OPEN; } else if (staleReportIDs.has(oldReportID) || isReportTotalPending(oldReport)) { markReportTotalAsStale(oldReportID); - } else if (oldReport.currency === sourceTransactionCurrency) { + } else if (oldReport.currency === transactionCurrency) { const currentTotal = updatedReportTotals[oldReportID] ?? oldReportTotal; - updatedReportTotals[oldReportID] = currentTotal + sourceTransactionAmount; + updatedReportTotals[oldReportID] = currentTotal + transactionAmount; const currentNonReimbursableTotal = updatedReportNonReimbursableTotals[oldReportID] ?? oldReport?.nonReimbursableTotal ?? 0; - updatedReportNonReimbursableTotals[oldReportID] = currentNonReimbursableTotal + (transaction?.reimbursable ? 0 : sourceTransactionAmount); + updatedReportNonReimbursableTotals[oldReportID] = currentNonReimbursableTotal + (transaction?.reimbursable ? 0 : transactionAmount); const currentUnheldNonReimbursableTotal = updatedReportUnheldNonReimbursableTotals[oldReportID] ?? oldReport?.unheldNonReimbursableTotal ?? 0; - updatedReportUnheldNonReimbursableTotals[oldReportID] = - currentUnheldNonReimbursableTotal + (transaction?.reimbursable && !isOnHold(transaction) ? 0 : sourceTransactionAmount); + updatedReportUnheldNonReimbursableTotals[oldReportID] = currentUnheldNonReimbursableTotal + (transaction?.reimbursable && !isOnHold(transaction) ? 0 : transactionAmount); const currentReimbursableTotal = updatedReportReimbursableTotals[oldReportID] ?? getReimbursableTotal(oldReport); - updatedReportReimbursableTotals[oldReportID] = currentReimbursableTotal + (transaction?.reimbursable ? sourceTransactionAmount : 0); + updatedReportReimbursableTotals[oldReportID] = currentReimbursableTotal + (transaction?.reimbursable ? transactionAmount : 0); const currentUnheldReimbursableTotal = updatedReportUnheldReimbursableTotals[oldReportID] ?? getUnheldReimbursableTotal(oldReport); - updatedReportUnheldReimbursableTotals[oldReportID] = currentUnheldReimbursableTotal + (transaction?.reimbursable && !isOnHold(transaction) ? sourceTransactionAmount : 0); + updatedReportUnheldReimbursableTotals[oldReportID] = currentUnheldReimbursableTotal + (transaction?.reimbursable && !isOnHold(transaction) ? transactionAmount : 0); } else { markReportTotalAsStale(oldReportID); } @@ -1453,27 +1338,26 @@ function getChangeTransactionsReportOnyxData({ const targetReportTransactionCount = updatedReportTransactionCounts[targetReportID] ?? targetReport?.transactionCount ?? 0; updatedReportTransactionCounts[targetReportID] = targetReportTransactionCount + 1; - if (staleReportIDs.has(targetReportID) || isReportTotalPending(targetReport) || new Set([...targetReportCurrencies, resolvedTargetTransactionCurrency]).size > 1) { + if (staleReportIDs.has(targetReportID) || isReportTotalPending(targetReport) || new Set([...targetReportCurrencies, resolvedTransactionCurrency]).size > 1) { markReportTotalAsStale(targetReportID); - } else if (targetTransactionCurrency === targetReport?.currency) { + } else if (transactionCurrency === targetReport?.currency) { const currentTotal = updatedReportTotals[targetReportID] ?? targetReport?.total ?? 0; - updatedReportTotals[targetReportID] = currentTotal - targetTransactionAmount; + updatedReportTotals[targetReportID] = currentTotal - transactionAmount; const currentNonReimbursableTotal = updatedReportNonReimbursableTotals[targetReportID] ?? targetReport?.nonReimbursableTotal ?? 0; - updatedReportNonReimbursableTotals[targetReportID] = currentNonReimbursableTotal - (transactionReimbursable ? 0 : targetTransactionAmount); + updatedReportNonReimbursableTotals[targetReportID] = currentNonReimbursableTotal - (transactionReimbursable ? 0 : transactionAmount); const currentUnheldNonReimbursableTotal = updatedReportUnheldNonReimbursableTotals[targetReportID] ?? targetReport?.unheldNonReimbursableTotal ?? 0; - updatedReportUnheldNonReimbursableTotals[targetReportID] = - currentUnheldNonReimbursableTotal - (transactionReimbursable && !isOnHold(transaction) ? 0 : targetTransactionAmount); + updatedReportUnheldNonReimbursableTotals[targetReportID] = currentUnheldNonReimbursableTotal - (transactionReimbursable && !isOnHold(transaction) ? 0 : transactionAmount); const currentReimbursableTotal = updatedReportReimbursableTotals[targetReportID] ?? getReimbursableTotal(targetReport); - updatedReportReimbursableTotals[targetReportID] = currentReimbursableTotal - (transactionReimbursable ? targetTransactionAmount : 0); + updatedReportReimbursableTotals[targetReportID] = currentReimbursableTotal - (transactionReimbursable ? transactionAmount : 0); const currentUnheldReimbursableTotal = updatedReportUnheldReimbursableTotals[targetReportID] ?? getUnheldReimbursableTotal(targetReport); - updatedReportUnheldReimbursableTotals[targetReportID] = currentUnheldReimbursableTotal - (transactionReimbursable && !isOnHold(transaction) ? targetTransactionAmount : 0); - } else if (transactionForViolations.convertedAmount && oldReport?.currency === targetReport?.currency) { + updatedReportUnheldReimbursableTotals[targetReportID] = currentUnheldReimbursableTotal - (transactionReimbursable && !isOnHold(transaction) ? transactionAmount : 0); + } else if (transaction.convertedAmount && oldReport?.currency === targetReport?.currency) { // Use convertedAmount when transaction currency differs but workspace currency is the same - const {convertedAmount} = transactionForViolations; + const {convertedAmount} = transaction; const currentTotal = updatedReportTotals[targetReportID] ?? targetReport?.total ?? 0; updatedReportTotals[targetReportID] = currentTotal + convertedAmount; @@ -1492,7 +1376,7 @@ function getChangeTransactionsReportOnyxData({ markReportTotalAsStale(targetReportID); } - targetReportCurrencies.add(resolvedTargetTransactionCurrency); + targetReportCurrencies.add(resolvedTransactionCurrency); } // 4. Optimistically update the IOU action reportID @@ -1988,7 +1872,6 @@ function getChangeTransactionsReportOnyxData({ successData, failureData, transactionIDToReportActionAndThreadData, - transactionIDToUpdatedCustomUnitRateID, updatedReportTotals, updatedReportTransactionCounts, updatedReportNonReimbursableTotals, @@ -2004,8 +1887,7 @@ function changeTransactionsReport(props: ChangeTransactionsReportProps) { if (!changeTransactionsReportOnyxData) { return; } - const {optimisticData, successData, failureData, transactionIDToReportActionAndThreadData, transactionIDToUpdatedCustomUnitRateID, movedTransactionIDs} = - changeTransactionsReportOnyxData; + const {optimisticData, successData, failureData, transactionIDToReportActionAndThreadData, movedTransactionIDs} = changeTransactionsReportOnyxData; // If every selected transaction is already in the destination report, there is nothing to move, so skip the API call. if (movedTransactionIDs.length === 0) { @@ -2018,9 +1900,6 @@ function changeTransactionsReport(props: ChangeTransactionsReportProps) { transactionList: movedTransactionIDs.join(','), reportID, transactionIDToReportActionAndThreadData: JSON.stringify(transactionIDToReportActionAndThreadData), - ...(Object.keys(transactionIDToUpdatedCustomUnitRateID).length > 0 && { - transactionIDToUpdatedCustomUnitRateID: JSON.stringify(transactionIDToUpdatedCustomUnitRateID), - }), }; API.write(WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT, parameters, { diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index 6a01f5b3cf9c..c25820189a95 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -1750,250 +1750,6 @@ describe('Transaction', () => { expect(updatedViolations?.some((violation) => violation.name === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE)).toBe(false); }); - it('should auto-select a valid distance rate when moving a distance expense with an invalid P2P rate to a workspace', async () => { - const policyID = '100'; - const validRateID = 'valid_rate_1'; - const transaction = generateTransaction({ - reportID: FAKE_OLD_REPORT_ID, - amount: -500, - currency: 'USD', - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, - comment: { - type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, - customUnit: { - name: CONST.CUSTOM_UNITS.NAME_DISTANCE, - customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, - distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - quantity: 10, - }, - }, - }); - const oldIOUAction = createIOUAction(transaction); - - const newExpenseReport = { - ...createExpenseReport(Number(FAKE_NEW_REPORT_ID)), - reportID: FAKE_NEW_REPORT_ID, - policyID, - ownerAccountID: CURRENT_USER_ID, - currency: 'USD', - }; - const policy = { - ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), - id: policyID, - outputCurrency: 'USD', - customUnits: { - distanceUnit: { - attributes: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, - customUnitID: 'distanceUnit', - defaultCategory: '', - enabled: true, - name: CONST.CUSTOM_UNITS.NAME_DISTANCE, - rates: { - [validRateID]: { - currency: 'USD', - customUnitRateID: validRateID, - enabled: true, - name: 'Default Rate', - rate: 6550, - }, - }, - }, - }, - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${FAKE_NEW_REPORT_ID}`, newExpenseReport); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction}); - - const allTransactions = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - - changeTransactionsReport({ - transactionIDs: [transaction.transactionID], - isASAPSubmitBetaEnabled: false, - accountID: CURRENT_USER_ID, - email: 'test@example.com', - newReport: newExpenseReport, - policy, - allTransactions, - policyTagList: undefined, - reports, - transactionViolations: {}, - isTrackIntentUser: false, - }); - await waitForBatchedUpdates(); - - const updatedTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`); - expect(updatedTransaction?.comment?.customUnit?.customUnitRateID).toBe(validRateID); - expect(updatedTransaction?.comment?.customUnit?.defaultP2PRate).toBeUndefined(); - }); - - it('should auto-select a valid distance rate when the current rate is disabled on the destination workspace', async () => { - const policyID = '101'; - const disabledRateID = 'disabled_rate'; - const enabledRateID = 'enabled_rate'; - const transaction = generateTransaction({ - reportID: FAKE_OLD_REPORT_ID, - amount: -500, - currency: 'USD', - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, - comment: { - type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, - customUnit: { - name: CONST.CUSTOM_UNITS.NAME_DISTANCE, - customUnitRateID: disabledRateID, - distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - quantity: 10, - }, - }, - }); - const oldIOUAction = createIOUAction(transaction); - - const newExpenseReport = { - ...createExpenseReport(Number(FAKE_NEW_REPORT_ID)), - reportID: FAKE_NEW_REPORT_ID, - policyID, - ownerAccountID: CURRENT_USER_ID, - currency: 'USD', - }; - const policy = { - ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), - id: policyID, - outputCurrency: 'USD', - customUnits: { - distanceUnit: { - attributes: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, - customUnitID: 'distanceUnit', - defaultCategory: '', - enabled: true, - name: CONST.CUSTOM_UNITS.NAME_DISTANCE, - rates: { - [disabledRateID]: { - currency: 'USD', - customUnitRateID: disabledRateID, - enabled: false, - name: 'Old Rate', - rate: 5000, - }, - [enabledRateID]: { - currency: 'USD', - customUnitRateID: enabledRateID, - enabled: true, - name: 'Default Rate', - rate: 6550, - }, - }, - }, - }, - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${FAKE_NEW_REPORT_ID}`, newExpenseReport); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction}); - - const allTransactions = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - - changeTransactionsReport({ - transactionIDs: [transaction.transactionID], - isASAPSubmitBetaEnabled: false, - accountID: CURRENT_USER_ID, - email: 'test@example.com', - newReport: newExpenseReport, - policy, - allTransactions, - policyTagList: undefined, - reports, - transactionViolations: {}, - isTrackIntentUser: false, - }); - await waitForBatchedUpdates(); - - const updatedTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`); - expect(updatedTransaction?.comment?.customUnit?.customUnitRateID).toBe(enabledRateID); - }); - - it('should not generate CUSTOM_UNIT_OUT_OF_POLICY violation when auto-selecting a valid rate during move', async () => { - const policyID = '102'; - const validRateID = 'workspace_rate'; - const transaction = generateTransaction({ - reportID: FAKE_OLD_REPORT_ID, - amount: -500, - currency: 'USD', - iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, - comment: { - type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, - customUnit: { - name: CONST.CUSTOM_UNITS.NAME_DISTANCE, - customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, - distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - quantity: 10, - }, - }, - }); - const oldIOUAction = createIOUAction(transaction); - - const newExpenseReport = { - ...createExpenseReport(Number(FAKE_NEW_REPORT_ID)), - reportID: FAKE_NEW_REPORT_ID, - policyID, - ownerAccountID: CURRENT_USER_ID, - currency: 'USD', - }; - const policy = { - ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), - id: policyID, - outputCurrency: 'USD', - customUnits: { - distanceUnit: { - attributes: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}, - customUnitID: 'distanceUnit', - defaultCategory: '', - enabled: true, - name: CONST.CUSTOM_UNITS.NAME_DISTANCE, - rates: { - [validRateID]: { - currency: 'USD', - customUnitRateID: validRateID, - enabled: true, - name: 'Default Rate', - rate: 6550, - }, - }, - }, - }, - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${FAKE_NEW_REPORT_ID}`, newExpenseReport); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction}); - - const allTransactions = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, - }; - - changeTransactionsReport({ - transactionIDs: [transaction.transactionID], - isASAPSubmitBetaEnabled: false, - accountID: CURRENT_USER_ID, - email: 'test@example.com', - newReport: newExpenseReport, - policy, - allTransactions, - policyTagList: undefined, - reports, - transactionViolations: {}, - isTrackIntentUser: false, - }); - await waitForBatchedUpdates(); - - const updatedViolations = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`); - const customUnitViolations = updatedViolations?.filter((v) => v.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY) ?? []; - expect(customUnitViolations).toHaveLength(0); - }); - it('should not change the rate when moving a distance expense with a valid rate to a workspace', async () => { const policyID = '103'; const validRateID = 'already_valid_rate'; diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index c9a4e8290737..6c79c53050ed 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -254,7 +254,7 @@ describe('getViolationsOnyxData', () => { expect(result.value).not.toContainEqual(customUnitOutOfPolicyViolation); }); - it('should keep the customUnitOutOfPolicy violation if the rate exists but is disabled', () => { + it('should clear the customUnitOutOfPolicy violation if the rate exists but is disabled', () => { const customUnitRateID = 'rate_id'; policy.customUnits = { unitId: { @@ -285,6 +285,41 @@ describe('getViolationsOnyxData', () => { isInvoiceTransaction: false, }); + expect(result.value).not.toContainEqual(expect.objectContaining({name: CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY})); + }); + + it('should keep the customUnitOutOfPolicy violation if the rate is pending deletion', () => { + const customUnitRateID = 'rate_id'; + policy.customUnits = { + unitId: { + attributes: {unit: 'mi'}, + customUnitID: 'unitId', + defaultCategory: 'Car', + enabled: true, + name: 'Distance', + rates: { + [customUnitRateID]: { + currency: 'USD', + customUnitRateID, + enabled: false, + name: 'Default Rate', + rate: 65.5, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + }, + }; + const result = ViolationsUtils.getViolationsOnyxData({ + ownerLogin: undefined, + updatedTransaction: transaction, + transactionViolations, + policy, + policyTagList: policyTags, + policyCategories, + hasDependentTags: false, + isInvoiceTransaction: false, + }); + expect(result.value).toContainEqual(expect.objectContaining({name: CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY})); }); }); @@ -446,6 +481,7 @@ describe('getViolationsOnyxData', () => { currency: 'USD', customUnitRateID, enabled: false, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, name: '2025 mileage', rate: 65.5, startDate: '2025-01-01',