From b90149809430580b12d8a31d68ac4ca756cd8c27 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Mon, 14 Sep 2026 11:51:10 +0000 Subject: [PATCH 1/4] Add getRateForPolicyChange to select a distance rate when the policy changes Co-authored-by: truph01 --- src/libs/DistanceRequestUtils.ts | 115 ++++++++++++---- tests/unit/DistanceRequestUtilsTest.ts | 176 ++++++++++++++++++++++++- 2 files changed, 263 insertions(+), 28 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index d369ce83097b..17c06964ade6 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -20,7 +20,7 @@ import {getDistanceUnitLabel, getFormattedDistanceInUnits} from './DistanceDispl import getStoredDefaultP2PMileageRate from './getStoredDefaultP2PMileageRate'; import {getDistanceRateCustomUnit, getDistanceRateCustomUnitRate, getUnitRateValue} from './PolicyUtils'; import replaceAllDigits from './replaceAllDigits'; -import {getCurrency, getRateID, isCustomUnitRateIDForP2P, isExpenseUnreported} from './TransactionUtils'; +import {getCurrency, getFormattedCreated, getRateID, isCustomUnitRateIDForP2P, isExpenseUnreported} from './TransactionUtils'; type MileageRate = { customUnitRateID?: string; @@ -569,13 +569,42 @@ function getFullyBoundedDateRangeMs(rate: MileageRate): number | undefined { } /** - * Finds the best eligible rate for a given expense date from a set of mileage rates. + * Ranks two rates that are both eligible for the same expense date. * Selection order per design doc: * 1. Most specific date range (fully bounded > partially bounded > unbounded) * 2. Narrower date range for two fully bounded ranges * 3. Latest start date * 4. Lowest index (creation order) */ +function compareRatesByDateSpecificity(a: MileageRate, b: MileageRate): number { + const aScore = getBoundednessScore(a); + const bScore = getBoundednessScore(b); + if (aScore !== bScore) { + return bScore - aScore; + } + + if (aScore === 2 && bScore === 2) { + const aRange = getFullyBoundedDateRangeMs(a); + const bRange = getFullyBoundedDateRangeMs(b); + if (aRange !== undefined && bRange !== undefined && aRange !== bRange) { + return aRange - bRange; + } + } + + const aStart = a.startDate ?? ''; + const bStart = b.startDate ?? ''; + if (aStart !== bStart) { + return aStart < bStart ? 1 : -1; + } + + const aIndex = a.index ?? CONST.DEFAULT_NUMBER_ID; + const bIndex = b.index ?? CONST.DEFAULT_NUMBER_ID; + return aIndex - bIndex; +} + +/** + * Finds the best eligible rate for a given expense date from a set of mileage rates. + */ function getBestEligibleRate(mileageRates: Record, expenseDate: string): MileageRate | undefined { const eligibleRates = Object.values(mileageRates).filter((rate) => rate.enabled !== false && isRateEligibleForDate(rate, expenseDate)); @@ -583,31 +612,7 @@ function getBestEligibleRate(mileageRates: Record, expenseD return undefined; } - eligibleRates.sort((a, b) => { - const aScore = getBoundednessScore(a); - const bScore = getBoundednessScore(b); - if (aScore !== bScore) { - return bScore - aScore; - } - - if (aScore === 2 && bScore === 2) { - const aRange = getFullyBoundedDateRangeMs(a); - const bRange = getFullyBoundedDateRangeMs(b); - if (aRange !== undefined && bRange !== undefined && aRange !== bRange) { - return aRange - bRange; - } - } - - const aStart = a.startDate ?? ''; - const bStart = b.startDate ?? ''; - if (aStart !== bStart) { - return aStart < bStart ? 1 : -1; - } - - const aIndex = a.index ?? CONST.DEFAULT_NUMBER_ID; - const bIndex = b.index ?? CONST.DEFAULT_NUMBER_ID; - return aIndex - bIndex; - }); + eligibleRates.sort(compareRatesByDateSpecificity); return eligibleRates.at(0); } @@ -621,6 +626,61 @@ function getBestEligibleRateOrPolicyDefault(mileageRates: Record, currentRate: MileageRate | undefined, expenseDate: string): MileageRate | undefined { + if (currentRate?.rate === undefined || !currentRate.currency) { + return undefined; + } + + return Object.values(mileageRates) + .filter( + (rate) => + rate.enabled !== false && + rate.rate === currentRate.rate && + rate.currency === currentRate.currency && + rate.unit === currentRate.unit && + isRateEligibleForDate(rate, expenseDate), + ) + .sort(compareRatesByDateSpecificity) + .at(0); +} + +/** + * Selects the distance rate to use for an expense that is moving to a different workspace. + * + * Selection order: + * 1. A rate on the destination policy that is equivalent to the expense's current rate. + * 2. The destination policy's best rate for the expense date, falling back to its default rate. + * 3. Nothing, when the destination policy has no usable rate. Callers keep the `customUnitOutOfPolicy` violation in that case. + * + * `currentRate` resolves against the source policy, which this module cannot look up, so callers that have it pass it in. It is + * optional only because a P2P expense carries its rate value on the transaction itself. + * + * Let's ensure this logic is consistent with the logic in the backend (Auth), which is authoritative here. The app computes the + * same answer only to build optimistic data. + */ +function getRateForPolicyChange({ + transaction, + policy, + currentRate, +}: { + transaction: OnyxEntry; + policy: OnyxEntry; + currentRate?: MileageRate; +}): MileageRate | undefined { + const expenseDate = getFormattedCreated(transaction); + const mileageRates = getMileageRates(policy); + const rateToMatch = currentRate ?? (isCustomUnitRateIDForP2P(transaction) ? getRateForP2P(getCurrency(transaction), transaction) : undefined); + + const selectedRate = getRateMatchingCurrentRate(mileageRates, rateToMatch, expenseDate) ?? getBestEligibleRateOrPolicyDefault(mileageRates, expenseDate, policy); + + // getDefaultMileageRate returns a fully shaped rate with an undefined customUnitRateID when the policy has no enabled rates, so normalize that case to undefined. + return selectedRate?.customUnitRateID ? selectedRate : undefined; +} + /** * Returns custom unit rate ID for the distance transaction. * When an expenseDate is provided, uses date-aware rate selection: @@ -933,6 +993,7 @@ export default { isRateEligibleForDate, isUnsetDistanceCustomUnitRateID, getBestEligibleRate, + getRateForPolicyChange, getRateDateLabel, }; diff --git a/tests/unit/DistanceRequestUtilsTest.ts b/tests/unit/DistanceRequestUtilsTest.ts index 2a69eed4c855..7f0bd2e4b188 100644 --- a/tests/unit/DistanceRequestUtilsTest.ts +++ b/tests/unit/DistanceRequestUtilsTest.ts @@ -4,7 +4,7 @@ import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import CONST from '@src/CONST'; import en from '@src/languages/en'; -import type {Unit} from '@src/types/onyx/Policy'; +import type {Rate, Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; import type Transaction from '@src/types/onyx/Transaction'; @@ -793,4 +793,178 @@ describe('DistanceRequestUtils', () => { expect(DistanceRequestUtils.isRateEligibleForDate(boundedRate, '2026-01-01 00:00:00')).toBe(false); }); }); + + describe('getRateForPolicyChange', () => { + const expenseDate = '2026-03-15'; + const currentRate = {customUnitRateID: 'SOURCE_RATE_ID', rate: 70, currency: 'USD', unit: distanceUnit}; + + const buildRate = (customUnitRateID: string, rate: number, overrides: Partial = {}): Rate => ({ + attributes: {}, + currency: 'USD', + customUnitRateID, + enabled: true, + name: customUnitRateID, + rate, + subRates: [], + ...overrides, + }); + + const buildDestinationPolicy = (rates: Record, unit: Unit = distanceUnit): Policy => ({ + ...FAKE_POLICY, + id: 'DESTINATION_POLICY_ID', + customUnits: { + C9031B6F4725D: { + ...distanceCustomUnitBase, + attributes: {taxEnabled: true, unit}, + rates, + }, + }, + }); + + const buildTransaction = (overrides: Partial = {}): Transaction => + ({ + ...createRandomTransaction(1), + created: expenseDate, + modifiedCreated: '', + currency: 'USD', + modifiedCurrency: '', + comment: {customUnit: {customUnitRateID: 'SOURCE_RATE_ID', distanceUnit}}, + ...overrides, + }) as Transaction; + + const defaultRate = buildRate('DEFAULT_RATE_ID', 67, {index: 0}); + + it('prefers a rate matching the current value, currency and unit over the default rate', () => { + // Given a destination policy that has a rate equivalent to the expense's current rate + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then the equivalent rate is chosen so the expense is not repriced + expect(result?.customUnitRateID).toBe('MATCHING_RATE_ID'); + }); + + it('does not match a disabled rate and falls back to the default rate', () => { + // Given the only value-matching rate on the destination policy is disabled + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1, enabled: false}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then the default rate is used instead + expect(result?.customUnitRateID).toBe('DEFAULT_RATE_ID'); + }); + + it('does not match a rate with the same value in a different currency', () => { + // Given a destination rate with the same value but a different currency + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1, currency: 'GBP'}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then it is not treated as a match + expect(result?.customUnitRateID).toBe('DEFAULT_RATE_ID'); + }); + + it('does not match any rate when the destination policy uses a different distance unit', () => { + // Given a destination policy measured in kilometers while the expense is in miles + const policy = buildDestinationPolicy( + { + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1}), + }, + CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, + ); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then the unit mismatch disqualifies the whole policy from matching and the default rate is used + expect(result?.customUnitRateID).toBe('DEFAULT_RATE_ID'); + }); + + it('does not match a value-matching rate whose date range excludes the expense date', () => { + // Given a value-matching rate that expired before the expense date + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1, startDate: '2025-01-01', endDate: '2025-12-31'}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then the default rate is used + expect(result?.customUnitRateID).toBe('DEFAULT_RATE_ID'); + }); + + it('picks the narrowest date range when several rates match the current value', () => { + // Given two value-matching rates covering the expense date, one with a narrower range + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_YEAR_RATE_ID: buildRate('MATCHING_YEAR_RATE_ID', 70, {index: 1, startDate: '2026-01-01', endDate: '2026-12-31'}), + MATCHING_H1_RATE_ID: buildRate('MATCHING_H1_RATE_ID', 70, {index: 2, startDate: '2026-01-01', endDate: '2026-06-30'}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then the narrower range wins, matching the date ranking used everywhere else + expect(result?.customUnitRateID).toBe('MATCHING_H1_RATE_ID'); + }); + + it('returns undefined when the destination policy has no enabled rate', () => { + // Given a destination policy whose only rate is disabled + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: buildRate('DEFAULT_RATE_ID', 67, {index: 0, enabled: false}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction: buildTransaction(), policy, currentRate}); + + // Then nothing is selected so the caller keeps the out of policy violation + expect(result).toBeUndefined(); + }); + + it('derives the current rate from a P2P expense when no current rate is passed', () => { + // Given a P2P expense that stores its own rate value on the transaction + const transaction = buildTransaction({ + comment: {customUnit: {customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, defaultP2PRate: 70, distanceUnit}}, + }); + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1}), + }); + + // When selecting the rate for the policy change without an explicit current rate + const result = DistanceRequestUtils.getRateForPolicyChange({transaction, policy}); + + // Then the P2P rate value is matched against the destination policy + expect(result?.customUnitRateID).toBe('MATCHING_RATE_ID'); + }); + + it('uses the modified date over the created date when checking rate eligibility', () => { + // Given an expense created in 2026 but backdated to 2025 + const transaction = buildTransaction({modifiedCreated: '2025-06-15'}); + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_2025_RATE_ID: buildRate('MATCHING_2025_RATE_ID', 70, {index: 1, startDate: '2025-01-01', endDate: '2025-12-31'}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction, policy, currentRate}); + + // Then the rate valid for the modified date is matched + expect(result?.customUnitRateID).toBe('MATCHING_2025_RATE_ID'); + }); + }); }); From 0ab82c4f1ca68f107ec9df3f6034feaf374dab87 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Mon, 14 Sep 2026 11:58:07 +0000 Subject: [PATCH 2/4] Run oxfmt on DistanceRequestUtils Co-authored-by: truph01 --- src/libs/DistanceRequestUtils.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 17c06964ade6..741389d87993 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -662,15 +662,7 @@ function getRateMatchingCurrentRate(mileageRates: Record, c * Let's ensure this logic is consistent with the logic in the backend (Auth), which is authoritative here. The app computes the * same answer only to build optimistic data. */ -function getRateForPolicyChange({ - transaction, - policy, - currentRate, -}: { - transaction: OnyxEntry; - policy: OnyxEntry; - currentRate?: MileageRate; -}): MileageRate | undefined { +function getRateForPolicyChange({transaction, policy, currentRate}: {transaction: OnyxEntry; policy: OnyxEntry; currentRate?: MileageRate}): MileageRate | undefined { const expenseDate = getFormattedCreated(transaction); const mileageRates = getMileageRates(policy); const rateToMatch = currentRate ?? (isCustomUnitRateIDForP2P(transaction) ? getRateForP2P(getCurrency(transaction), transaction) : undefined); From a21b3006df6c2c5fdf951082fae2ffe3f340fb58 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Tue, 15 Sep 2026 04:37:02 +0000 Subject: [PATCH 3/4] Address review: use the transaction unit for P2P matching and reuse getBestEligibleRate Co-authored-by: truph01 --- src/libs/DistanceRequestUtils.ts | 24 ++++++------- tests/unit/DistanceRequestUtilsTest.ts | 48 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 741389d87993..1b4772602f0a 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -583,7 +583,8 @@ function compareRatesByDateSpecificity(a: MileageRate, b: MileageRate): number { return bScore - aScore; } - if (aScore === 2 && bScore === 2) { + // Both scores are equal here, so aScore === 2 means both rates are fully bounded. + if (aScore === 2) { const aRange = getFullyBoundedDateRangeMs(a); const bRange = getFullyBoundedDateRangeMs(b); if (aRange !== undefined && bRange !== undefined && aRange !== bRange) { @@ -635,17 +636,11 @@ function getRateMatchingCurrentRate(mileageRates: Record, c return undefined; } - return Object.values(mileageRates) - .filter( - (rate) => - rate.enabled !== false && - rate.rate === currentRate.rate && - rate.currency === currentRate.currency && - rate.unit === currentRate.unit && - isRateEligibleForDate(rate, expenseDate), - ) - .sort(compareRatesByDateSpecificity) - .at(0); + const equivalentRates = Object.fromEntries( + Object.entries(mileageRates).filter(([, rate]) => rate.rate === currentRate.rate && rate.currency === currentRate.currency && rate.unit === currentRate.unit), + ); + + return getBestEligibleRate(equivalentRates, expenseDate); } /** @@ -665,7 +660,10 @@ function getRateMatchingCurrentRate(mileageRates: Record, c function getRateForPolicyChange({transaction, policy, currentRate}: {transaction: OnyxEntry; policy: OnyxEntry; currentRate?: MileageRate}): MileageRate | undefined { const expenseDate = getFormattedCreated(transaction); const mileageRates = getMileageRates(policy); - const rateToMatch = currentRate ?? (isCustomUnitRateIDForP2P(transaction) ? getRateForP2P(getCurrency(transaction), transaction) : undefined); + const p2pRate = isCustomUnitRateIDForP2P(transaction) ? getRateForP2P(getCurrency(transaction), transaction) : undefined; + // getRateForP2P reports the loaded global default's unit, not the expense's, so read the unit off the transaction the way getRate does. + // Otherwise an expense saved in kilometers is matched as miles and the fallback rate reprices it. + const rateToMatch = currentRate ?? (p2pRate ? {...p2pRate, unit: getDistanceUnit(transaction, p2pRate)} : undefined); const selectedRate = getRateMatchingCurrentRate(mileageRates, rateToMatch, expenseDate) ?? getBestEligibleRateOrPolicyDefault(mileageRates, expenseDate, policy); diff --git a/tests/unit/DistanceRequestUtilsTest.ts b/tests/unit/DistanceRequestUtilsTest.ts index 7f0bd2e4b188..0f0f9d0c27c4 100644 --- a/tests/unit/DistanceRequestUtilsTest.ts +++ b/tests/unit/DistanceRequestUtilsTest.ts @@ -1,6 +1,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import getStoredDefaultP2PMileageRate from '@libs/getStoredDefaultP2PMileageRate'; import CONST from '@src/CONST'; import en from '@src/languages/en'; @@ -11,6 +12,10 @@ import type Transaction from '@src/types/onyx/Transaction'; import createRandomTransaction from '../utils/collections/transaction'; import {translateLocal} from '../utils/TestHelper'; +// Auto-mocked so it returns undefined by default, which is the "default P2P rate not loaded yet" state the +// getRateForP2P tests below rely on. Individual tests override it when they need a loaded default. +jest.mock('@libs/getStoredDefaultP2PMileageRate'); + const customUnitRateIDWithTaxClaimablePercentage = 'FG515011039A4'; const rateWithTaxClaimablePercentage = 100; const totalDistance = 1000; @@ -834,6 +839,10 @@ describe('DistanceRequestUtils', () => { const defaultRate = buildRate('DEFAULT_RATE_ID', 67, {index: 0}); + afterEach(() => { + jest.mocked(getStoredDefaultP2PMileageRate).mockReset(); + }); + it('prefers a rate matching the current value, currency and unit over the default rate', () => { // Given a destination policy that has a rate equivalent to the expense's current rate const policy = buildDestinationPolicy({ @@ -952,6 +961,45 @@ describe('DistanceRequestUtils', () => { expect(result?.customUnitRateID).toBe('MATCHING_RATE_ID'); }); + it('matches a P2P expense against the unit saved on the transaction, not the loaded global default unit', () => { + // Given the global default P2P rate is loaded in miles while the expense itself was saved in kilometers + jest.mocked(getStoredDefaultP2PMileageRate).mockReturnValue({rate: 67, unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}); + const transaction = buildTransaction({ + comment: {customUnit: {customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, defaultP2PRate: 70, distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS}}, + }); + const policy = buildDestinationPolicy( + { + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1}), + }, + CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, + ); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction, policy}); + + // Then the kilometer rate still matches, so the expense is not repriced by the fallback rate + expect(result?.customUnitRateID).toBe('MATCHING_RATE_ID'); + }); + + it('does not match a P2P expense against a destination policy whose unit differs from the transaction unit', () => { + // Given the global default P2P rate is loaded in miles and the destination policy is in miles, but the expense was saved in kilometers + jest.mocked(getStoredDefaultP2PMileageRate).mockReturnValue({rate: 67, unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}); + const transaction = buildTransaction({ + comment: {customUnit: {customUnitRateID: CONST.CUSTOM_UNITS.FAKE_P2P_ID, defaultP2PRate: 70, distanceUnit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS}}, + }); + const policy = buildDestinationPolicy({ + DEFAULT_RATE_ID: defaultRate, + MATCHING_RATE_ID: buildRate('MATCHING_RATE_ID', 70, {index: 1}), + }); + + // When selecting the rate for the policy change + const result = DistanceRequestUtils.getRateForPolicyChange({transaction, policy}); + + // Then the unit mismatch disqualifies the match and the destination's default rate is used + expect(result?.customUnitRateID).toBe('DEFAULT_RATE_ID'); + }); + it('uses the modified date over the created date when checking rate eligibility', () => { // Given an expense created in 2026 but backdated to 2025 const transaction = buildTransaction({modifiedCreated: '2025-06-15'}); From 40500a2aa8c532ec61bbeb0daefd372296c366b0 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Tue, 15 Sep 2026 05:27:01 +0000 Subject: [PATCH 4/4] Shorten the getRateForPolicyChange doc comment Co-authored-by: truph01 --- src/libs/DistanceRequestUtils.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 1b4772602f0a..935f2bb730d1 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -644,18 +644,13 @@ function getRateMatchingCurrentRate(mileageRates: Record, c } /** - * Selects the distance rate to use for an expense that is moving to a different workspace. + * Selects the distance rate for an expense moving to a different workspace: an equivalent rate on the destination policy, else its + * best rate for the expense date, else its default rate, else nothing so the caller keeps the `customUnitOutOfPolicy` violation. * - * Selection order: - * 1. A rate on the destination policy that is equivalent to the expense's current rate. - * 2. The destination policy's best rate for the expense date, falling back to its default rate. - * 3. Nothing, when the destination policy has no usable rate. Callers keep the `customUnitOutOfPolicy` violation in that case. + * `currentRate` resolves against the source policy, which this module cannot look up, so callers pass it in. It is optional because + * a P2P expense carries its rate on the transaction. * - * `currentRate` resolves against the source policy, which this module cannot look up, so callers that have it pass it in. It is - * optional only because a P2P expense carries its rate value on the transaction itself. - * - * Let's ensure this logic is consistent with the logic in the backend (Auth), which is authoritative here. The app computes the - * same answer only to build optimistic data. + * Let's ensure this logic is consistent with the logic in the backend (Auth), which is authoritative here. */ function getRateForPolicyChange({transaction, policy, currentRate}: {transaction: OnyxEntry; policy: OnyxEntry; currentRate?: MileageRate}): MileageRate | undefined { const expenseDate = getFormattedCreated(transaction);