Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7d22b03
Merge the first bunch of fixes
ZhenjaHorbach May 28, 2026
0600e38
Fix conflicts
ZhenjaHorbach May 29, 2026
a88c95a
Add fix for 91907
ZhenjaHorbach May 29, 2026
56fc7d1
Add fix for 91920
ZhenjaHorbach May 29, 2026
db9ac60
Fix conflicts
ZhenjaHorbach Jun 4, 2026
b4a8331
Add fix for 91917
ZhenjaHorbach Jun 4, 2026
b865cad
Fix conflicts
ZhenjaHorbach Jun 19, 2026
12d104c
Fix conflicts
ZhenjaHorbach Jun 22, 2026
f5a75fb
Refactor code
ZhenjaHorbach Jun 22, 2026
7477e46
Refactor code x2
ZhenjaHorbach Jun 22, 2026
57e546e
Refactor code x3
ZhenjaHorbach Jun 22, 2026
8378193
Fix comments with new selector for hasWorkspaceToSubmitTo and policy …
ZhenjaHorbach Jun 22, 2026
ac85f9f
Fix comments
ZhenjaHorbach Jun 25, 2026
b13de09
Fix conflicts
ZhenjaHorbach Jun 25, 2026
61aa7c9
Fix comments, new bugs and improve fix for 91917
ZhenjaHorbach Jun 26, 2026
eb19860
Improve logic for selfdm rates
ZhenjaHorbach Jun 29, 2026
1e8d7c7
Improve logic for rates and add shouldSelectPolicy
ZhenjaHorbach Jun 30, 2026
4bc6e57
Fix bug with zero amount after creating worksapce in selfDM splits flow
ZhenjaHorbach Jun 30, 2026
5be0b7e
Fix comments and implement clearErrorWithOriginalTransaction
ZhenjaHorbach Jun 30, 2026
3d1fb50
Rename clearErrorWithOriginalTransactionError
ZhenjaHorbach Jun 30, 2026
464842f
Fix lint issues
ZhenjaHorbach Jun 30, 2026
2eff98f
Fix comments
ZhenjaHorbach Jul 1, 2026
95d5a44
Fix conflicts
ZhenjaHorbach Jul 2, 2026
0398f5e
Fix comments
ZhenjaHorbach Jul 2, 2026
4decb7b
Fix conflicts
ZhenjaHorbach Jul 3, 2026
20e4e35
Fix fmt issues
ZhenjaHorbach Jul 3, 2026
7ed9083
Fix issues from review comments
ZhenjaHorbach Jul 6, 2026
1214e45
Fix spellcheck issues
ZhenjaHorbach Jul 6, 2026
2965da0
Fix conflicts
ZhenjaHorbach Jul 10, 2026
627c7e8
Fix TS issues
ZhenjaHorbach Jul 10, 2026
d4685ed
Fix conflicts
ZhenjaHorbach Jul 14, 2026
eeca781
Fix lint issues
ZhenjaHorbach Jul 14, 2026
61ebdd4
Merge branch 'main' into fix-bugs-with-selfDM-splits
ZhenjaHorbach Jul 14, 2026
e0c425d
Fix conflicts
ZhenjaHorbach Jul 20, 2026
7b1ab32
Fix lint issues
ZhenjaHorbach Jul 20, 2026
87ae568
Fix conflicts
ZhenjaHorbach Jul 21, 2026
c6d9b7e
Fix conflicts
ZhenjaHorbach Jul 22, 2026
e9cba9c
Fix comments
ZhenjaHorbach Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1798,7 +1798,7 @@
"../../tests/actions/IOUTest/ReportWorkflowTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 62
"../../tests/actions/IOUTest/SendInvoiceTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../tests/actions/IOUTest/SplitSelfDMTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/actions/IOUTest/SplitTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 20
"../../tests/actions/IOUTest/SplitTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 21
"../../tests/actions/IOUTest/TrackExpenseTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../tests/actions/IOUTest/TrackExpenseTest.ts" "no-restricted-imports" 1
"../../tests/actions/IOUTest/UpdateMoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
Expand Down
13 changes: 10 additions & 3 deletions src/hooks/useDeleteTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getChildTransactions,
getOriginalTransactionWithSplitInfo,
isPerDiemRequest as isPerDiemRequestTransactionUtils,
isSplitChildTransaction,
shouldRedirectDeleteToSplitExpenseEdit,
} from '@libs/TransactionUtils';

