Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 1 addition & 3 deletions src/components/MoneyRequestHeaderSecondaryActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import {
isCurrentUserSubmitter,
isDM,
isExpenseReport,
isOpenReport,
isSelfDM,
navigateToDetailsPage,
rejectMoneyRequestReason,
Expand Down Expand Up @@ -193,8 +192,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
const shouldDuplicateCloseModalOnSelect = isDistanceExpenseUnsupportedForDuplicating || hasCustomUnitOutOfPolicyViolation || isPerDiemRequestOnNonDefaultWorkspace;
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
const hasMultipleSplits = useHasMultipleSplitChildren(transaction?.comment?.originalTransactionID);
const isReportOpen = isOpenReport(parentReport);
const shouldShowSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
const shouldShowSplitIndicator = isExpenseSplit && hasMultipleSplits;
const shouldShowEditSplitOnDeleteAction = !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]);
const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport);
const draftTransactionIDs = Object.keys(transactionDrafts ?? {});
Expand Down
4 changes: 1 addition & 3 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ import {
getTripIDFromTransactionParentReportID,
isExpenseReport,
isInvoiceReport,
isOpenReport,
isPaidGroupPolicy,
isReportApproved,
isReportInGroupPolicy,
Expand Down Expand Up @@ -352,8 +351,7 @@ function MoneyRequestView({
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`);
const hasMultipleSplits = useHasMultipleSplitChildren(transaction?.comment?.originalTransactionID);
const isReportOpen = isOpenReport(moneyRequestReport);
const shouldShowSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
const shouldShowSplitIndicator = isExpenseSplit && hasMultipleSplits;
const isSplitAvailable =
moneyRequestReport &&
transaction &&
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useDeleteTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,14 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
}

const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.comment?.originalTransactionID}`];
if (!shouldRedirectDeleteToSplitExpenseEdit(transaction, originalTransaction)) {
const hasMultipleSplits = getChildTransactions(allTransactions, allReports, originalTransaction?.transactionID, true).length > 1;
if (!shouldRedirectDeleteToSplitExpenseEdit(transaction, originalTransaction) || (!hasMultipleSplits && isPerDiemRequestTransactionUtils(originalTransaction))) {
return undefined;
}

return transaction;
},
[allTransactions],
[allTransactions, allReports],
);

const shouldOpenSplitExpenseEditFlowOnDelete = useCallback(
Expand Down
6 changes: 2 additions & 4 deletions src/hooks/useExpenseActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
getAddExpenseDropdownOptions,
getPolicyExpenseChat,
isDM,
isOpenReport,
isSelfDM,
navigateOnDeleteExpense,
} from '@libs/ReportUtils';
Expand Down Expand Up @@ -170,9 +169,8 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic

// Split indicator
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
const hasMultipleSplits = !!transaction?.comment?.originalTransactionID && getChildTransactions(allTransactions, allReports, transaction.comment.originalTransactionID).length > 1;
const isReportOpen = isOpenReport(moneyRequestReport);
const hasSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
const hasMultipleSplits = !!transaction?.comment?.originalTransactionID && getChildTransactions(allTransactions, allReports, transaction.comment.originalTransactionID, true).length > 1;
const hasSplitIndicator = isExpenseSplit && hasMultipleSplits;
const shouldShowEditSplitOnDeleteAction = !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]);

