Skip to content

Commit 74c1ca6

Browse files
authored
Merge pull request Expensify#85293 from marufsharifi/fix/per-diem-split-expense-delete-error
Fix unexpected error after deleting per diem split expense
2 parents 4d4890c + ed38065 commit 74c1ca6

15 files changed

Lines changed: 468 additions & 52 deletions

src/components/ButtonWithDropdownMenu/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ type DropdownOption<TValueType> = WithSentryLabel & {
5252
shouldShowLoadingSpinnerIcon?: boolean;
5353
/** The type of brick road indicator to show */
5454
brickRoadIndicator?: ValueOf<typeof CONST.BRICK_ROAD_INDICATOR_STATUS>;
55+
/** Whether selecting this option should bypass the delete confirmation modal */
56+
shouldSkipDeleteModal?: boolean;
5557

5658
/** Whether to ignore compact popover menu styling for this item */
5759
shouldIgnoreCompactStyle?: boolean;

src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,9 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn
409409

410410
const mappedOptions = originalSelectedTransactionsOptions.map((option) => {
411411
if (option.value === CONST.REPORT.SECONDARY_ACTIONS.DELETE) {
412+
if (option.shouldSkipDeleteModal) {
413+
return option;
414+
}
412415
return {...option, onSelected: showDeleteModal};
413416
}
414417
if (option.value === CONST.REPORT.SECONDARY_ACTIONS.REJECT) {

src/components/MoneyRequestHeaderSecondaryActions.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
149149
const [defaultPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(defaultExpensePolicy?.id)}`);
150150
const {policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiemRequest(transaction));
151151
const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy;
152-
const {deleteTransactions} = useDeleteTransactions({report: parentReport, reportActions: parentReportAction ? [parentReportAction] : [], policy});
152+
const {deleteTransactions, shouldOpenSplitExpenseEditFlowOnDelete} = useDeleteTransactions({report: parentReport, reportActions: parentReportAction ? [parentReportAction] : [], policy});
153153
const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(parentReportAction);
154154
const isParentReportArchived = useReportIsArchived(report?.parentReportID);
155155
const {isBetaEnabled} = usePermissions();
@@ -190,6 +190,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
190190
const hasMultipleSplits = useHasMultipleSplitChildren(transaction?.comment?.originalTransactionID);
191191
const isReportOpen = isOpenReport(parentReport);
192192
const shouldShowSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
193+
const shouldShowEditSplitOnDeleteAction = !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]);
193194
const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport);
194195
const draftTransactionIDs = Object.keys(transactionDrafts ?? {});
195196
const targetPolicyTags = defaultPolicyTags ?? {};
@@ -402,10 +403,15 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
402403
},
403404
},
404405
[CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DELETE]: {
405-
text: translate('common.delete'),
406-
icon: expensifyIcons.Trashcan,
406+
text: shouldShowEditSplitOnDeleteAction ? translate('iou.editSplits') : translate('common.delete'),
407+
icon: shouldShowEditSplitOnDeleteAction ? expensifyIcons.ArrowSplit : expensifyIcons.Trashcan,
407408
value: CONST.REPORT.SECONDARY_ACTIONS.DELETE,
408409
onSelected: () => {
410+
if (shouldShowEditSplitOnDeleteAction && transaction?.transactionID) {
411+
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true);
412+
return;
413+
}
414+
409415
showConfirmModal({
410416
title: translate('iou.deleteExpense', {count: 1}),
411417
prompt: translate('iou.deleteConfirmation', {count: 1}),
@@ -440,8 +446,24 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
440446
currentUserEmail: currentUserLogin ?? '',
441447
});
442448
} else {
449+
if (shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID])) {
450+
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true);
451+
return;
452+
}
453+
// eslint-disable-next-line @typescript-eslint/no-deprecated
443454
InteractionManager.runAfterInteractions(() => {
444-
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, isReportInSearch ? currentSearchHash : undefined, true);
455+
const deleteResult = deleteTransactions(
456+
[transaction.transactionID],
457+
duplicateTransactions,
458+
duplicateTransactionViolations,
459+
isReportInSearch ? currentSearchHash : undefined,
460+
true,
461+
);
462+
463+
if (deleteResult.action === 'redirected') {
464+
return;
465+
}
466+
445467
removeTransaction(transaction.transactionID);
446468
});
447469
}

src/hooks/useDeleteTransactions.ts

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@ import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate';
77
import {getIOURequestPolicyID} from '@libs/actions/IOU/MoneyRequest';
88
import {initSplitExpenseItemData} from '@libs/actions/IOU/SplitExpenseItems';
99
import {updateSplitTransactions} from '@libs/actions/IOU/SplitTransactionUpdate';
10+
import initSplitExpense from '@libs/actions/SplitExpenses';
1011
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
12+
import {calculateAmount as calculateIOUAmount} from '@libs/IOUUtils';
1113
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
1214
import {getActiveGroupSearchHashes} from '@libs/SearchUIUtils';
13-
import {getChildTransactions, getOriginalTransactionWithSplitInfo} from '@libs/TransactionUtils';
15+
import {
16+
getChildTransactions,
17+
getOriginalTransactionWithSplitInfo,
18+
isPerDiemRequest as isPerDiemRequestTransactionUtils,
19+
shouldRedirectDeleteToSplitExpenseEdit,
20+
} from '@libs/TransactionUtils';
1421
import CONST from '@src/CONST';
1522
import ONYXKEYS from '@src/ONYXKEYS';
1623
import type {Policy, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
24+
import type {SplitExpense} from '@src/types/onyx/IOU';
1725
import useArchivedReportsIdSet from './useArchivedReportsIdSet';
1826
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
1927
import useNetwork from './useNetwork';
@@ -29,6 +37,24 @@ type UseDeleteTransactionsParams = {
2937
policy?: Policy;
3038
};
3139

40+
type DeleteTransactionsResult =
41+
| {
42+
action: 'redirected';
43+
}
44+
| {
45+
action: 'deleted';
46+
deletedTransactionThreadReportIDs: string[];
47+
};
48+
49+
function redistributeRemainingPerDiemSplitExpenses(splitExpenses: SplitExpense[], total: number, currency: string): SplitExpense[] {
50+
const lastSplitIndex = splitExpenses.length - 1;
51+
52+
return splitExpenses.map((splitExpense, index) => ({
53+
...splitExpense,
54+
amount: calculateIOUAmount(lastSplitIndex, total, currency, index === lastSplitIndex, true),
55+
}));
56+
}
57+
3258
/**
3359
* Pure hook for deleting transactions
3460
* All data must be provided through function parameters
@@ -53,15 +79,41 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
5379
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
5480
const {isOffline} = useNetwork();
5581

82+
const getSplitExpenseEditTransactionOnDelete = useCallback(
83+
(transactionIDs: string[]): Transaction | undefined => {
84+
if (transactionIDs.length !== 1) {
85+
return undefined;
86+
}
87+
88+
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDs.at(0)}`];
89+
90+
if (!transaction) {
91+
return undefined;
92+
}
93+
94+
const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.comment?.originalTransactionID}`];
95+
if (!shouldRedirectDeleteToSplitExpenseEdit(transaction, originalTransaction)) {
96+
return undefined;
97+
}
98+
99+
return transaction;
100+
},
101+
[allTransactions],
102+
);
103+
104+
const shouldOpenSplitExpenseEditFlowOnDelete = useCallback(
105+
(transactionIDs: string[]): boolean => !!getSplitExpenseEditTransactionOnDelete(transactionIDs),
106+
[getSplitExpenseEditTransactionOnDelete],
107+
);
108+
56109
/**
57110
* Delete transactions by IDs
58111
* @param transactionIDs - Array of transaction IDs to delete
59112
* @param duplicateTransactions - Collection of duplicate transactions
60113
* @param duplicateTransactionViolations - Collection of duplicate transaction violations
61114
* @param currentSearchHash - Current search hash for updating split transactions
62-
* @param onClearSelection - Optional callback to clear selection after deletion
63115
* @param isSingleTransactionView - Optional flag indicating if the deletion is from a single transaction view
64-
* @returns Array of deleted transaction thread report IDs for navigation handling
116+
* @returns Result describing whether the delete redirected or deleted transaction threads
65117
*/
66118
const deleteTransactions = useCallback(
67119
(
@@ -70,9 +122,21 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
70122
duplicateTransactionViolations: OnyxCollection<TransactionViolations>,
71123
currentSearchHash?: number,
72124
isSingleTransactionView?: boolean,
73-
): string[] => {
125+
): DeleteTransactionsResult => {
74126
if (!transactionIDs.length) {
75-
return [];
127+
return {
128+
action: 'deleted',
129+
deletedTransactionThreadReportIDs: [],
130+
};
131+
}
132+
133+
const splitExpenseEditTransaction = getSplitExpenseEditTransactionOnDelete(transactionIDs);
134+
135+
if (splitExpenseEditTransaction) {
136+
initSplitExpense(splitExpenseEditTransaction, policy, {navigateToEditSplitExpense: true});
137+
return {
138+
action: 'redirected',
139+
};
76140
}
77141

78142
const iouActions = reportActions.filter((action) => isMoneyRequestAction(action));
@@ -122,6 +186,10 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
122186
const originalTransactionIouActions = getIOUActionForTransactions([transactionID], report?.reportID);
123187
const iouReportID = isMoneyRequestAction(originalTransactionIouActions.at(0)) ? getOriginalMessage(originalTransactionIouActions.at(0))?.IOUReportID : undefined;
124188
const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`];
189+
const splitExpensesTotal = allChildTransactions.reduce((total, childTransaction) => {
190+
const transactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childTransaction?.reportID}`];
191+
return total + initSplitExpenseItemData(childTransaction, transactionReport).amount;
192+
}, 0);
125193
const policyRecentlyUsedCategories =
126194
allPolicyRecentlyUsedCategories?.[
127195
`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${getNonEmptyStringOnyxID(getIOURequestPolicyID(originalTransaction, report))}`
@@ -141,6 +209,11 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
141209
const transactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childTransaction?.reportID}`];
142210
return initSplitExpenseItemData(childTransaction, transactionReport);
143211
});
212+
const remainingSplitExpensesTotal = remainingSplitExpenses.reduce((total, splitExpense) => total + splitExpense.amount, 0);
213+
const updatedRemainingSplitExpenses =
214+
originalTransaction && isPerDiemRequestTransactionUtils(originalTransaction) && remainingSplitExpenses.length > 0 && remainingSplitExpensesTotal !== splitExpensesTotal
215+
? redistributeRemainingPerDiemSplitExpenses(remainingSplitExpenses, splitExpensesTotal, originalTransaction.currency ?? CONST.CURRENCY.USD)
216+
: remainingSplitExpenses;
144217