Expand Down Expand Up @@ -58,6 +59,8 @@ type DeleteTransactionsResult =
deletedTransactionThreadReportIDs: string[];
};

type TransactionWithAction = {transactionID: string; action?: ReportAction; transaction?: Transaction};

function redistributeRemainingPerDiemSplitExpenses(splitExpenses: SplitExpense[], total: number, currency: string): SplitExpense[] {
const lastSplitIndex = splitExpenses.length - 1;

Expand Down Expand Up @@ -222,7 +225,11 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
return acc;
}

if (isExpenseSplit && originalTransactionID) {
const transactionCurrentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
const isMovedExpenseSplitChild =
!isExpenseSplit && !!originalTransactionID && isSplitChildTransaction(transaction) && !originalTransaction?.comment?.splits && isIOUReport(transactionCurrentReport);

if ((isExpenseSplit || isMovedExpenseSplitChild) && originalTransactionID) {
acc.splitTransactionsByOriginalTransactionID[originalTransactionID] ??= [];
acc.splitTransactionsByOriginalTransactionID[originalTransactionID].push(item);
} else {
Expand All @@ -232,8 +239,8 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
return acc;
},
{splitTransactionsByOriginalTransactionID: {}, nonSplitTransactions: []} as {
splitTransactionsByOriginalTransactionID: Record<string, Array<{transactionID: string; action?: ReportAction; transaction?: Transaction}>>;
nonSplitTransactions: Array<{transactionID: string; action?: ReportAction; transaction?: Transaction}>;
splitTransactionsByOriginalTransactionID: Record<string, TransactionWithAction[]>;
nonSplitTransactions: TransactionWithAction[];
},
);

Expand Down
21 changes: 20 additions & 1 deletion src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {Unit} from '@src/types/onyx/Policy';
import type Policy from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxEntry} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';

import {format, parseISO} from 'date-fns';