// Duplicate report throttle
Expand Down
6 changes: 1 addition & 5 deletions src/hooks/useHasMultipleSplitChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ function selectChildTransactionInfo(transactions: OnyxCollection<Transaction>, o
}
const result: ChildTransactionInfo[] = [];
for (const t of Object.values(transactions ?? {})) {
if (
t?.comment?.originalTransactionID === originalTransactionID &&
t?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
t?.reportID !== CONST.REPORT.UNREPORTED_REPORT_ID
) {
if (t?.comment?.originalTransactionID === originalTransactionID && t?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
result.push({reportID: t?.reportID, isSplitSource: t?.comment?.source === CONST.IOU.TYPE.SPLIT});
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
isTrackExpenseReport,
} from '@libs/ReportUtils';
import {getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils';
import {getOriginalTransactionWithSplitInfo, hasTransactionBeenRejected} from '@libs/TransactionUtils';
import {getChildTransactions, getOriginalTransactionWithSplitInfo, hasTransactionBeenRejected} from '@libs/TransactionUtils';
import type {IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -471,8 +471,12 @@ function useSelectedTransactionsActions({
const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${firstTransaction?.comment?.originalTransactionID}`];

const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(firstTransaction, originalTransaction);
const hasMultipleSplits = getChildTransactions(allTransactions, allReports, firstTransaction?.comment?.originalTransactionID, true).length > 1;
const canSplitTransaction =
selectedTransactionsList.length === 1 && report && !isExpenseSplit && isSplitAction(report, [firstTransaction], originalTransaction, login ?? '', currentUserAccountID, policy);
selectedTransactionsList.length === 1 &&
report &&
!(isExpenseSplit && hasMultipleSplits) &&
isSplitAction(report, [firstTransaction], originalTransaction, login ?? '', currentUserAccountID, policy);

if (canSplitTransaction) {
options.push({
Expand Down
7 changes: 3 additions & 4 deletions src/libs/actions/SplitExpenses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {calculateAmount} from '@libs/IOUUtils';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import Navigation from '@libs/Navigation/Navigation';
import {rand64} from '@libs/NumberUtils';
import {getTransactionDetails, isOpenReport} from '@libs/ReportUtils';
import {getTransactionDetails} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {buildOptimisticTransaction, getChildTransactions, getOriginalTransactionWithSplitInfo, isDistanceRequest} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -82,10 +82,9 @@ function initSplitExpense(transaction: OnyxEntry<Transaction>, policy?: OnyxEntr
const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`];
const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);
const relatedTransactions = getChildTransactions(allTransactions, allReports, originalTransactionID);
const hasMultipleSplits = relatedTransactions.length > 1;
const hasMultipleSplits = getChildTransactions(allTransactions, allReports, originalTransactionID, true).length > 1;
const transactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`];
const isReportOpen = isOpenReport(transactionReport);
const shouldShowSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
const shouldShowSplitIndicator = isExpenseSplit && hasMultipleSplits;

if (isExpenseSplit && shouldShowSplitIndicator) {
const transactionDetails = getTransactionDetails(originalTransaction);
Expand Down
125 changes: 125 additions & 0 deletions tests/actions/IOUTest/SplitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4997,6 +4997,131 @@ describe('initSplitExpense', () => {
expect(splitExpenses?.[1].merchant).toBeTruthy();
expect(splitExpenses?.[1].merchant).toContain('100');
});

it('should let you split the expense again after its other split half was deleted', async () => {
const originalTransactionID = 'dissolved-original';
const remainingTransactionID = 'dissolved-remaining';
const deletedSiblingTransactionID = 'dissolved-deleted-sibling';
const expenseReportID = 'dissolved-expense-report';

// Given an open expense report (an open report previously kept the remaining transaction stuck in the "edit splits" flow)
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReportID}`, {
reportID: expenseReportID,
type: CONST.REPORT.TYPE.EXPENSE,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
});

// And the original parent expense transaction still exists, hidden on the split report
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, {
transactionID: originalTransactionID,
amount: -100,
currency: 'USD',
merchant: 'Test Merchant',
comment: {comment: 'Original expense'},
created: DateUtils.getDBTime(),
reportID: CONST.REPORT.SPLIT_REPORT_ID,
});

// And the sibling split was unreported and then deleted
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${deletedSiblingTransactionID}`, {
transactionID: deletedSiblingTransactionID,
amount: -50,
currency: 'USD',
merchant: 'Test Merchant',
comment: {originalTransactionID, source: CONST.IOU.TYPE.SPLIT},
created: DateUtils.getDBTime(),
reportID: CONST.REPORT.UNREPORTED_REPORT_ID,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
});

// And the remaining split is the only child left and still references the original
const remainingTransaction: Transaction = {
transactionID: remainingTransactionID,
amount: -50,
currency: 'USD',
merchant: 'Test Merchant',
comment: {originalTransactionID, source: CONST.IOU.TYPE.SPLIT},
created: DateUtils.getDBTime(),
reportID: expenseReportID,
};
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${remainingTransactionID}`, remainingTransaction);
await waitForBatchedUpdates();

// When the user initiates a split on the remaining transaction
initSplitExpense(remainingTransaction, undefined);
await waitForBatchedUpdates();

// Then a fresh split is started keyed by the remaining transaction (2 new splits)
const freshDraft = await getOnyxValue(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${remainingTransactionID}`);
expect(freshDraft).toBeTruthy();
expect(freshDraft?.comment?.splitExpenses).toHaveLength(2);

// And the split's edit flow keyed by the original transaction is not re-opened
const editDraft = await getOnyxValue(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${originalTransactionID}`);
expect(editDraft).toBeFalsy();
});