145218
const parentTransactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
146219
const expenseReport = report?.type === CONST.REPORT.TYPE.EXPENSE ? report : parentTransactionReport;
@@ -156,11 +229,8 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
156229
transactionData: {
157230
reportID: report?.reportID ?? String(CONST.DEFAULT_NUMBER_ID),
158231
originalTransactionID: transactionID,
159-
splitExpenses: remainingSplitExpenses,
160-
splitExpensesTotal: allChildTransactions.reduce((total, childTransaction) => {
161-
const transactionReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childTransaction?.reportID}`];
162-
return total + initSplitExpenseItemData(childTransaction, transactionReport).amount;
163-
}, 0),
232+
splitExpenses: updatedRemainingSplitExpenses,
233+
splitExpensesTotal,
164234
},
165235
searchContext: {
166236
currentSearchHash,
@@ -216,7 +286,10 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
216286
}
217287
}
218288

219-
return Array.from(deletedTransactionThreadReportIDs);
289+
return {
290+
action: 'deleted',
291+
deletedTransactionThreadReportIDs: Array.from(deletedTransactionThreadReportIDs),
292+
};
220293
},
221294
[
222295
allPolicyRecentlyUsedCategories,
@@ -240,12 +313,14 @@ function useDeleteTransactions({report, reportActions, policy}: UseDeleteTransac
240313
betas,
241314
allPolicyTags,
242315
personalDetails,
316+
getSplitExpenseEditTransactionOnDelete,
243317
isOffline,
244318
],
245319
);
246320

247321
return {
248322
deleteTransactions,
323+
shouldOpenSplitExpenseEditFlowOnDelete,
249324
};
250325
}
251326

src/hooks/useExpenseActions.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
159159
const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID));
160160

161161
// Delete hook — pass chatReport (as in MoneyReportHeader) not moneyRequestReport
162-
const {deleteTransactions} = useDeleteTransactions({
162+
const {deleteTransactions, shouldOpenSplitExpenseEditFlowOnDelete} = useDeleteTransactions({
163163
report: chatReport,
164164
reportActions,
165165
policy,
@@ -173,6 +173,7 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
173173
const hasMultipleSplits = !!transaction?.comment?.originalTransactionID && getChildTransactions(allTransactions, allReports, transaction.comment.originalTransactionID).length > 1;
174174
const isReportOpen = isOpenReport(moneyRequestReport);
175175
const hasSplitIndicator = isExpenseSplit && (hasMultipleSplits || isReportOpen);
176+
const shouldShowEditSplitOnDeleteAction = !!transaction?.transactionID && shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID]);
176177

177178
// Duplicate report throttle
178179
const [isDuplicateReportActive, temporarilyDisableDuplicateReportAction] = useThrottledButtonState();
@@ -461,14 +462,19 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
461462
},
462463
},
463464
[CONST.REPORT.SECONDARY_ACTIONS.DELETE]: {
464-
text: translate('common.delete'),
465-
icon: expensifyIcons.Trashcan,
465+
text: shouldShowEditSplitOnDeleteAction ? translate('iou.editSplits') : translate('common.delete'),
466+
icon: shouldShowEditSplitOnDeleteAction ? expensifyIcons.ArrowSplit : expensifyIcons.Trashcan,
466467
value: CONST.REPORT.SECONDARY_ACTIONS.DELETE,
467-
sentryLabel: CONST.SENTRY_LABEL.MORE_MENU.DELETE,
468+
sentryLabel: shouldShowEditSplitOnDeleteAction ? CONST.SENTRY_LABEL.MORE_MENU.SPLIT : CONST.SENTRY_LABEL.MORE_MENU.DELETE,
468469
onSelected: async () => {
469470
const transactionCount = Object.keys(transactions).length;
470471

471472
if (transactionCount === 1) {
473+
if (shouldShowEditSplitOnDeleteAction && transaction?.transactionID) {
474+
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false);
475+
return;
476+
}
477+
472478
const result = await showConfirmModal({
473479
title: translate('iou.deleteExpense', {count: 1}),
474480
prompt: translate('iou.deleteConfirmation', {count: 1}),
@@ -484,6 +490,10 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
484490
if (!requestParentReportAction || !transaction?.transactionID) {
485491
throw new Error('Missing data!');
486492
}
493+
if (shouldOpenSplitExpenseEditFlowOnDelete([transaction.transactionID])) {
494+
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, false);
495+
return;
496+
}
487497
const goBackRoute = getNavigationUrlOnMoneyRequestDelete(
488498
transaction.transactionID,
489499
requestParentReportAction,
@@ -498,7 +508,18 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
498508
navigateOnDeleteExpense(goBackRoute);
499509
}
500510
InteractionManager.runAfterInteractions(() => {
501-
deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, isReportInSearch ? currentSearchHash : undefined, false);
511+
const deleteResult = deleteTransactions(
512+
[transaction.transactionID],
513+
duplicateTransactions,
514+
duplicateTransactionViolations,
515+
isReportInSearch ? currentSearchHash : undefined,
516+
false,
517+
);
518+
519+
if (deleteResult.action === 'redirected') {
520+
return;
521+
}
522+
502523
removeTransaction(transaction.transactionID);
503524
});
504525
}

0 commit comments

Comments
 (0)