Expand Down Expand Up @@ -596,6 +596,24 @@ function getRateByCustomUnitRateID({customUnitRateID, policy}: {customUnitRateID
return getMileageRates(policy, true, customUnitRateID)[customUnitRateID];
}

/**
* Resolve an enabled mileage rate by its ID across every policy the user belongs to.
* Useful when the caller doesn't know which policy owns the rate (e.g. a self-DM split on the personal rate).
* Returns `undefined` for the P2P rate, a missing ID, or a disabled/deleted rate.
*/
function getEnabledRateByCustomUnitRateIDFromAnyPolicy(customUnitRateID: string | undefined, policies: OnyxCollection<Policy>): MileageRate | undefined {
if (!customUnitRateID || customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID) {
return undefined;
}
for (const policy of Object.values(policies ?? {})) {
const rate = getMileageRates(policy)[customUnitRateID];
if (rate) {
return rate;
}
}
return undefined;
}

/**
* Returns whether the selected custom unit rate is out of its valid date range for the given expense date.
*/
Expand Down Expand Up @@ -696,6 +714,7 @@ export default {
getUpdatedDistanceUnit,
getRate,
getRateByCustomUnitRateID,
getEnabledRateByCustomUnitRateIDFromAnyPolicy,
getDistanceForDisplayLabel,
convertDistanceUnit,
getRateForExpenseDisplay,
Expand Down
5 changes: 3 additions & 2 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ function getActivePolicies(policies: OnyxCollection<Policy> | null, currentUserL
* These will be policies that has expense chat enabled.
* These are policies that we can use to create reports with in NewDot.
*/
function getActivePoliciesWithExpenseChat(policies: OnyxCollection<Policy> | null, currentUserLogin: string | undefined): Policy[] {
function getActivePoliciesWithExpenseChat(policies: OnyxCollection<Policy> | null, currentUserLogin: string | undefined, isSubmit2026BetaEnabled = false): Policy[] {
return Object.values(policies ?? {}).filter<Policy>(
(policy): policy is Policy =>
!!policy &&
policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
!!policy.name &&
!!policy.id &&
!!getPolicyRole(policy, currentUserLogin) &&
isPaidGroupPolicy(policy),
(isPaidGroupPolicy(policy) || canAccessSubmitWorkspaceFeatures(policy, isSubmit2026BetaEnabled)),
);
}

Expand Down Expand Up @@ -2883,6 +2883,7 @@ export {
canPolicyAccessFeature,
escapeTagName,
getActivePolicies,
getActivePoliciesWithExpenseChat,
getAdminEmployees,
getCleanedTagName,
getCommaSeparatedTagNameWithSanitizedColons,
Expand Down
32 changes: 31 additions & 1 deletion src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ function getUpdatedTransaction({
isFromExpenseReport,
shouldUpdateReceiptState = true,
policy = undefined,
policies = undefined,
isSplitTransaction = false,
personalPolicyOutputCurrency,
}: {
Expand All @@ -615,6 +616,7 @@ function getUpdatedTransaction({
isFromExpenseReport: boolean;
shouldUpdateReceiptState?: boolean;
policy?: OnyxEntry<Policy>;
policies?: OnyxCollection<Policy>;
isSplitTransaction?: boolean;
personalPolicyOutputCurrency: string | undefined;
}): Transaction {
Expand Down Expand Up @@ -737,7 +739,25 @@ function getUpdatedTransaction({
// When the waypoints are being fetched from the server, we have no information about the distance, and cannot recalculate the updated amount.
// Otherwise, recalculate the fields based on the new rate.

const updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false, personalPolicyOutputCurrency});
let updatedMileageRate = DistanceRequestUtils.getRate({transaction: updatedTransaction, policy, useTransactionDistanceUnit: false, personalPolicyOutputCurrency});
// The provided `policy` may not own the new rate, leaving the amount at 0. Fall back to
// resolving the rate across every policy the user belongs to.
if (!updatedMileageRate.rate && transactionChanges.customUnitRateID) {
const rateFromAnyPolicy = DistanceRequestUtils.getEnabledRateByCustomUnitRateIDFromAnyPolicy(transactionChanges.customUnitRateID, policies);
if (rateFromAnyPolicy?.rate) {
updatedMileageRate = rateFromAnyPolicy;
Comment on lines +746 to +748

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute distance units for fallback mileage rates

When the selected customUnitRateID is only found via the new cross-policy fallback and that rate uses a different unit, the transaction has already stored newDistanceUnit from the wrong/missing policy and skipped the quantity conversion above. The later amount calculation then treats the old quantity as if it were in the fallback rate's unit (for example, 10 mi becomes 10 km when selecting a km rate), so the split amount/merchant are wrong until corrected by the server, and remain wrong offline. Resolve the fallback rate before setting/converting comment.customUnit.distanceUnit, or redo that conversion when rateFromAnyPolicy is used.

Useful? React with 👍 / 👎.


// The fallback rate wasn't known when the distance unit/quantity were set above from the
// (rate-less) provided policy, so redo that conversion against the fallback rate's actual unit.
if (rateFromAnyPolicy.unit && rateFromAnyPolicy.unit !== newDistanceUnit && !isOdometerDistanceRequest(transaction)) {
lodashSet(updatedTransaction, 'comment.customUnit.distanceUnit', rateFromAnyPolicy.unit);
const fallbackConversionFactor =
newDistanceUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? CONST.CUSTOM_UNITS.MILES_TO_KILOMETERS : CONST.CUSTOM_UNITS.KILOMETERS_TO_MILES;
const currentQuantity = updatedTransaction?.comment?.customUnit?.quantity ?? 0;
lodashSet(updatedTransaction, 'comment.customUnit.quantity', roundToTwoDecimalPlaces(currentQuantity * fallbackConversionFactor));
}
}
}
const {unit, rate} = updatedMileageRate;

const distanceInMeters = getDistanceInMeters(updatedTransaction, unit);
Expand Down Expand Up @@ -2887,6 +2907,15 @@ function isSplitChildTransaction(transaction: OnyxEntry<Transaction> | Transacti
return transaction?.comment?.source === CONST.IOU.TYPE.SPLIT;
}

/**
* The original (container) transaction of a split lives in SPLIT_REPORT_ID while the split exists, so it's
* hidden and has no dismiss UI of its own. Used to decide whether a split failure error on the original
* should be cleared alongside the visible child's error.
*/
function isSplitContainerTransaction(transaction: OnyxEntry<Transaction> | Transaction): boolean {
return transaction?.reportID === CONST.REPORT.SPLIT_REPORT_ID;
}

function hasSplitExpenseInSelection(transactions: Transaction[]): boolean {
return transactions.some(isSplitChildTransaction);
}
Expand Down Expand Up @@ -3274,6 +3303,7 @@ export {
isExpenseSplit,
hasSplitExpenseInSelection,
isSplitChildTransaction,
isSplitContainerTransaction,
getAttendeesListDisplayString,
isCorporateCardTransaction,
isExpenseUnreported,
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/IOU/BulkEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ function updateMultipleMoneyRequests({
transactionChanges,
isFromExpenseReport,
policy: transactionPolicy,
policies: allPolicies,
personalPolicyOutputCurrency,
});
const isTransactionOnHold = isOnHold(transaction);
Expand Down
2 changes: 2 additions & 0 deletions src/libs/actions/IOU/Split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,7 @@ function setDraftSplitTransaction(
transactionChanges: TransactionChanges = {},
policy?: OnyxEntry<OnyxTypes.Policy>,
personalPolicyOutputCurrency?: string,
policies?: OnyxCollection<OnyxTypes.Policy>,
) {
if (!transactionID) {
return undefined;
Expand All @@ -1297,6 +1298,7 @@ function setDraftSplitTransaction(
isFromExpenseReport: false,
shouldUpdateReceiptState: false,
policy,
policies,
isSplitTransaction: true,
personalPolicyOutputCurrency,
})
Expand Down
19 changes: 13 additions & 6 deletions src/libs/actions/IOU/SplitExpenseItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ function updateSplitExpenseAmountField(
policy: OnyxEntry<OnyxTypes.Policy>,
isSelfDMSplit: boolean,
personalPolicyOutputCurrency: string | undefined,
policies?: OnyxCollection<OnyxTypes.Policy>,
) {
if (!draftTransaction?.transactionID || !currentItemTransactionID || Number.isNaN(amount)) {
return;
Expand All @@ -672,12 +673,18 @@ function updateSplitExpenseAmountField(
// Update distance for distance transactions based on new amount and rate
if (isDistanceRequest && originalTransaction && splitExpense.customUnit) {
const mileageRate = resolveSplitMileageRate({transaction: originalTransaction, policy, isSelfDMSplit, personalPolicyOutputCurrency});
const {rate: currentRate = 0} =
DistanceRequestUtils.getRateByCustomUnitRateID({policy, customUnitRateID: splitExpense.customUnit?.customUnitRateID ?? String(CONST.DEFAULT_NUMBER_ID)}) ?? {};
const {unit, rate: mileageRateValue} = mileageRate;
const preferredRate = isSelfDMSplit ? mileageRateValue : currentRate;
const secondaryRate = isSelfDMSplit ? currentRate : mileageRateValue;
const rate = preferredRate && preferredRate > 0 ? preferredRate : secondaryRate;
const splitRateID = splitExpense.customUnit?.customUnitRateID ?? String(CONST.DEFAULT_NUMBER_ID);
// `policy` is undefined for a self-DM split on the personal rate, so also resolve the split's
// picked rate across all policies, so the selection isn't lost.
const splitSelectedRate =
DistanceRequestUtils.getRateByCustomUnitRateID({policy, customUnitRateID: splitRateID}) ??
DistanceRequestUtils.getEnabledRateByCustomUnitRateIDFromAnyPolicy(splitRateID, policies);
const isSplitP2PRate = splitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID;
// Prefer the split's own selected rate when it's a real enabled rate; otherwise fall back to
// the original-transaction rate (covers the P2P and deleted-rate cases).
const useSplitSelectedRate = !isSplitP2PRate && !!splitSelectedRate?.rate && splitSelectedRate.rate > 0 && splitSelectedRate.enabled !== false;
const rate = useSplitSelectedRate ? (splitSelectedRate?.rate ?? 0) : mileageRate.rate;
Comment thread
ZhenjaHorbach marked this conversation as resolved.
const unit = useSplitSelectedRate ? (splitSelectedRate?.unit ?? mileageRate.unit) : mileageRate.unit;

if (rate && rate > 0) {
const {customUnit: updatedCustomUnit, merchant} = updateSplitExpenseDistanceFromAmount(
Expand Down
Loading
Loading