it('should open the edit-splits flow for an intact multi-child split', async () => {
const originalTransactionID = 'intact-original';
const firstChildTransactionID = 'intact-child-1';
const secondChildTransactionID = 'intact-child-2';
const expenseReportID = 'intact-expense-report';

// Given an open expense report
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReportID}`, {
reportID: expenseReportID,
type: CONST.REPORT.TYPE.EXPENSE,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
});

// And the original parent expense transaction still exists, hidden on the split report
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`, {
transactionID: originalTransactionID,
amount: -100,
currency: 'USD',
merchant: 'Test Merchant',
comment: {comment: 'Original expense'},
created: DateUtils.getDBTime(),
reportID: CONST.REPORT.SPLIT_REPORT_ID,
});

// And two split children both live on the expense report (intact split, no deletions)
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${firstChildTransactionID}`, {
transactionID: firstChildTransactionID,
amount: -50,
currency: 'USD',
merchant: 'Test Merchant',
comment: {originalTransactionID, source: CONST.IOU.TYPE.SPLIT},
created: DateUtils.getDBTime(),
reportID: expenseReportID,
});
const secondChildTransaction: Transaction = {
transactionID: secondChildTransactionID,
amount: -50,
currency: 'USD',
merchant: 'Test Merchant',
comment: {originalTransactionID, source: CONST.IOU.TYPE.SPLIT},
created: DateUtils.getDBTime(),
reportID: expenseReportID,
};
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${secondChildTransactionID}`, secondChildTransaction);
await waitForBatchedUpdates();

// When the user opens the edit-splits flow from one of the children
initSplitExpense(secondChildTransaction, undefined);
await waitForBatchedUpdates();

// Then the edit-splits draft is keyed by the original transaction and rebuilt from the existing children
const editDraft = await getOnyxValue(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${originalTransactionID}`);
expect(editDraft).toBeTruthy();
expect(editDraft?.comment?.splitExpenses).toHaveLength(2);

// And no fresh split was started keyed by the child the user opened it from
const freshDraft = await getOnyxValue(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${secondChildTransactionID}`);
expect(freshDraft).toBeFalsy();
});
});

describe('addSplitExpenseField', () => {
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3158,6 +3158,61 @@ describe('getSecondaryTransactionThreadActions', () => {
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SPLIT)).toBe(true);
});

it('includes the SPLIT option after the other split half was deleted', async () => {
// Given an open expense report owned by the current user
const report = {
reportID: REPORT_ID,
policyID: POLICY_ID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: EMPLOYEE_ACCOUNT_ID,
managerID: EMPLOYEE_ACCOUNT_ID,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
} as unknown as Report;

// And a surviving split child that still references the original (its sibling was unreported then deleted)
const survivingSplit = {
transactionID: 'SURVIVING_SPLIT',
status: CONST.TRANSACTION.STATUS.POSTED,
amount: 50,
merchant: 'Merchant',
date: '2025-01-01',
reportID: REPORT_ID,
comment: {originalTransactionID: 'ORIGINAL_TXN', source: CONST.IOU.TYPE.SPLIT},
} as unknown as Transaction;

// And the original ("parent") expense transaction still existing, hidden on the split report
const originalTransaction = {
transactionID: 'ORIGINAL_TXN',
amount: 100,
merchant: 'Merchant',
date: '2025-01-01',
reportID: CONST.REPORT.SPLIT_REPORT_ID,
comment: {},
} as unknown as Transaction;

// And the current user is a member of the policy
const policy = {
id: POLICY_ID,
type: CONST.POLICY.TYPE.TEAM,
isPolicyExpenseChatEnabled: true,
employeeList: {
[EMPLOYEE_EMAIL]: {email: EMPLOYEE_EMAIL, role: CONST.POLICY.ROLE.USER},
[ADMIN_EMAIL]: {email: ADMIN_EMAIL, role: CONST.POLICY.ROLE.ADMIN},
},
role: CONST.POLICY.ROLE.ADMIN,
} as unknown as Policy;

await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);

// When the secondary transaction-thread actions are computed
const result = getSecondaryTransactionThreadActions(EMPLOYEE_EMAIL, EMPLOYEE_ACCOUNT_ID, report, survivingSplit, actionR14932, originalTransaction, policy);

// Then the SPLIT option is available
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SPLIT)).toBe(true);
});

it('does not include the SPLIT option if the current user does not belong to the workspace', async () => {
const report = {
reportID: REPORT_ID,
Expand Down
Loading
Loading