From 116297617a0fff41d7586879d8fd6baef9014f07 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 24 Jun 2025 21:54:05 +0300 Subject: [PATCH 01/34] inital testing --- src/libs/actions/IOU.ts | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7d14ac91ec87..5c7887aff928 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4306,13 +4306,21 @@ function getUpdateMoneyRequestParams( const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( updatedTransaction, - currentTransactionViolations, + currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION), policy, policyTagList ?? {}, policyCategories ?? {}, hasDependentTags(policy, policyTagList ?? {}), isInvoiceReportReportUtils(iouReport), ); + + if (transactionID && hasModifiedAmount){ + ((currentTransactionViolations ?? []).find( + (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION + )?.data?.duplicates ?? []).forEach( + (duplicateID) => updateDuplicateTransactionViolation(transactionID, duplicateID, optimisticData, failureData) + ) + } optimisticData.push(violationsOnyxData); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -4368,6 +4376,52 @@ function getUpdateMoneyRequestParams( }; } +/** + * Update duplicate transaction violations + */ +function updateDuplicateTransactionViolation( + transactionID: string, + duplicateID: string, + optimisticData: OnyxUpdate[], + failureData: OnyxUpdate[], +){ + const duplicateTransactionsViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; + if (!duplicateTransactionsViolations) { + return; + } + + const duplicateViolation = duplicateTransactionsViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + if (!duplicateViolation?.data?.duplicates) { + return; + } + + const duplicateTransactionIDs = duplicateViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); + + const optimisticViolations: OnyxTypes.TransactionViolations = duplicateTransactionsViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + + if (duplicateTransactionIDs.length > 0) { + optimisticViolations.push({ + ...duplicateViolation, + data: { + ...duplicateViolation.data, + duplicates: duplicateTransactionIDs, + }, + }); + } + + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, + value: optimisticViolations.length > 0 ? optimisticViolations : null, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, + value: duplicateTransactionsViolations, + }); +}; + /** * @param transactionID * @param transactionThreadReportID From b33699c5aa8366b20dfa2c4864842575508b5d8b Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 25 Jun 2025 01:22:49 +0300 Subject: [PATCH 02/34] renaming varible --- src/libs/actions/IOU.ts | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5c7887aff928..76a2a12661c7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4304,23 +4304,16 @@ function getUpdateMoneyRequestParams( (hasModifiedTag || hasModifiedCategory || hasModifiedComment || hasModifiedDistanceRate || hasModifiedAmount || hasModifiedCreated) ) { const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; + const optimisticViolations = hasModifiedAmount ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) : currentTransactionViolations const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( updatedTransaction, - currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION), + optimisticViolations, policy, policyTagList ?? {}, policyCategories ?? {}, hasDependentTags(policy, policyTagList ?? {}), isInvoiceReportReportUtils(iouReport), ); - - if (transactionID && hasModifiedAmount){ - ((currentTransactionViolations ?? []).find( - (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION - )?.data?.duplicates ?? []).forEach( - (duplicateID) => updateDuplicateTransactionViolation(transactionID, duplicateID, optimisticData, failureData) - ) - } optimisticData.push(violationsOnyxData); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -4377,7 +4370,11 @@ function getUpdateMoneyRequestParams( } /** - * Update duplicate transaction violations + * Update duplicates transaction violation + * @param transactionID + * @param duplicateID + * @param optimisticData + * @param failureData */ function updateDuplicateTransactionViolation( transactionID: string, @@ -4385,19 +4382,19 @@ function updateDuplicateTransactionViolation( optimisticData: OnyxUpdate[], failureData: OnyxUpdate[], ){ - const duplicateTransactionsViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; - if (!duplicateTransactionsViolations) { + const duplicateTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; + if (!duplicateTransactionViolations) { return; } - const duplicateViolation = duplicateTransactionsViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + const duplicateViolation = duplicateTransactionViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); if (!duplicateViolation?.data?.duplicates) { return; } const duplicateTransactionIDs = duplicateViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); - const optimisticViolations: OnyxTypes.TransactionViolations = duplicateTransactionsViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + const optimisticViolations: OnyxTypes.TransactionViolations = duplicateTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); if (duplicateTransactionIDs.length > 0) { optimisticViolations.push({ @@ -4418,7 +4415,7 @@ function updateDuplicateTransactionViolation( failureData.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: duplicateTransactionsViolations, + value: duplicateTransactionViolations, }); }; @@ -7440,6 +7437,17 @@ function updateMoneyRequestAmountAndCurrency({ undefined, allowNegative, ); + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; + ((currentTransactionViolations ?? []).find( + (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION + )?.data?.duplicates ?? []).forEach( + (duplicateID) => updateDuplicateTransactionViolation( + transactionID, + duplicateID, + data.onyxData.optimisticData ?? [], + data.onyxData.failureData ?? [] + ) + ) } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); From 193740ad5638882af5185608588c65281dd7c267 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 27 Jun 2025 04:10:50 +0300 Subject: [PATCH 03/34] using helper function for deleting money request --- src/libs/actions/IOU.ts | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d3aede8b861b..a08933688c6c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7857,43 +7857,7 @@ function deleteMoneyRequest(transactionID: string | undefined, reportAction: Ony if (transactionViolations) { removeSettledAndApprovedTransactions( transactionViolations.filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION).flatMap((violation) => violation?.data?.duplicates ?? []), - ).forEach((duplicateID) => { - const duplicateTransactionsViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; - if (!duplicateTransactionsViolations) { - return; - } - - const duplicateViolation = duplicateTransactionsViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); - if (!duplicateViolation?.data?.duplicates) { - return; - } - - const duplicateTransactionIDs = duplicateViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); - - const optimisticViolations: OnyxTypes.TransactionViolations = duplicateTransactionsViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); - - if (duplicateTransactionIDs.length > 0) { - optimisticViolations.push({ - ...duplicateViolation, - data: { - ...duplicateViolation.data, - duplicates: duplicateTransactionIDs, - }, - }); - } - - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: optimisticViolations.length > 0 ? optimisticViolations : null, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: duplicateTransactionsViolations, - }); - }); + ).forEach((duplicateID) => updateDuplicateTransactionViolation(transactionID, duplicateID, optimisticData, failureData)); } if (shouldDeleteTransactionThread) { From fb801ab6bf94d5bd606fbc07bfde81d0ba6bd371 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 27 Jun 2025 18:02:56 +0300 Subject: [PATCH 04/34] prettier --- src/libs/actions/IOU.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a08933688c6c..87f28e1fe686 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4301,7 +4301,9 @@ function getUpdateMoneyRequestParams( (hasModifiedTag || hasModifiedCategory || hasModifiedComment || hasModifiedDistanceRate || hasModifiedAmount || hasModifiedCreated) ) { const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; - const optimisticViolations = hasModifiedAmount ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) : currentTransactionViolations + const optimisticViolations = hasModifiedAmount + ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + : currentTransactionViolations; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( updatedTransaction, optimisticViolations, @@ -4373,12 +4375,7 @@ function getUpdateMoneyRequestParams( * @param optimisticData * @param failureData */ -function updateDuplicateTransactionViolation( - transactionID: string, - duplicateID: string, - optimisticData: OnyxUpdate[], - failureData: OnyxUpdate[], -){ +function updateDuplicateTransactionViolation(transactionID: string, duplicateID: string, optimisticData: OnyxUpdate[], failureData: OnyxUpdate[]) { const duplicateTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; if (!duplicateTransactionViolations) { return; @@ -4414,7 +4411,7 @@ function updateDuplicateTransactionViolation( key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, value: duplicateTransactionViolations, }); -}; +} /** * @param transactionID @@ -7422,16 +7419,9 @@ function updateMoneyRequestAmountAndCurrency({ } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; - ((currentTransactionViolations ?? []).find( - (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION - )?.data?.duplicates ?? []).forEach( - (duplicateID) => updateDuplicateTransactionViolation( - transactionID, - duplicateID, - data.onyxData.optimisticData ?? [], - data.onyxData.failureData ?? [] - ) - ) + ((currentTransactionViolations ?? []).find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? []).forEach((duplicateID) => + updateDuplicateTransactionViolation(transactionID, duplicateID, data.onyxData.optimisticData ?? [], data.onyxData.failureData ?? []), + ); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); From c1bfda2a1f815ca5431f2234542e56b68696c020 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 28 Jun 2025 00:59:58 +0300 Subject: [PATCH 05/34] applied fix to updateMoneyRequestDate --- src/libs/actions/IOU.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 87f28e1fe686..85c74bbb6de8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4369,9 +4369,12 @@ function getUpdateMoneyRequestParams( } /** - * Update duplicates transaction violation - * @param transactionID - * @param duplicateID + * Removes the transactionID from it's duplicates transaction violations data. + * If the duplicateID has no more duplicates, it removes the violation from the Onyx data. + * If the duplicateID has other duplicates, it updates the duplicates list in the violation. + * We need to update the duplicates' violations when a transaction is deleted. + * @param transactionID - The ID of the transaction being deleted or updated. + * @param duplicateI - The ID of the duplicate transaction. * @param optimisticData * @param failureData */ @@ -4386,7 +4389,11 @@ function updateDuplicateTransactionViolation(transactionID: string, duplicateID: return; } + // If the transactionID is not in the duplicates list, we don't need to update the violation const duplicateTransactionIDs = duplicateViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); + if (duplicateTransactionIDs.length === duplicateViolation.data.duplicates.length) { + return; + } const optimisticViolations: OnyxTypes.TransactionViolations = duplicateTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); @@ -4403,7 +4410,7 @@ function updateDuplicateTransactionViolation(transactionID: string, duplicateID: optimisticData.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: optimisticViolations.length > 0 ? optimisticViolations : null, + value: optimisticViolations, }); failureData.push({ @@ -4605,6 +4612,15 @@ function updateMoneyRequestDate( data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + if (currentTransactionViolations) { + const duplicateTransactionViolation = currentTransactionViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates; + if (duplicateTransactionViolation) { + duplicateTransactionViolation.forEach((duplicateID) => + updateDuplicateTransactionViolation(transactionID, duplicateID, data.onyxData.optimisticData ?? [], data.onyxData.failureData ?? []), + ); + } + } } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); @@ -7418,10 +7434,15 @@ function updateMoneyRequestAmountAndCurrency({ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null); - const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; - ((currentTransactionViolations ?? []).find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? []).forEach((duplicateID) => - updateDuplicateTransactionViolation(transactionID, duplicateID, data.onyxData.optimisticData ?? [], data.onyxData.failureData ?? []), - ); + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + if (currentTransactionViolations) { + const duplicateTransactionViolation = currentTransactionViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates; + if (duplicateTransactionViolation) { + duplicateTransactionViolation.forEach((duplicateID) => + updateDuplicateTransactionViolation(transactionID, duplicateID, data.onyxData.optimisticData ?? [], data.onyxData.failureData ?? []), + ); + } + } } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); From b8b1bab10b373c76b1e4bfdf94bc4c9d17f27aa9 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Thu, 3 Jul 2025 02:11:12 +0300 Subject: [PATCH 06/34] clearing up comments --- src/libs/actions/IOU.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cddd1204770b..00ca34ba0634 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4385,12 +4385,10 @@ function getUpdateMoneyRequestParams( } /** - * Removes the transactionID from it's duplicates transaction violations data. - * If the duplicateID has no more duplicates, it removes the violation from the Onyx data. - * If the duplicateID has other duplicates, it updates the duplicates list in the violation. - * We need to update the duplicates' violations when a transaction is deleted. + * Removes the transactionID from its duplicate violations. + * We need to update the duplicate violations when a transaction is deleted. * @param transactionID - The ID of the transaction being deleted or updated. - * @param duplicateI - The ID of the duplicate transaction. + * @param duplicateID - The ID of the duplicate transaction. * @param optimisticData * @param failureData */ From ee7721fc4fd757e7d3550157058092c08504f513 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 3 Jul 2025 02:20:13 +0300 Subject: [PATCH 07/34] prettier --- src/libs/actions/IOU.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 00ca34ba0634..fef2e1ab59d0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7885,8 +7885,8 @@ function deleteMoneyRequest(transactionID: string | undefined, reportAction: Ony .filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) .flatMap((violation) => violation?.data?.duplicates ?? []) .map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); - - removeSettledAndApprovedTransactions(duplicates).forEach((duplicate) => updateDuplicateTransactionViolation(transactionID, duplicateID, optimisticData, failureData)); + + removeSettledAndApprovedTransactions(duplicates).forEach((duplicateID) => updateDuplicateTransactionViolation(transactionID, duplicateID, optimisticData, failureData)); } if (shouldDeleteTransactionThread) { From b2eab4b93e1305740af03981477bf4eeeb18efb9 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 3 Jul 2025 02:45:41 +0300 Subject: [PATCH 08/34] prettier --- src/libs/actions/IOU.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index fef2e1ab59d0..c89365fcc17d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7881,12 +7881,12 @@ function deleteMoneyRequest(transactionID: string | undefined, reportAction: Ony ]; if (transactionViolations) { - const duplicates = transactionViolations - .filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - .flatMap((violation) => violation?.data?.duplicates ?? []) - .map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); - - removeSettledAndApprovedTransactions(duplicates).forEach((duplicateID) => updateDuplicateTransactionViolation(transactionID, duplicateID, optimisticData, failureData)); + removeSettledAndApprovedTransactions( + transactionViolations + .filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap((violation) => violation?.data?.duplicates ?? []) + .map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]), + ).forEach((duplicate) => updateDuplicateTransactionViolation(transactionID, duplicate.transactionID, optimisticData, failureData)); } if (shouldDeleteTransactionThread) { From 7f7df39df0d0e919cf66595b7d3bc39ac766419a Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Thu, 3 Jul 2025 23:19:39 +0300 Subject: [PATCH 09/34] refactoring function into one (#61) * Refactoring `updateDuplicateTransactionViolation` function. * lint * Restored src/index.js --- src/components/MoneyReportHeader.tsx | 4 +- src/components/MoneyRequestHeader.tsx | 4 +- src/libs/TransactionUtils/index.ts | 92 +++++++++++++++ src/libs/actions/IOU.ts | 109 +++++------------- .../iou/request/step/IOURequestStepAmount.tsx | 2 + .../iou/request/step/IOURequestStepDate.tsx | 3 +- tests/actions/IOUTest.ts | 3 + 7 files changed, 130 insertions(+), 87 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 6220246fea56..f95207ca88ec 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -888,6 +888,8 @@ function MoneyReportHeader({ const isMoreContentShown = shouldShowNextStep || !!statusBarProps; + const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); + const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : ''; const unapproveWarningText = useMemo( () => ( @@ -1078,7 +1080,7 @@ function MoneyReportHeader({ } // it's deleting transaction but not the report which leads to bug (that is actually also on staging) // Money request should be deleted when interactions are done, to not show the not found page before navigating to goBackRoute - InteractionManager.runAfterInteractions(() => deleteMoneyRequest(transaction?.transactionID, requestParentReportAction)); + InteractionManager.runAfterInteractions(() => deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, false, allTransactionViolations)); goBackRoute = getNavigationUrlOnMoneyRequestDelete(transaction.transactionID, requestParentReportAction, false); } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 8ef567e79ae1..d28691bacb3d 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -79,7 +79,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre {canBeMissing: true}, ); const transactionViolations = useTransactionViolations(transaction?.transactionID); - + const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: false}); @@ -330,7 +330,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre if (isTrackExpenseAction(parentReportAction)) { deleteTrackExpense(report?.parentReportID, transaction.transactionID, parentReportAction, true); } else { - deleteMoneyRequest(transaction.transactionID, parentReportAction, true); + deleteMoneyRequest(transaction.transactionID, parentReportAction, true, allTransactionViolations); removeTransaction(transaction.transactionID); } onBackButtonPress(); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index b65c84c8db5e..dd51a49fb9c5 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -16,6 +16,7 @@ import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {toLocaleDigit} from '@libs/LocaleDigitUtils'; import {translateLocal} from '@libs/Localize'; +import Log from '@libs/Log'; import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import { @@ -48,6 +49,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; import type {Attendee, Participant, SplitExpense} from '@src/types/onyx/IOU'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {OnyxData} from '@src/types/onyx/Request'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt, TransactionChanges, TransactionCustomUnit, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -1321,6 +1323,95 @@ type FieldsToChange = { billable?: Array; reimbursable?: Array; }; +/** + * Updates the duplicates DUPLICATED_TRANSACTION violation duplicate data by removing the transactionID + * of a transaction that is either being deleted or updated. + * @param transactionID - The ID of the transaction being deleted or updated. + * @param transactionViolations - The collection of all transaction violations. + */ +function updateDuplicatesTransactionsViolations(transactionID: string, transactionViolations: OnyxCollection, onyxData: OnyxData) { + if (!transactionID || !transactionViolations) { + return; + } + + const currentTransactionViolations = transactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + if (!currentTransactionViolations || currentTransactionViolations.length === 0) { + return; + } + + const duplicateIDs = currentTransactionViolations + .filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap((violation) => violation?.data?.duplicates ?? []); + + if (duplicateIDs.length === 0) { + return; + } + + // Filter out transactions assumed that they have be reviewed by removing settled and approved transactions + const duplicates = removeSettledAndApprovedTransactions(Array.from(new Set(duplicateIDs)).map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`])); + + if (duplicates.length === 0) { + return; + } + + duplicates.forEach((duplicate) => { + const duplicateID = duplicate.transactionID; + if (!duplicateID) { + return; + } + const duplicateViolations = transactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; + if (!duplicateViolations) { + return; + } + + const duplicateTransactionViolations = duplicateViolations.filter((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + + // If there are no duplicate transaction violations, we don't need to update the violation + if (duplicateTransactionViolations.length === 0) { + return; + } + + if (duplicateTransactionViolations.length > 1) { + Log.warn(`There are ${duplicateTransactionViolations.length} duplicate transaction violations for transactionID: ${duplicateID}. This should not happen.`); + return; + } + + const duplicateTransactionViolation = duplicateTransactionViolations.at(0); + if (!duplicateTransactionViolation?.data?.duplicates || duplicateTransactionViolation?.data?.duplicates.length === 0) { + return; + } + + // If the transactionID is not in the duplicates list, we don't need to update the violation + const duplicateTransactionIDs = duplicateTransactionViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); + if (duplicateTransactionIDs.length === duplicateTransactionViolation.data.duplicates.length) { + return; + } + + const optimisticViolations = duplicateViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + + if (duplicateTransactionIDs.length > 0) { + optimisticViolations.push({ + ...duplicateTransactionViolation, + data: { + ...duplicateTransactionViolation.data, + duplicates: duplicateTransactionIDs, + }, + }); + } + + onyxData.optimisticData?.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, + value: optimisticViolations, + }); + + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, + value: duplicateViolations, + }); + }); +} function removeSettledAndApprovedTransactions(transactions: Array>): Transaction[] { return transactions.filter((transaction) => !!transaction && !isSettled(transaction?.reportID) && !isReportIDApproved(transaction?.reportID)) as Transaction[]; @@ -1721,6 +1812,7 @@ export { getReimbursable, isPayAtEndExpense, removeSettledAndApprovedTransactions, + updateDuplicatesTransactionsViolations, getCardName, hasReceiptSource, shouldShowAttendees, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c89365fcc17d..5ac29d1288d9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -197,7 +197,7 @@ import { isPerDiemRequest as isPerDiemRequestTransactionUtils, isScanning, isScanRequest as isScanRequestTransactionUtils, - removeSettledAndApprovedTransactions, + updateDuplicatesTransactionsViolations, } from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUActionParams, IOUType} from '@src/CONST'; @@ -4317,9 +4317,11 @@ function getUpdateMoneyRequestParams( (hasModifiedTag || hasModifiedCategory || hasModifiedComment || hasModifiedDistanceRate || hasModifiedAmount || hasModifiedCreated) ) { const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; - const optimisticViolations = hasModifiedAmount - ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - : currentTransactionViolations; + const hasModifiedDate = 'date' in transactionChanges; + const optimisticViolations = + hasModifiedAmount || hasModifiedDate + ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + : currentTransactionViolations; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( updatedTransaction, optimisticViolations, @@ -4384,56 +4386,6 @@ function getUpdateMoneyRequestParams( }; } -/** - * Removes the transactionID from its duplicate violations. - * We need to update the duplicate violations when a transaction is deleted. - * @param transactionID - The ID of the transaction being deleted or updated. - * @param duplicateID - The ID of the duplicate transaction. - * @param optimisticData - * @param failureData - */ -function updateDuplicateTransactionViolation(transactionID: string, duplicateID: string, optimisticData: OnyxUpdate[], failureData: OnyxUpdate[]) { - const duplicateTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; - if (!duplicateTransactionViolations) { - return; - } - - const duplicateViolation = duplicateTransactionViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); - if (!duplicateViolation?.data?.duplicates) { - return; - } - - // If the transactionID is not in the duplicates list, we don't need to update the violation - const duplicateTransactionIDs = duplicateViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); - if (duplicateTransactionIDs.length === duplicateViolation.data.duplicates.length) { - return; - } - - const optimisticViolations: OnyxTypes.TransactionViolations = duplicateTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); - - if (duplicateTransactionIDs.length > 0) { - optimisticViolations.push({ - ...duplicateViolation, - data: { - ...duplicateViolation.data, - duplicates: duplicateTransactionIDs, - }, - }); - } - - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: optimisticViolations, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: duplicateTransactionViolations, - }); -} - /** * @param transactionID * @param transactionThreadReportID @@ -4611,6 +4563,7 @@ function getUpdateTrackExpenseParams( function updateMoneyRequestDate( transactionID: string, transactionThreadReportID: string, + transactionViolations: OnyxCollection, value: string, policy: OnyxEntry, policyTags: OnyxEntry, @@ -4626,15 +4579,7 @@ function updateMoneyRequestDate( data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); - const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - if (currentTransactionViolations) { - const duplicateTransactionViolation = currentTransactionViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates; - if (duplicateTransactionViolation) { - duplicateTransactionViolation.forEach((duplicateID) => - updateDuplicateTransactionViolation(transactionID, duplicateID, data.onyxData.optimisticData ?? [], data.onyxData.failureData ?? []), - ); - } - } + updateDuplicatesTransactionsViolations(transactionID, transactionViolations, data.onyxData); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); @@ -7422,6 +7367,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = { policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; taxCode: string; + transactionViolations: OnyxCollection; }; /** Updates the amount and currency fields of an expense */ @@ -7435,6 +7381,7 @@ function updateMoneyRequestAmountAndCurrency({ policyTagList, policyCategories, taxCode, + transactionViolations, }: UpdateMoneyRequestAmountAndCurrencyParams) { const transactionChanges = { amount, @@ -7449,15 +7396,7 @@ function updateMoneyRequestAmountAndCurrency({ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null); - const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - if (currentTransactionViolations) { - const duplicateTransactionViolation = currentTransactionViolations.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates; - if (duplicateTransactionViolation) { - duplicateTransactionViolation.forEach((duplicateID) => - updateDuplicateTransactionViolation(transactionID, duplicateID, data.onyxData.optimisticData ?? [], data.onyxData.failureData ?? []), - ); - } - } + updateDuplicatesTransactionsViolations(transactionID, transactionViolations, data.onyxData); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); @@ -7833,7 +7772,12 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo * @param isSingleTransactionView - whether we are in the transaction thread report * @return the url to navigate back once the money request is deleted */ -function deleteMoneyRequest(transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { +function deleteMoneyRequest( + transactionID: string | undefined, + reportAction: OnyxTypes.ReportAction, + isSingleTransactionView = false, + violations: OnyxCollection = {}, +) { if (!transactionID) { return; } @@ -7880,14 +7824,7 @@ function deleteMoneyRequest(transactionID: string | undefined, reportAction: Ony }, ]; - if (transactionViolations) { - removeSettledAndApprovedTransactions( - transactionViolations - .filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - .flatMap((violation) => violation?.data?.duplicates ?? []) - .map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]), - ).forEach((duplicate) => updateDuplicateTransactionViolation(transactionID, duplicate.transactionID, optimisticData, failureData)); - } + updateDuplicatesTransactionsViolations(transactionID, violations, {optimisticData, failureData}); if (shouldDeleteTransactionThread) { optimisticData.push( @@ -8129,7 +8066,13 @@ function deleteMoneyRequest(transactionID: string | undefined, reportAction: Ony return urlToNavigateBack; } -function deleteTrackExpense(chatReportID: string | undefined, transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { +function deleteTrackExpense( + chatReportID: string | undefined, + transactionID: string | undefined, + reportAction: OnyxTypes.ReportAction, + isSingleTransactionView = false, + violations: OnyxCollection = {}, +) { if (!chatReportID || !transactionID) { return; } @@ -8139,7 +8082,7 @@ function deleteTrackExpense(chatReportID: string | undefined, transactionID: str // STEP 1: Get all collections we're updating const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; if (!isSelfDM(chatReport)) { - deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView); + deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView, violations); return urlToNavigateBack; } diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index f07a9912235b..7dd65e22647d 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -81,6 +81,7 @@ function IOURequestStepAmount({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; @@ -293,6 +294,7 @@ function IOURequestStepAmount({ updateMoneyRequestAmountAndCurrency({ transactionID, transactionThreadReportID: reportID, + transactionViolations: allTransactionViolations, currency, amount: newAmount, taxAmount, diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/IOURequestStepDate.tsx index c22144639f68..da7f201a5b89 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDate.tsx @@ -42,6 +42,7 @@ function IOURequestStepDate({ const styles = useThemeStyles(); const {translate} = useLocalize(); const policy = usePolicy(report?.policyID); + const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`, {canBeMissing: false}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: false}); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); @@ -107,7 +108,7 @@ function IOURequestStepDate({ setMoneyRequestCreated(transactionID, newCreated, isTransactionDraft); if (isEditing) { - updateMoneyRequestDate(transactionID, reportID, newCreated, policy, policyTags, policyCategories); + updateMoneyRequestDate(transactionID, reportID, allTransactionViolations, newCreated, policy, policyTags, policyCategories); } navigateBack(); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 37d9cea4c6a0..38d10735f7a3 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3475,6 +3475,7 @@ describe('actions/IOU', () => { updateMoneyRequestAmountAndCurrency({ transactionID: transaction.transactionID, transactionThreadReportID: thread.reportID, + transactionViolations: {}, amount: 20000, currency: CONST.CURRENCY.USD, taxAmount: 0, @@ -5664,6 +5665,7 @@ describe('actions/IOU', () => { }, policyTagList: {}, policyCategories: {}, + transactionViolations: {}, }); await waitForBatchedUpdates(); @@ -5722,6 +5724,7 @@ describe('actions/IOU', () => { }, policyTagList: {}, policyCategories: {}, + transactionViolations: {}, }); await waitForBatchedUpdates(); From 4fd141952228b568b027d05d0c155c3429e1041b Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 4 Jul 2025 01:37:35 +0300 Subject: [PATCH 10/34] prettier --- src/components/MoneyReportHeader.tsx | 3 +-- src/components/MoneyRequestHeader.tsx | 2 +- src/pages/iou/request/step/IOURequestStepAmount.tsx | 2 +- src/pages/iou/request/step/IOURequestStepDate.tsx | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index f95207ca88ec..33431145eb25 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -175,6 +175,7 @@ function MoneyReportHeader({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`, { canBeMissing: true, }); + const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const [invoiceReceiverPolicy] = useOnyx( @@ -888,8 +889,6 @@ function MoneyReportHeader({ const isMoreContentShown = shouldShowNextStep || !!statusBarProps; - const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); - const connectedIntegrationName = connectedIntegration ? translate('workspace.accounting.connectionName', {connectionName: connectedIntegration}) : ''; const unapproveWarningText = useMemo( () => ( diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index d28691bacb3d..38e722404653 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -79,7 +79,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre {canBeMissing: true}, ); const transactionViolations = useTransactionViolations(transaction?.transactionID); - const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); + const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: false}); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 7dd65e22647d..8c645d763733 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -81,7 +81,7 @@ function IOURequestStepAmount({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); + const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/IOURequestStepDate.tsx index da7f201a5b89..2be9c4b23d06 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDate.tsx @@ -42,7 +42,7 @@ function IOURequestStepDate({ const styles = useThemeStyles(); const {translate} = useLocalize(); const policy = usePolicy(report?.policyID); - const [allTransactionViolations = {}] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`, {canBeMissing: true}); + const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`, {canBeMissing: false}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: false}); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); From deb5be86d10e69798156d89f0b7ba1417e538b89 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 10 Jul 2025 00:59:16 +0300 Subject: [PATCH 11/34] revert condition --- src/libs/TransactionUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 582db55ed1e4..972f48207021 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1423,7 +1423,7 @@ function updateDuplicatesTransactionsViolations(transactionID: string, transacti onyxData.optimisticData?.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, - value: optimisticViolations, + value: optimisticViolations.length > 0 ? optimisticViolations : null, }); onyxData.failureData?.push({ From 74ee6d8acede9a4e9510486a0667154c1b34c90a Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 15 Jul 2025 23:19:08 +0300 Subject: [PATCH 12/34] lint --- src/components/MoneyReportHeader.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 4 ++-- src/hooks/useSelectedTransactionsActions.ts | 4 +++- src/libs/actions/IOU.ts | 13 ++++++++---- src/pages/ReportDetailsPage.tsx | 5 +++-- .../PopoverReportActionContextMenu.tsx | 11 +++++----- tests/actions/IOUTest.ts | 20 +++++++++---------- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c9857fb19110..9f228c978af0 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1088,7 +1088,7 @@ function MoneyReportHeader({ } // it's deleting transaction but not the report which leads to bug (that is actually also on staging) // Money request should be deleted when interactions are done, to not show the not found page before navigating to goBackRoute - InteractionManager.runAfterInteractions(() => deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, false, allTransactionViolations)); + InteractionManager.runAfterInteractions(() => deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, allTransactionViolations)); goBackRoute = getNavigationUrlOnMoneyRequestDelete(transaction.transactionID, requestParentReportAction, false); } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 009425dbe07e..9a51a0d8f9f4 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -328,9 +328,9 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre throw new Error('Data missing'); } if (isTrackExpenseAction(parentReportAction)) { - deleteTrackExpense(report?.parentReportID, transaction.transactionID, parentReportAction, true); + deleteTrackExpense(report?.parentReportID, transaction.transactionID, parentReportAction, allTransactionViolations, true); } else { - deleteMoneyRequest(transaction.transactionID, parentReportAction, true, allTransactionViolations); + deleteMoneyRequest(transaction.transactionID, parentReportAction, allTransactionViolations, true); removeTransaction(transaction.transactionID); } onBackButtonPress(); diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index f4a9ccd220e3..b60b722fe234 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -45,6 +45,7 @@ function useSelectedTransactionsActions({ }) { const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext(); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); + const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const isReportArchived = useReportIsArchived(report?.reportID); const selectedTransactions = useMemo( () => @@ -62,6 +63,7 @@ function useSelectedTransactionsActions({ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const isTrackExpenseThread = isTrackExpenseReport(report); const isInvoice = isInvoiceReport(report); + let iouType: IOUType = CONST.IOU.TYPE.SUBMIT; if (isTrackExpenseThread) { @@ -87,7 +89,7 @@ function useSelectedTransactionsActions({ return; } - deleteMoneyRequest(transactionID, action, undefined, deletedTransactionIDs); + deleteMoneyRequest(transactionID, action, allTransactionViolations, false, deletedTransactionIDs); deletedTransactionIDs.push(transactionID); }); clearSelectedTransactions(true); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 412ab4ded19f..2153f49644df 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7942,8 +7942,13 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo * @param isSingleTransactionView - whether we are in the transaction thread report * @return the url to navigate back once the money request is deleted */ - -function deleteMoneyRequest(transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false, transactionIDsPendingDeletion?: string[]) { +function deleteMoneyRequest( + transactionID: string | undefined, + reportAction: OnyxTypes.ReportAction, + violations: OnyxCollection, + isSingleTransactionView = false, + transactionIDsPendingDeletion?: string[], +) { if (!transactionID) { return; } @@ -8236,8 +8241,8 @@ function deleteTrackExpense( chatReportID: string | undefined, transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, + violations: OnyxCollection, isSingleTransactionView = false, - violations: OnyxCollection = {}, ) { if (!chatReportID || !transactionID) { return; @@ -8248,7 +8253,7 @@ function deleteTrackExpense( // STEP 1: Get all collections we're updating const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; if (!isSelfDM(chatReport)) { - deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView, violations); + deleteMoneyRequest(transactionID, reportAction, violations, isSingleTransactionView); return urlToNavigateBack; } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index dc98755ef9f0..01e387362c70 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -155,6 +155,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail canBeMissing: true, }); + const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: false}); const {reportActions} = usePaginatedReportActions(report.reportID); @@ -770,9 +771,9 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const isTrackExpense = isTrackExpenseAction(requestParentReportAction); if (isTrackExpense) { - deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, isSingleTransactionView); + deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, allTransactionViolations, isSingleTransactionView); } else { - deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + deleteMoneyRequest(iouTransactionID, requestParentReportAction, allTransactionViolations, isSingleTransactionView); removeTransaction(iouTransactionID); } }, [caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, removeTransaction, report, requestParentReportAction]); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index e13032099ab2..64ede5e566e5 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -54,7 +54,8 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef void)>(undefined); const onCancelDeleteModal = useRef(() => {}); const onConfirmDeleteModal = useRef(() => {}); - + const onPopoverHideActionCallback = useRef(() => {}); const callbackWhenDeleteModalHide = useRef(() => {}); @@ -278,16 +279,16 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); const reportAction = reportActionRef.current; if (isMoneyRequestAction(reportAction)) { const originalMessage = getOriginalMessage(reportAction); if (isTrackExpenseAction(reportAction)) { - deleteTrackExpense(reportIDRef.current, originalMessage?.IOUTransactionID, reportAction); + deleteTrackExpense(reportIDRef.current, originalMessage?.IOUTransactionID, reportAction, allTransactionViolations); } else { - deleteMoneyRequest(originalMessage?.IOUTransactionID, reportAction); + deleteMoneyRequest(originalMessage?.IOUTransactionID, reportAction, allTransactionViolations, false); } } else if (reportAction) { deleteReportComment(reportIDRef.current, reportAction); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 1375cbaf5f5d..4b93edabb674 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3221,7 +3221,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When the expense is deleted - deleteMoneyRequest(transaction?.transactionID, createIOUAction, true); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); } await waitForBatchedUpdates(); @@ -3300,7 +3300,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When the IOU expense is deleted - deleteMoneyRequest(transaction?.transactionID, createIOUAction, true); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); } await waitForBatchedUpdates(); @@ -3361,7 +3361,7 @@ describe('actions/IOU', () => { // When we attempt to delete an expense from the IOU report mockFetch?.pause?.(); if (transaction && createIOUAction) { - deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); } await waitForBatchedUpdates(); @@ -3456,7 +3456,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When Deleting an expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); } await waitForBatchedUpdates(); @@ -3579,7 +3579,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When Deleting an expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, ); } await waitForBatchedUpdates(); @@ -3653,7 +3653,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When deleting expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, ); } await waitForBatchedUpdates(); @@ -3804,7 +3804,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); if (transaction && createIOUAction) { // When we delete the expense - deleteMoneyRequest(transaction.transactionID, createIOUAction, false); + deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, ); } await waitForBatchedUpdates(); @@ -3895,7 +3895,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); jest.advanceTimersByTime(10); if (transaction && createIOUAction) { - deleteMoneyRequest(transaction.transactionID, createIOUAction, false); + deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, ); } await waitForBatchedUpdates(); @@ -3970,7 +3970,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, true); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, false); } let allReports = await new Promise>((resolve) => { @@ -4018,7 +4018,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { // When we delete the expense and we should delete the IOU report - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, false); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, false); } // Then we expect to navigate to the chat report expect(chatReport?.reportID).not.toBeUndefined(); From dbfc6951e131ce0d8092c71a6224d8406799750a Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 15 Jul 2025 23:41:22 +0300 Subject: [PATCH 13/34] prettier --- src/hooks/useSelectedTransactionsActions.ts | 2 +- .../ContextMenu/PopoverReportActionContextMenu.tsx | 9 +++++---- tests/actions/IOUTest.ts | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index b60b722fe234..e7a2691738f8 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -63,7 +63,7 @@ function useSelectedTransactionsActions({ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const isTrackExpenseThread = isTrackExpenseReport(report); const isInvoice = isInvoiceReport(report); - + let iouType: IOUType = CONST.IOU.TYPE.SUBMIT; if (isTrackExpenseThread) { diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 64ede5e566e5..acf170ec5d0e 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -5,7 +5,7 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; import {DeviceEventEmitter, Dimensions} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import {type OnyxEntry, useOnyx} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; @@ -15,6 +15,7 @@ import {deleteReportComment} from '@libs/actions/Report'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import {getOriginalMessage, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {AnchorDimensions} from '@src/styles'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; @@ -55,7 +56,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef void)>(undefined); const onCancelDeleteModal = useRef(() => {}); const onConfirmDeleteModal = useRef(() => {}); - + const onPopoverHideActionCallback = useRef(() => {}); const callbackWhenDeleteModalHide = useRef(() => {}); @@ -279,7 +280,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); const reportAction = reportActionRef.current; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 4b93edabb674..2e5218028127 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3579,7 +3579,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When Deleting an expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, ); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); } await waitForBatchedUpdates(); @@ -3653,7 +3653,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When deleting expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, ); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); } await waitForBatchedUpdates(); @@ -3804,7 +3804,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); if (transaction && createIOUAction) { // When we delete the expense - deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, ); + deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); } await waitForBatchedUpdates(); @@ -3895,7 +3895,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); jest.advanceTimersByTime(10); if (transaction && createIOUAction) { - deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, ); + deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); } await waitForBatchedUpdates(); From 7d5cbd3a24eda6bb331c434a9ca9c2ee3480130f Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 16 Jul 2025 00:02:15 +0300 Subject: [PATCH 14/34] lint --- .../home/report/ContextMenu/PopoverReportActionContextMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index acf170ec5d0e..427270de7496 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -5,7 +5,7 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; import {DeviceEventEmitter, Dimensions} from 'react-native'; -import {type OnyxEntry, useOnyx} from 'react-native-onyx'; +import type { OnyxEntry, useOnyx} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; From 29af4bc7e552696127bcb12d2b0984f7a7a2d45a Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 16 Jul 2025 00:12:02 +0300 Subject: [PATCH 15/34] prettier --- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 427270de7496..300ae6ad8ae4 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -5,7 +5,8 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; import {DeviceEventEmitter, Dimensions} from 'react-native'; -import type { OnyxEntry, useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; @@ -289,7 +290,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef Date: Wed, 16 Jul 2025 00:23:04 +0300 Subject: [PATCH 16/34] fixed tests... --- tests/actions/IOUTest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 2e5218028127..ac3b927ed1dd 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3221,7 +3221,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When the expense is deleted - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, true); } await waitForBatchedUpdates(); @@ -3300,7 +3300,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When the IOU expense is deleted - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, true); } await waitForBatchedUpdates(); @@ -3361,7 +3361,7 @@ describe('actions/IOU', () => { // When we attempt to delete an expense from the IOU report mockFetch?.pause?.(); if (transaction && createIOUAction) { - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); } await waitForBatchedUpdates(); @@ -3456,7 +3456,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When Deleting an expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, false); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); } await waitForBatchedUpdates(); @@ -3970,7 +3970,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, false); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); } let allReports = await new Promise>((resolve) => { @@ -4018,7 +4018,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { // When we delete the expense and we should delete the IOU report - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, false); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); } // Then we expect to navigate to the chat report expect(chatReport?.reportID).not.toBeUndefined(); From 4f2a157de1bd292d039d5e70dadf6d4dcc378f74 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 16 Jul 2025 00:32:14 +0300 Subject: [PATCH 17/34] opps, tests again... --- tests/actions/IOUTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index ac3b927ed1dd..a38df2236200 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3970,7 +3970,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, true); } let allReports = await new Promise>((resolve) => { From a7370fab0b61d3f2b92e26b020fa1c3891fb3cb6 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 17 Jul 2025 02:02:15 +0300 Subject: [PATCH 18/34] applied suggestions --- src/hooks/useSelectedTransactionsActions.ts | 2 +- src/libs/TransactionUtils/index.ts | 101 ++++++++++++++---- src/libs/actions/IOU.ts | 15 +-- src/pages/ReportDetailsPage.tsx | 2 +- .../PopoverReportActionContextMenu.tsx | 2 +- 5 files changed, 93 insertions(+), 29 deletions(-) diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index e7a2691738f8..1c385aaadc83 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -97,7 +97,7 @@ function useSelectedTransactionsActions({ turnOffMobileSelectionMode(); } setIsDeleteModalVisible(false); - }, [allTransactionsLength, reportActions, selectedTransactionIDs, clearSelectedTransactions]); + }, [allTransactionViolations, allTransactionsLength, reportActions, selectedTransactionIDs, clearSelectedTransactions]); const showDeleteModal = useCallback(() => { setIsDeleteModalVisible(true); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index b5914d9ff981..a2986b567c82 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1392,13 +1392,87 @@ type FieldsToChange = { billable?: Array; reimbursable?: Array; }; + /** - * Updates the duplicates DUPLICATED_TRANSACTION violation duplicate data by removing the transactionID - * of a transaction that is either being deleted or updated. + * Extracts a set of valid duplicate transaction IDs associated with a given transaction, + * excluding: + * - the transaction itself + * - duplicate IDs that appear more than once + * - duplicates referencing missing or invalid transactions + * - settled or approved transactions + * + * @param transactionID - The ID of the transaction being validated. + * @param currentTransactionViolations - The list of violations associated with this transaction. + * @returns A set of valid duplicate transaction IDs. + */ +function getValidDuplicateTransactionIDs(transactionID: string, currentTransactionViolations: TransactionViolation[]): Set { + const result = new Set(); + const seen = new Set(); + let foundDuplicateViolation = false; + + for (const violation of currentTransactionViolations) { + if (violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION){ + continue; + } + + // Skip further violations + if (foundDuplicateViolation) { + Log.warn(`Multiple duplicate violations found for transaction ${transactionID}. Only one expected.`); + continue; + } + + foundDuplicateViolation = true; + + const duplicatesID = violation?.data?.duplicates; + if (!duplicatesID || duplicatesID.length === 0) { + Log.warn(`Violation ${violation.name} lacks duplicates. Transaction ID: ${transactionID}`); + break; + } + + const validTransactions: Transaction[] = []; + + for (const duplicateID of duplicatesID) { + // Skip self-reference + if (duplicateID === transactionID) { + continue; + } + + if (seen.has(duplicateID)) { + Log.warn(`Duplicate ID ${duplicateID} is repeated in violation for transaction ${transactionID}.`); + continue; + } + + seen.add(duplicateID); + + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`]; + if (!transaction?.transactionID) { + Log.warn(`Transaction ${duplicateID} does not exist or is invalid. Found in transaction ${transactionID}.`); + continue; + } + + validTransactions.push(transaction); + } + + // Filter out transactions assumed that they have be reviewed by removing settled and approved transactions + const filtered = removeSettledAndApprovedTransactions(validTransactions); + + for (const transaction of filtered) { + result.add(transaction.transactionID); + } + } + + return result; +} + +/** + * Adds onyx updates to the passed onyxData to update the DUPLICATED_TRANSACTION violation data + * by removing the passed transactionID from any violation that referenced it. + * @param onyxData - An object to store optimistic and failure updates. * @param transactionID - The ID of the transaction being deleted or updated. * @param transactionViolations - The collection of all transaction violations. + * */ -function updateDuplicatesTransactionsViolations(transactionID: string, transactionViolations: OnyxCollection, onyxData: OnyxData) { +function updateDuplicatesTransactionsViolations(onyxData: OnyxData, transactionID: string, transactionViolations: OnyxCollection) { if (!transactionID || !transactionViolations) { return; } @@ -1408,26 +1482,13 @@ function updateDuplicatesTransactionsViolations(transactionID: string, transacti return; } - const duplicateIDs = currentTransactionViolations - .filter((violation) => violation?.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - .flatMap((violation) => violation?.data?.duplicates ?? []); + const duplicateIDs = getValidDuplicateTransactionIDs(transactionID, currentTransactionViolations); - if (duplicateIDs.length === 0) { + if (duplicateIDs.size === 0) { return; } - // Filter out transactions assumed that they have be reviewed by removing settled and approved transactions - const duplicates = removeSettledAndApprovedTransactions(Array.from(new Set(duplicateIDs)).map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`])); - - if (duplicates.length === 0) { - return; - } - - duplicates.forEach((duplicate) => { - const duplicateID = duplicate.transactionID; - if (!duplicateID) { - return; - } + for (const duplicateID of duplicateIDs) { const duplicateViolations = transactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; if (!duplicateViolations) { return; @@ -1479,7 +1540,7 @@ function updateDuplicatesTransactionsViolations(transactionID: string, transacti key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`, value: duplicateViolations, }); - }); + } } function removeSettledAndApprovedTransactions(transactions: Array>): Transaction[] { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7d6ebe61dfe6..30d8d9fba2bb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4327,18 +4327,21 @@ function getUpdateMoneyRequestParams( value: {...iouReport, ...(isTotalIndeterminate && {pendingFields: {total: null}})}, }); } + + const hasModifiedCurrency = 'currency' in transactionChanges; const hasModifiedComment = 'comment' in transactionChanges; + const hasModifiedDate = 'date' in transactionChanges; if ( policy && isPaidGroupPolicy(policy) && updatedTransaction && - (hasModifiedTag || hasModifiedCategory || hasModifiedComment || hasModifiedDistanceRate || hasModifiedAmount || hasModifiedCreated) + (hasModifiedTag || hasModifiedCategory || hasModifiedComment || hasModifiedDistanceRate || hasModifiedDate || hasModifiedCurrency || hasModifiedAmount || hasModifiedCreated) ) { const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; - const hasModifiedDate = 'date' in transactionChanges; + // If the amount, currency or date have been modified, we remove the duplicate violations since they would be out of date as the transaction has changed const optimisticViolations = - hasModifiedAmount || hasModifiedDate + hasModifiedAmount || hasModifiedDate || hasModifiedCurrency ? currentTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) : currentTransactionViolations; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( @@ -4598,7 +4601,7 @@ function updateMoneyRequestDate( data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); - updateDuplicatesTransactionsViolations(transactionID, transactionViolations, data.onyxData); + updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactionViolations); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); @@ -7570,7 +7573,7 @@ function updateMoneyRequestAmountAndCurrency({ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null); - updateDuplicatesTransactionsViolations(transactionID, transactionViolations, data.onyxData); + updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactionViolations); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); @@ -7999,7 +8002,7 @@ function deleteMoneyRequest( }, ]; - updateDuplicatesTransactionsViolations(transactionID, violations, {optimisticData, failureData}); + updateDuplicatesTransactionsViolations({optimisticData, failureData}, transactionID, violations); if (shouldDeleteTransactionThread) { optimisticData.push( diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index bf5b6fad6aef..d9c6a4339ec6 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -777,7 +777,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail deleteMoneyRequest(iouTransactionID, requestParentReportAction, allTransactionViolations, isSingleTransactionView); removeTransaction(iouTransactionID); } - }, [caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, removeTransaction, report, requestParentReportAction]); + }, [allTransactionViolations, caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, removeTransaction, report, requestParentReportAction]); // A flag to indicate whether the user chose to delete the transaction or not const isTransactionDeleted = useRef(false); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 300ae6ad8ae4..e8bf233d8a00 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -298,7 +298,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); From 9e6a00300b9a563d5c8a9d173ce4f855653b2911 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 17 Jul 2025 03:21:59 +0300 Subject: [PATCH 19/34] lint --- src/components/MoneyReportHeader.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 2 +- src/hooks/useSelectedTransactionsActions.ts | 2 +- src/libs/TransactionUtils/index.ts | 8 ++++---- src/pages/ReportDetailsPage.tsx | 2 +- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 2 +- src/pages/iou/request/step/IOURequestStepAmount.tsx | 2 +- src/pages/iou/request/step/IOURequestStepDate.tsx | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2cc16580c729..13c4367e7e21 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -181,7 +181,7 @@ function MoneyReportHeader({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`, { canBeMissing: true, }); - const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const [invoiceReceiverPolicy] = useOnyx( diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 6d921e3f08ff..0a224f956fe0 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -80,7 +80,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre {canBeMissing: true}, ); const transactionViolations = useTransactionViolations(transaction?.transactionID); - const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {canBeMissing: false}); diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 1c385aaadc83..690c4453e37a 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -45,7 +45,7 @@ function useSelectedTransactionsActions({ }) { const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext(); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); - const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const isReportArchived = useReportIsArchived(report?.reportID); const selectedTransactions = useMemo( () => diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 4b7f6ac66256..489beebb8d26 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1413,14 +1413,14 @@ function getValidDuplicateTransactionIDs(transactionID: string, currentTransacti let foundDuplicateViolation = false; for (const violation of currentTransactionViolations) { - if (violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION){ + if (violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) { continue; } // Skip further violations if (foundDuplicateViolation) { Log.warn(`Multiple duplicate violations found for transaction ${transactionID}. Only one expected.`); - continue; + continue; } foundDuplicateViolation = true; @@ -1436,7 +1436,7 @@ function getValidDuplicateTransactionIDs(transactionID: string, currentTransacti for (const duplicateID of duplicatesID) { // Skip self-reference if (duplicateID === transactionID) { - continue; + continue; } if (seen.has(duplicateID)) { @@ -1467,7 +1467,7 @@ function getValidDuplicateTransactionIDs(transactionID: string, currentTransacti } /** - * Adds onyx updates to the passed onyxData to update the DUPLICATED_TRANSACTION violation data + * Adds onyx updates to the passed onyxData to update the DUPLICATED_TRANSACTION violation data * by removing the passed transactionID from any violation that referenced it. * @param onyxData - An object to store optimistic and failure updates. * @param transactionID - The ID of the transaction being deleted or updated. diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index d9c6a4339ec6..428547170872 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -156,7 +156,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail canBeMissing: true, }); - const [allTransactionViolations = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: false}); const {reportActions} = usePaginatedReportActions(report.reportID); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index e8bf233d8a00..cfecdfe799de 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -56,7 +56,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef Date: Fri, 25 Jul 2025 02:21:29 +0300 Subject: [PATCH 20/34] useDuplicateTransactionsAndViolations --- src/components/MoneyReportHeader.tsx | 7 +- src/components/MoneyRequestHeader.tsx | 9 +- .../useDuplicateTransactionsAndViolations.ts | 130 ++++++++++++++++++ src/hooks/useSelectedTransactionsActions.ts | 7 +- src/libs/TransactionUtils/index.ts | 50 ++++--- src/libs/actions/IOU.ts | 13 +- src/pages/ReportDetailsPage.tsx | 20 ++- .../PopoverReportActionContextMenu.tsx | 13 +- .../iou/request/step/IOURequestStepAmount.tsx | 6 +- .../iou/request/step/IOURequestStepDate.tsx | 5 +- 10 files changed, 212 insertions(+), 48 deletions(-) create mode 100644 src/hooks/useDuplicateTransactionsAndViolations.ts diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index dae353c733d6..573d03083328 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'reac import {ActivityIndicator, InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -183,7 +184,7 @@ function MoneyReportHeader({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`, { canBeMissing: true, }); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID)); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const [invoiceReceiverPolicy] = useOnyx( @@ -1115,7 +1116,9 @@ function MoneyReportHeader({ } // it's deleting transaction but not the report which leads to bug (that is actually also on staging) // Money request should be deleted when interactions are done, to not show the not found page before navigating to goBackRoute - InteractionManager.runAfterInteractions(() => deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, allTransactionViolations)); + InteractionManager.runAfterInteractions(() => + deleteMoneyRequest(transaction?.transactionID, requestParentReportAction, duplicateTransactions, duplicateTransactionViolations), + ); goBackRoute = getNavigationUrlOnMoneyRequestDelete(transaction.transactionID, requestParentReportAction, false); } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 04ab30ff5a68..ff34cfc0fba6 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -4,6 +4,7 @@ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'reac import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useLoadingBarVisibility from '@hooks/useLoadingBarVisibility'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -79,8 +80,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre }`, {canBeMissing: true}, ); - const transactionViolations = useTransactionViolations(transaction?.transactionID); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const transactionViolations = useTransactionViolations(); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transaction?.transactionID ? [transaction.transactionID] : []); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {canBeMissing: true}); @@ -334,9 +335,9 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre throw new Error('Data missing'); } if (isTrackExpenseAction(parentReportAction)) { - deleteTrackExpense(report?.parentReportID, transaction.transactionID, parentReportAction, allTransactionViolations, true); + deleteTrackExpense(report?.parentReportID, transaction.transactionID, parentReportAction, duplicateTransactions, duplicateTransactionViolations, true); } else { - deleteMoneyRequest(transaction.transactionID, parentReportAction, allTransactionViolations, true); + deleteMoneyRequest(transaction.transactionID, parentReportAction, duplicateTransactions, duplicateTransactionViolations, true); removeTransaction(transaction.transactionID); } onBackButtonPress(); diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts new file mode 100644 index 000000000000..9ea4190e0d65 --- /dev/null +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -0,0 +1,130 @@ +import ONYXKEYS from "@src/ONYXKEYS"; +import { OnyxCollection, useOnyx } from "react-native-onyx"; +import type { Transaction, TransactionViolations } from "@src/types/onyx"; +import CONST from "@src/CONST"; +import { useMemo } from "react"; + +/** + * Selects violations related to provided transaction IDs and if present, the violations of their duplicates. + * @param {string[]} transactionIDs - An array of transaction IDs to fetch their violations for. + * @param {OnyxCollection} allViolations - A collection of all transaction violations currently in the onyx. + * @returns {OnyxCollection} - A collection of violations related to the transaction IDs and if present, the violations of their duplicates. + * @private + */ +function selectViolationsWithDuplicates( + transactionIDs: string[], + allViolations: OnyxCollection +): OnyxCollection { + if (!allViolations) { + return {}; + } + + const result: OnyxCollection = {}; + + for (const transactionID of transactionIDs) { + const key = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; + const violations = allViolations[key]; + + if (!violations) { + continue; + } + + result[key] = violations; + + violations + .filter(violations => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap(violations => violations?.data?.duplicates ?? []) + .forEach(duplicateID => { + if (!duplicateID) { + return; + } + + const duplicateKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`; + const duplicateViolations = allViolations[duplicateKey]; + + if (duplicateViolations) { + result[duplicateKey] = duplicateViolations; + } + }); + } + + return result; +} + +/** + * Hook to fetch transactions, their violations and if present, the duplicate transactions and their violations. + * @param {string[]} transactionIDs - Array of transaction IDs to check for duplicates. + * @returns {Object} - An object containing duplicate transactions and their violations. + * @property {OnyxCollection} duplicateTransactions - Collection of duplicate transactions. + * @property {OnyxCollection} duplicateTransactionViolations - Collection of violations related to duplicate transactions. + */ +function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { + + const violationsSelectorMemo = useMemo(() => { + return (allViolations: OnyxCollection) => selectViolationsWithDuplicates(transactionIDs, allViolations); + }, [transactionIDs.join(',')]); + + const [duplicateTransactionViolations] = useOnyx( + ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + { + canBeMissing: true, + selector: violationsSelectorMemo, + } + ); + + const transactionSelector = useMemo(() => { + return (allTransactions: OnyxCollection) => { + if (!allTransactions) { + return {}; + } + + const result: OnyxCollection = {}; + + for (const transactionID of transactionIDs) { + const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; + const transaction = allTransactions[key]; + if (transaction) result[key] = transaction; + + const violationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; + const violations = duplicateTransactionViolations?.[violationsKey]; + + if (!violations){ + continue; + } + + violations + .filter(violations => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap(violations => violations?.data?.duplicates ?? []) + .forEach(duplicateID => { + if (!duplicateID){ + return; + } + + const duplicateKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`; + const duplicateTransaction = allTransactions[duplicateKey]; + + if (duplicateTransaction) { + result[duplicateKey] = duplicateTransaction; + } + }); + } + return result; + }; + }, [transactionIDs.join(','), duplicateTransactionViolations]); + + const [duplicateTransactions] = useOnyx( + ONYXKEYS.COLLECTION.TRANSACTION, + { + canBeMissing: true, + selector: transactionSelector, + } + ); + + return useMemo(() => ({ + duplicateTransactions, + duplicateTransactionViolations, + }), [duplicateTransactions, duplicateTransactionViolations]); + +} + +export default useDuplicateTransactionsAndViolations; diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 258d49f71e6e..be7483fad8e9 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -21,6 +21,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OriginalMessageIOU, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; +import useDuplicateTransactionsAndViolations from './useDuplicateTransactionsAndViolations'; import useLocalize from './useLocalize'; import useOnyx from './useOnyx'; import useReportIsArchived from './useReportIsArchived'; @@ -45,7 +46,7 @@ function useSelectedTransactionsActions({ }) { const {selectedTransactionIDs, clearSelectedTransactions} = useSearchContext(); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(selectedTransactionIDs); const isReportArchived = useReportIsArchived(report?.reportID); const selectedTransactions = useMemo( () => @@ -89,7 +90,7 @@ function useSelectedTransactionsActions({ return; } - deleteMoneyRequest(transactionID, action, allTransactionViolations, false, deletedTransactionIDs); + deleteMoneyRequest(transactionID, action, duplicateTransactions, duplicateTransactionViolations, false, deletedTransactionIDs); deletedTransactionIDs.push(transactionID); }); clearSelectedTransactions(true); @@ -97,7 +98,7 @@ function useSelectedTransactionsActions({ turnOffMobileSelectionMode(); } setIsDeleteModalVisible(false); - }, [allTransactionViolations, allTransactionsLength, reportActions, selectedTransactionIDs, clearSelectedTransactions]); + }, [duplicateTransactions, duplicateTransactionViolations, allTransactionsLength, reportActions, selectedTransactionIDs, clearSelectedTransactions]); const showDeleteModal = useCallback(() => { setIsDeleteModalVisible(true); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index c6d14106629b..6909e0353755 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1399,14 +1399,19 @@ type FieldsToChange = { * - settled or approved transactions * * @param transactionID - The ID of the transaction being validated. + * @param transactionCollection - A collection of all transactions and their duplicates. * @param currentTransactionViolations - The list of violations associated with this transaction. * @returns A set of valid duplicate transaction IDs. */ -function getValidDuplicateTransactionIDs(transactionID: string, currentTransactionViolations: TransactionViolation[]): Set { +function getValidDuplicateTransactionIDs(transactionID: string, transactionCollection: OnyxCollection, currentTransactionViolations: TransactionViolation[]): Set { const result = new Set(); const seen = new Set(); let foundDuplicateViolation = false; + if (!transactionCollection) { + return result; + } + for (const violation of currentTransactionViolations) { if (violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION) { continue; @@ -1441,7 +1446,7 @@ function getValidDuplicateTransactionIDs(transactionID: string, currentTransacti seen.add(duplicateID); - const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`]; + const transaction = transactionCollection?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`]; if (!transaction?.transactionID) { Log.warn(`Transaction ${duplicateID} does not exist or is invalid. Found in transaction ${transactionID}.`); continue; @@ -1466,55 +1471,57 @@ function getValidDuplicateTransactionIDs(transactionID: string, currentTransacti * by removing the passed transactionID from any violation that referenced it. * @param onyxData - An object to store optimistic and failure updates. * @param transactionID - The ID of the transaction being deleted or updated. - * @param transactionViolations - The collection of all transaction violations. + * @param transactions - A collection of all transactions and their duplicates. + * @param transactionViolations - The collection of the transaction violations incuding the duplicates violations. * */ -function updateDuplicatesTransactionsViolations(onyxData: OnyxData, transactionID: string, transactionViolations: OnyxCollection) { - if (!transactionID || !transactionViolations) { +function updateDuplicatesTransactionsViolations( + onyxData: OnyxData, + transactionID: string, + transactions: OnyxCollection, + transactionViolations: OnyxCollection, +) { + if (!transactionID || !transactions || !transactionViolations) { return; } + const violations = transactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - const currentTransactionViolations = transactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - if (!currentTransactionViolations || currentTransactionViolations.length === 0) { + if (!violations) { return; } - const duplicateIDs = getValidDuplicateTransactionIDs(transactionID, currentTransactionViolations); - - if (duplicateIDs.size === 0) { - return; - } + const duplicateIDs = getValidDuplicateTransactionIDs(transactionID, transactions, violations); for (const duplicateID of duplicateIDs) { const duplicateViolations = transactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`]; + if (!duplicateViolations) { - return; + continue; } const duplicateTransactionViolations = duplicateViolations.filter((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); - // If there are no duplicate transaction violations, we don't need to update the violation - if (duplicateTransactionViolations.length === 0) { - return; + if (duplicateTransactionViolations.length == 0) { + continue; } if (duplicateTransactionViolations.length > 1) { Log.warn(`There are ${duplicateTransactionViolations.length} duplicate transaction violations for transactionID: ${duplicateID}. This should not happen.`); - return; + continue; } const duplicateTransactionViolation = duplicateTransactionViolations.at(0); - if (!duplicateTransactionViolation?.data?.duplicates || duplicateTransactionViolation?.data?.duplicates.length === 0) { - return; + if (!duplicateTransactionViolation?.data?.duplicates) { + continue; } // If the transactionID is not in the duplicates list, we don't need to update the violation const duplicateTransactionIDs = duplicateTransactionViolation.data.duplicates.filter((duplicateTransactionID) => duplicateTransactionID !== transactionID); if (duplicateTransactionIDs.length === duplicateTransactionViolation.data.duplicates.length) { - return; + continue; } - const optimisticViolations = duplicateViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + const optimisticViolations = duplicateTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); if (duplicateTransactionIDs.length > 0) { optimisticViolations.push({ @@ -1899,6 +1906,7 @@ export { isReceiptBeingScanned, didReceiptScanSucceed, getValidWaypoints, + getValidDuplicateTransactionIDs, isDistanceRequest, isFetchingWaypointsFromServer, isExpensifyCardTransaction, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f649eea87e6a..a5771091475a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4569,6 +4569,7 @@ function getUpdateTrackExpenseParams( function updateMoneyRequestDate( transactionID: string, transactionThreadReportID: string, + transactions: OnyxCollection, transactionViolations: OnyxCollection, value: string, policy: OnyxEntry, @@ -4585,7 +4586,7 @@ function updateMoneyRequestDate( data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); - updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactionViolations); + updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactions, transactionViolations); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); @@ -7528,6 +7529,7 @@ type UpdateMoneyRequestAmountAndCurrencyParams = { policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; taxCode: string; + transactions: OnyxCollection; transactionViolations: OnyxCollection; }; @@ -7542,6 +7544,7 @@ function updateMoneyRequestAmountAndCurrency({ policyTagList, policyCategories, taxCode, + transactions, transactionViolations, }: UpdateMoneyRequestAmountAndCurrencyParams) { const transactionChanges = { @@ -7557,7 +7560,7 @@ function updateMoneyRequestAmountAndCurrency({ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null); - updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactionViolations); + updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactions, transactionViolations); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); @@ -7936,6 +7939,7 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo function deleteMoneyRequest( transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, + transactions: OnyxCollection, violations: OnyxCollection, isSingleTransactionView = false, transactionIDsPendingDeletion?: string[], @@ -7986,7 +7990,7 @@ function deleteMoneyRequest( }, ]; - updateDuplicatesTransactionsViolations({optimisticData, failureData}, transactionID, violations); + updateDuplicatesTransactionsViolations({optimisticData, failureData}, transactionID, transactions, violations); if (shouldDeleteTransactionThread) { optimisticData.push( @@ -8232,6 +8236,7 @@ function deleteTrackExpense( chatReportID: string | undefined, transactionID: string | undefined, reportAction: OnyxTypes.ReportAction, + transactions: OnyxCollection, violations: OnyxCollection, isSingleTransactionView = false, ) { @@ -8244,7 +8249,7 @@ function deleteTrackExpense( // STEP 1: Get all collections we're updating const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; if (!isSelfDM(chatReport)) { - deleteMoneyRequest(transactionID, reportAction, violations, isSingleTransactionView); + deleteMoneyRequest(transactionID, reportAction, transactions, violations, isSingleTransactionView); return urlToNavigateBack; } diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 0f7e3aa46679..c3a822c7c709 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -24,6 +24,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; import TextWithCopy from '@components/TextWithCopy'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -155,8 +156,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail selector: (actions) => (report?.parentReportActionID ? actions?.[report.parentReportActionID] : undefined), canBeMissing: true, }); - - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: false}); const {reportActions} = usePaginatedReportActions(report.reportID); @@ -281,6 +280,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const canDeleteRequest = isActionOwner && (canDeleteTransaction(moneyRequestReport, isMoneyRequestReportArchived) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${iouTransactionID}`, {canBeMissing: true}); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(iouTransactionID ? [iouTransactionID] : []); const isCardTransactionCanBeDeleted = canDeleteCardTransactionByLiabilityType(iouTransaction); const shouldShowDeleteButton = shouldShowTaskDeleteButton || (canDeleteRequest && isCardTransactionCanBeDeleted) || isDemoTransaction(iouTransaction); @@ -772,12 +772,22 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const isTrackExpense = isTrackExpenseAction(requestParentReportAction); if (isTrackExpense) { - deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, allTransactionViolations, isSingleTransactionView); + deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, duplicateTransactions, duplicateTransactionViolations, isSingleTransactionView); } else { - deleteMoneyRequest(iouTransactionID, requestParentReportAction, allTransactionViolations, isSingleTransactionView); + deleteMoneyRequest(iouTransactionID, requestParentReportAction, duplicateTransactions, duplicateTransactionViolations, isSingleTransactionView); removeTransaction(iouTransactionID); } - }, [allTransactionViolations, caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, removeTransaction, report, requestParentReportAction]); + }, [ + duplicateTransactions, + duplicateTransactionViolations, + caseID, + iouTransactionID, + isSingleTransactionView, + moneyRequestReport?.reportID, + removeTransaction, + report, + requestParentReportAction, + ]); // A flag to indicate whether the user chose to delete the transaction or not const isTransactionDeleted = useRef(false); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index d1e1cc62ccde..cc87e270a92a 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -10,6 +10,7 @@ import {useOnyx} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useLocalize from '@hooks/useLocalize'; import {deleteMoneyRequest, deleteTrackExpense} from '@libs/actions/IOU'; import {deleteReportComment} from '@libs/actions/Report'; @@ -56,8 +57,6 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); const reportAction = reportActionRef.current; if (isMoneyRequestAction(reportAction)) { const originalMessage = getOriginalMessage(reportAction); if (isTrackExpenseAction(reportAction)) { - deleteTrackExpense(reportIDRef.current, originalMessage?.IOUTransactionID, reportAction, allTransactionViolations); + deleteTrackExpense(reportIDRef.current, originalMessage?.IOUTransactionID, reportAction, duplicateTransactions, duplicateTransactionViolations); } else { - deleteMoneyRequest(originalMessage?.IOUTransactionID, reportAction, allTransactionViolations); + deleteMoneyRequest(originalMessage?.IOUTransactionID, reportAction, duplicateTransactions, duplicateTransactionViolations); } } else if (reportAction) { InteractionManager.runAfterInteractions(() => { @@ -300,7 +303,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 5ad02e682aae..b7104b0539fd 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {createDraftTransaction, removeDraftTransaction} from '@libs/actions/TransactionEdit'; @@ -81,7 +82,7 @@ function IOURequestStepAmount({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionID ? [transactionID] : []); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; @@ -294,7 +295,8 @@ function IOURequestStepAmount({ updateMoneyRequestAmountAndCurrency({ transactionID, transactionThreadReportID: reportID, - transactionViolations: allTransactionViolations, + transactions: duplicateTransactions, + transactionViolations: duplicateTransactionViolations, currency, amount: newAmount, taxAmount, diff --git a/src/pages/iou/request/step/IOURequestStepDate.tsx b/src/pages/iou/request/step/IOURequestStepDate.tsx index a6837993fab1..913355272051 100644 --- a/src/pages/iou/request/step/IOURequestStepDate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDate.tsx @@ -5,6 +5,7 @@ import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; @@ -42,7 +43,7 @@ function IOURequestStepDate({ const styles = useThemeStyles(); const {translate} = useLocalize(); const policy = usePolicy(report?.policyID); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionID ? [transactionID] : []); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`, {canBeMissing: false}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: false}); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); @@ -108,7 +109,7 @@ function IOURequestStepDate({ setMoneyRequestCreated(transactionID, newCreated, isTransactionDraft); if (isEditing) { - updateMoneyRequestDate(transactionID, reportID, allTransactionViolations, newCreated, policy, policyTags, policyCategories); + updateMoneyRequestDate(transactionID, reportID, duplicateTransactions, duplicateTransactionViolations, newCreated, policy, policyTags, policyCategories); } navigateBack(); From 76a83adbfe26188e7b0f9ea0e2328b16cdb04df4 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 25 Jul 2025 02:58:27 +0300 Subject: [PATCH 21/34] prettier... --- .../useDuplicateTransactionsAndViolations.ts | 68 ++++++++----------- src/libs/TransactionUtils/index.ts | 2 +- .../PopoverReportActionContextMenu.tsx | 17 +++-- tests/actions/IOUTest.ts | 24 ++++--- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 9ea4190e0d65..5e7ee9cfbba3 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -1,8 +1,8 @@ -import ONYXKEYS from "@src/ONYXKEYS"; -import { OnyxCollection, useOnyx } from "react-native-onyx"; -import type { Transaction, TransactionViolations } from "@src/types/onyx"; -import CONST from "@src/CONST"; -import { useMemo } from "react"; +import {useMemo} from 'react'; +import {OnyxCollection, useOnyx} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Transaction, TransactionViolations} from '@src/types/onyx'; /** * Selects violations related to provided transaction IDs and if present, the violations of their duplicates. @@ -11,10 +11,7 @@ import { useMemo } from "react"; * @returns {OnyxCollection} - A collection of violations related to the transaction IDs and if present, the violations of their duplicates. * @private */ -function selectViolationsWithDuplicates( - transactionIDs: string[], - allViolations: OnyxCollection -): OnyxCollection { +function selectViolationsWithDuplicates(transactionIDs: string[], allViolations: OnyxCollection): OnyxCollection { if (!allViolations) { return {}; } @@ -32,9 +29,9 @@ function selectViolationsWithDuplicates( result[key] = violations; violations - .filter(violations => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - .flatMap(violations => violations?.data?.duplicates ?? []) - .forEach(duplicateID => { + .filter((violations) => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap((violations) => violations?.data?.duplicates ?? []) + .forEach((duplicateID) => { if (!duplicateID) { return; } @@ -59,19 +56,15 @@ function selectViolationsWithDuplicates( * @property {OnyxCollection} duplicateTransactionViolations - Collection of violations related to duplicate transactions. */ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { - const violationsSelectorMemo = useMemo(() => { return (allViolations: OnyxCollection) => selectViolationsWithDuplicates(transactionIDs, allViolations); }, [transactionIDs.join(',')]); - const [duplicateTransactionViolations] = useOnyx( - ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - { - canBeMissing: true, - selector: violationsSelectorMemo, - } - ); - + const [duplicateTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, { + canBeMissing: true, + selector: violationsSelectorMemo, + }); + const transactionSelector = useMemo(() => { return (allTransactions: OnyxCollection) => { if (!allTransactions) { @@ -88,15 +81,15 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { const violationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; const violations = duplicateTransactionViolations?.[violationsKey]; - if (!violations){ + if (!violations) { continue; } violations - .filter(violations => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - .flatMap(violations => violations?.data?.duplicates ?? []) - .forEach(duplicateID => { - if (!duplicateID){ + .filter((violations) => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap((violations) => violations?.data?.duplicates ?? []) + .forEach((duplicateID) => { + if (!duplicateID) { return; } @@ -112,19 +105,18 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { }; }, [transactionIDs.join(','), duplicateTransactionViolations]); - const [duplicateTransactions] = useOnyx( - ONYXKEYS.COLLECTION.TRANSACTION, - { - canBeMissing: true, - selector: transactionSelector, - } + const [duplicateTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, { + canBeMissing: true, + selector: transactionSelector, + }); + + return useMemo( + () => ({ + duplicateTransactions, + duplicateTransactionViolations, + }), + [duplicateTransactions, duplicateTransactionViolations], ); - - return useMemo(() => ({ - duplicateTransactions, - duplicateTransactionViolations, - }), [duplicateTransactions, duplicateTransactionViolations]); - } export default useDuplicateTransactionsAndViolations; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 6909e0353755..fbbd52008eab 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1472,7 +1472,7 @@ function getValidDuplicateTransactionIDs(transactionID: string, transactionColle * @param onyxData - An object to store optimistic and failure updates. * @param transactionID - The ID of the transaction being deleted or updated. * @param transactions - A collection of all transactions and their duplicates. - * @param transactionViolations - The collection of the transaction violations incuding the duplicates violations. + * @param transactionViolations - The collection of the transaction violations including the duplicates violations. * */ function updateDuplicatesTransactionsViolations( diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index cc87e270a92a..2c53d4af253d 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -6,7 +6,6 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; import {DeviceEventEmitter, Dimensions, InteractionManager} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; @@ -17,7 +16,6 @@ import {deleteReportComment} from '@libs/actions/Report'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import {getOriginalMessage, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {AnchorDimensions} from '@src/styles'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; @@ -281,9 +279,18 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef([]); + + useEffect(() => { + const originalMessage = getOriginalMessage(reportActionRef.current); + if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { + setTransactionIDs([originalMessage.IOUTransactionID]); + return; + } + setTransactionIDs([]); + }, [reportActionRef.current]); + + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionIDs); const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 10c4b516980a..1fb4d9308ec7 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3223,7 +3223,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When the expense is deleted - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, true); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, {}, true); } await waitForBatchedUpdates(); @@ -3302,7 +3302,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When the IOU expense is deleted - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, true); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, {}, true); } await waitForBatchedUpdates(); @@ -3363,7 +3363,7 @@ describe('actions/IOU', () => { // When we attempt to delete an expense from the IOU report mockFetch?.pause?.(); if (transaction && createIOUAction) { - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, {}); } await waitForBatchedUpdates(); @@ -3458,7 +3458,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When Deleting an expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, {}); } await waitForBatchedUpdates(); @@ -3532,6 +3532,7 @@ describe('actions/IOU', () => { createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => isMoneyRequestAction(reportAction), ); + expect(createIOUAction?.childReportID).toBe(thread.reportID); await waitForBatchedUpdates(); @@ -3540,6 +3541,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { updateMoneyRequestAmountAndCurrency({ transactionID: transaction.transactionID, + transactions: {}, transactionThreadReportID: thread.reportID, transactionViolations: {}, amount: 20000, @@ -3581,7 +3583,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When Deleting an expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, {}); } await waitForBatchedUpdates(); @@ -3655,7 +3657,7 @@ describe('actions/IOU', () => { if (transaction && createIOUAction) { // When deleting expense - deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}); + deleteMoneyRequest(transaction?.transactionID, createIOUAction, {}, {}); } await waitForBatchedUpdates(); @@ -3806,7 +3808,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); if (transaction && createIOUAction) { // When we delete the expense - deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); + deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, {}); } await waitForBatchedUpdates(); @@ -3897,7 +3899,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); jest.advanceTimersByTime(10); if (transaction && createIOUAction) { - deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); + deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, {}); } await waitForBatchedUpdates(); @@ -3972,7 +3974,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, true); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, {}, true); } let allReports = await new Promise>((resolve) => { @@ -4020,7 +4022,7 @@ describe('actions/IOU', () => { let navigateToAfterDelete; if (transaction && createIOUAction) { // When we delete the expense and we should delete the IOU report - navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}); + navigateToAfterDelete = deleteMoneyRequest(transaction.transactionID, createIOUAction, {}, {}); } // Then we expect to navigate to the chat report expect(chatReport?.reportID).not.toBeUndefined(); @@ -5735,6 +5737,7 @@ describe('actions/IOU', () => { }, policyTagList: {}, policyCategories: {}, + transactions: {}, transactionViolations: {}, }); @@ -5794,6 +5797,7 @@ describe('actions/IOU', () => { }, policyTagList: {}, policyCategories: {}, + transactions: {}, transactionViolations: {}, }); From 47be6d1ecd4630aa9b23a8588fe0745cbdb13aac Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 25 Jul 2025 03:12:43 +0300 Subject: [PATCH 22/34] lint --- .../useDuplicateTransactionsAndViolations.ts | 41 ++++++++++--------- src/libs/TransactionUtils/index.ts | 2 +- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 5e7ee9cfbba3..0c068396930c 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -1,18 +1,19 @@ import {useMemo} from 'react'; -import {OnyxCollection, useOnyx} from 'react-native-onyx'; +import {OnyxCollection} from 'react-native-onyx'; +import useOnyx from '@hooks/useOnyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction, TransactionViolations} from '@src/types/onyx'; /** * Selects violations related to provided transaction IDs and if present, the violations of their duplicates. - * @param {string[]} transactionIDs - An array of transaction IDs to fetch their violations for. - * @param {OnyxCollection} allViolations - A collection of all transaction violations currently in the onyx. - * @returns {OnyxCollection} - A collection of violations related to the transaction IDs and if present, the violations of their duplicates. + * @param transactionIDs - An array of transaction IDs to fetch their violations for. + * @param allTransactionsViolations - A collection of all transaction violations currently in the onyx. + * @returns - A collection of violations related to the transaction IDs and if present, the violations of their duplicates. * @private */ -function selectViolationsWithDuplicates(transactionIDs: string[], allViolations: OnyxCollection): OnyxCollection { - if (!allViolations) { +function selectViolationsWithDuplicates(transactionIDs: string[], allTransactionsViolations: OnyxCollection): OnyxCollection { + if (!allTransactionsViolations) { return {}; } @@ -20,15 +21,15 @@ function selectViolationsWithDuplicates(transactionIDs: string[], allViolations: for (const transactionID of transactionIDs) { const key = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; - const violations = allViolations[key]; + const transactionViolations = allTransactionsViolations[key]; - if (!violations) { + if (!transactionViolations) { continue; } - result[key] = violations; + result[key] = transactionViolations; - violations + transactionViolations .filter((violations) => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) .flatMap((violations) => violations?.data?.duplicates ?? []) .forEach((duplicateID) => { @@ -37,7 +38,7 @@ function selectViolationsWithDuplicates(transactionIDs: string[], allViolations: } const duplicateKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicateID}`; - const duplicateViolations = allViolations[duplicateKey]; + const duplicateViolations = allTransactionsViolations[duplicateKey]; if (duplicateViolations) { result[duplicateKey] = duplicateViolations; @@ -57,8 +58,8 @@ function selectViolationsWithDuplicates(transactionIDs: string[], allViolations: */ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { const violationsSelectorMemo = useMemo(() => { - return (allViolations: OnyxCollection) => selectViolationsWithDuplicates(transactionIDs, allViolations); - }, [transactionIDs.join(',')]); + return (allTransactionsViolations: OnyxCollection) => selectViolationsWithDuplicates(transactionIDs, allTransactionsViolations); + }, [transactionIDs]); const [duplicateTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, { canBeMissing: true, @@ -76,16 +77,16 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { for (const transactionID of transactionIDs) { const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; const transaction = allTransactions[key]; - if (transaction) result[key] = transaction; - - const violationsKey = `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; - const violations = duplicateTransactionViolations?.[violationsKey]; + if (transaction) { + result[key] = transaction; + } + const transactionViolations = duplicateTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - if (!violations) { + if (!transactionViolations) { continue; } - violations + transactionViolations .filter((violations) => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) .flatMap((violations) => violations?.data?.duplicates ?? []) .forEach((duplicateID) => { @@ -103,7 +104,7 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { } return result; }; - }, [transactionIDs.join(','), duplicateTransactionViolations]); + }, [transactionIDs, duplicateTransactionViolations]); const [duplicateTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, { canBeMissing: true, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index fbbd52008eab..81d7caa89e1b 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1501,7 +1501,7 @@ function updateDuplicatesTransactionsViolations( const duplicateTransactionViolations = duplicateViolations.filter((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); - if (duplicateTransactionViolations.length == 0) { + if (duplicateTransactionViolations.length === 0) { continue; } From 233da3172023ad83a99b08d7c9f180a72aaebac1 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 25 Jul 2025 04:07:12 +0300 Subject: [PATCH 23/34] Resolving conflicts --- .../useDuplicateTransactionsAndViolations.ts | 10 ++++------ .../PopoverReportActionContextMenu.tsx | 15 +++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 0c068396930c..589df09e4796 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -1,5 +1,5 @@ import {useMemo} from 'react'; -import {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import useOnyx from '@hooks/useOnyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,11 +50,9 @@ function selectViolationsWithDuplicates(transactionIDs: string[], allTransaction } /** - * Hook to fetch transactions, their violations and if present, the duplicate transactions and their violations. - * @param {string[]} transactionIDs - Array of transaction IDs to check for duplicates. - * @returns {Object} - An object containing duplicate transactions and their violations. - * @property {OnyxCollection} duplicateTransactions - Collection of duplicate transactions. - * @property {OnyxCollection} duplicateTransactionViolations - Collection of violations related to duplicate transactions. + * A hook to fetch transactions, their violations and if present, the duplicate transactions and their violations. + * @param transactionIDs - Array of transaction IDs to check for duplicates. + * @returns - An object containing duplicate transactions and their violations. */ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { const violationsSelectorMemo = useMemo(() => { diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 2c53d4af253d..84c75a3a0434 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -282,13 +282,16 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef([]); useEffect(() => { - const originalMessage = getOriginalMessage(reportActionRef.current); - if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { - setTransactionIDs([originalMessage.IOUTransactionID]); - return; + const reportAction = reportActionRef.current; + if (isMoneyRequestAction(reportAction)) { + const originalMessage = getOriginalMessage(reportAction); + if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { + setTransactionIDs([originalMessage.IOUTransactionID]); + return; + } } setTransactionIDs([]); - }, [reportActionRef.current]); + }, [reportActionRef]); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionIDs); @@ -310,7 +313,7 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef { callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); From 9480a57c2310c8e7ff0f8f4b74f36af833a0b3e2 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 28 Jul 2025 02:34:06 +0300 Subject: [PATCH 24/34] Update useDuplicateTransactionsAndViolations.ts --- src/hooks/useDuplicateTransactionsAndViolations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 589df09e4796..e30cefb53914 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -1,6 +1,6 @@ import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import useOnyx from '@hooks/useOnyx'; +import useOnyx from './useOnyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction, TransactionViolations} from '@src/types/onyx'; From b6a589486535fb52c350d1c75710add621673c33 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Sun, 27 Jul 2025 23:48:13 +0000 Subject: [PATCH 25/34] lint --- src/hooks/useDuplicateTransactionsAndViolations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index e30cefb53914..4187a20156ee 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -1,9 +1,10 @@ import {useMemo} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; -import useOnyx from './useOnyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction, TransactionViolations} from '@src/types/onyx'; +import useOnyx from './useOnyx'; + /** * Selects violations related to provided transaction IDs and if present, the violations of their duplicates. From 7c1671913a6d812f52277c3e9a11b7d0b17c09cc Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 28 Jul 2025 00:02:18 +0000 Subject: [PATCH 26/34] useDuplicateTransactionAndViolation.ts --- src/hooks/useDuplicateTransactionsAndViolations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 4187a20156ee..0c55cd8ebc78 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -5,7 +5,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Transaction, TransactionViolations} from '@src/types/onyx'; import useOnyx from './useOnyx'; - /** * Selects violations related to provided transaction IDs and if present, the violations of their duplicates. * @param transactionIDs - An array of transaction IDs to fetch their violations for. From 9657362bbe6460e5a2baaac19dfd96adef05a7b5 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 28 Jul 2025 22:15:08 +0000 Subject: [PATCH 27/34] moving useDuplicateTransactionsAndViolations --- .../useDuplicateTransactionsAndViolations.ts | 93 +++++++++++-------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 0c55cd8ebc78..58a6c1f696e9 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -49,12 +49,64 @@ function selectViolationsWithDuplicates(transactionIDs: string[], allTransaction return result; } +/** + * Selects transactions related to provided transaction IDs and if present, the duplicate transactions. + * @param transactionIDs - An array of transaction IDs to fetch their transactions for. + * @param allTransactions - A collection of all transactions currently in the onyx. + * @param duplicateTransactionViolations - A collection of all duplicate transaction violations currently in the onyx. + * @returns - A collection of transactions related to the transaction IDs and if present, the duplicate transactions. + */ + +function selectTransactionsWithDuplicates(transactionIDs: string[], allTransactions: OnyxCollection, duplicateTransactionViolations: OnyxCollection): OnyxCollection { + if (!allTransactions) { + return {}; + } + + const result: OnyxCollection = {}; + + for (const transactionID of transactionIDs) { + const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; + const transaction = allTransactions[key]; + if (transaction) { + result[key] = transaction; + } + + const transactionViolations = duplicateTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + + if (!transactionViolations) { + continue; + } + + transactionViolations + .filter((violations) => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) + .flatMap((violations) => violations?.data?.duplicates ?? []) + .forEach((duplicateID) => { + if (!duplicateID) { + return; + } + + const duplicateKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`; + const duplicateTransaction = allTransactions[duplicateKey]; + + if (duplicateTransaction) { + result[duplicateKey] = duplicateTransaction; + } + }); + } + return result; +}; + +type DuplicateTransactionsAndViolations = { + duplicateTransactions: OnyxCollection; + duplicateTransactionViolations: OnyxCollection; +}; + /** * A hook to fetch transactions, their violations and if present, the duplicate transactions and their violations. * @param transactionIDs - Array of transaction IDs to check for duplicates. * @returns - An object containing duplicate transactions and their violations. */ -function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { +function useDuplicateTransactionsAndViolations(transactionIDs: string[]): DuplicateTransactionsAndViolations { const violationsSelectorMemo = useMemo(() => { return (allTransactionsViolations: OnyxCollection) => selectViolationsWithDuplicates(transactionIDs, allTransactionsViolations); }, [transactionIDs]); @@ -65,43 +117,7 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { }); const transactionSelector = useMemo(() => { - return (allTransactions: OnyxCollection) => { - if (!allTransactions) { - return {}; - } - - const result: OnyxCollection = {}; - - for (const transactionID of transactionIDs) { - const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; - const transaction = allTransactions[key]; - if (transaction) { - result[key] = transaction; - } - const transactionViolations = duplicateTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - - if (!transactionViolations) { - continue; - } - - transactionViolations - .filter((violations) => violations.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION) - .flatMap((violations) => violations?.data?.duplicates ?? []) - .forEach((duplicateID) => { - if (!duplicateID) { - return; - } - - const duplicateKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`; - const duplicateTransaction = allTransactions[duplicateKey]; - - if (duplicateTransaction) { - result[duplicateKey] = duplicateTransaction; - } - }); - } - return result; - }; + return (allTransactions: OnyxCollection) => selectTransactionsWithDuplicates(transactionIDs, allTransactions, duplicateTransactionViolations); }, [transactionIDs, duplicateTransactionViolations]); const [duplicateTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, { @@ -118,4 +134,5 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]) { ); } + export default useDuplicateTransactionsAndViolations; From 9705cf9887f15b91ec75590134238e771c8d042d Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 28 Jul 2025 22:23:38 +0000 Subject: [PATCH 28/34] applied suggestions --- src/hooks/useDuplicateTransactionsAndViolations.ts | 2 +- src/libs/TransactionUtils/index.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 58a6c1f696e9..c14de3be5334 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -13,7 +13,7 @@ import useOnyx from './useOnyx'; * @private */ function selectViolationsWithDuplicates(transactionIDs: string[], allTransactionsViolations: OnyxCollection): OnyxCollection { - if (!allTransactionsViolations) { + if (!allTransactionsViolations || !transactionIDs?.length) { return {}; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index d2b6ef06a745..54841534bfc2 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1425,15 +1425,15 @@ function getValidDuplicateTransactionIDs(transactionID: string, transactionColle foundDuplicateViolation = true; - const duplicatesID = violation?.data?.duplicates; - if (!duplicatesID || duplicatesID.length === 0) { + const duplicatesIDs = violation?.data?.duplicates; + if (!duplicatesIDs || duplicatesIDs.length === 0) { Log.warn(`Violation ${violation.name} lacks duplicates. Transaction ID: ${transactionID}`); break; } const validTransactions: Transaction[] = []; - for (const duplicateID of duplicatesID) { + for (const duplicateID of duplicatesIDs) { // Skip self-reference if (duplicateID === transactionID) { continue; From 6e8a9af13e01872f122cfa0eb6afa7519bb52098 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 28 Jul 2025 23:51:49 +0000 Subject: [PATCH 29/34] applied changes --- src/components/MoneyRequestHeader.tsx | 2 +- .../useDuplicateTransactionsAndViolations.ts | 11 +++++++---- .../PopoverReportActionContextMenu.tsx | 19 +++++++------------ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index ff34cfc0fba6..eb963e9d1737 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -80,7 +80,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre }`, {canBeMissing: true}, ); - const transactionViolations = useTransactionViolations(); + const transactionViolations = useTransactionViolations(transaction?.transactionID); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transaction?.transactionID ? [transaction.transactionID] : []); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index c14de3be5334..979c53d4b8de 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -57,7 +57,11 @@ function selectViolationsWithDuplicates(transactionIDs: string[], allTransaction * @returns - A collection of transactions related to the transaction IDs and if present, the duplicate transactions. */ -function selectTransactionsWithDuplicates(transactionIDs: string[], allTransactions: OnyxCollection, duplicateTransactionViolations: OnyxCollection): OnyxCollection { +function selectTransactionsWithDuplicates( + transactionIDs: string[], + allTransactions: OnyxCollection, + duplicateTransactionViolations: OnyxCollection, +): OnyxCollection { if (!allTransactions) { return {}; } @@ -94,12 +98,12 @@ function selectTransactionsWithDuplicates(transactionIDs: string[], allTransacti }); } return result; -}; +} type DuplicateTransactionsAndViolations = { duplicateTransactions: OnyxCollection; duplicateTransactionViolations: OnyxCollection; -}; +}; /** * A hook to fetch transactions, their violations and if present, the duplicate transactions and their violations. @@ -134,5 +138,4 @@ function useDuplicateTransactionsAndViolations(transactionIDs: string[]): Duplic ); } - export default useDuplicateTransactionsAndViolations; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 84c75a3a0434..47c2a6c53dc3 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -279,19 +279,14 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef([]); - - useEffect(() => { - const reportAction = reportActionRef.current; - if (isMoneyRequestAction(reportAction)) { - const originalMessage = getOriginalMessage(reportAction); - if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { - setTransactionIDs([originalMessage.IOUTransactionID]); - return; - } + let transactionIDs: string[] = []; + if (isMoneyRequestAction(reportActionRef.current)) { + const originalMessage = getOriginalMessage(reportActionRef.current); + if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { + transactionIDs.push(originalMessage.IOUTransactionID); + return; } - setTransactionIDs([]); - }, [reportActionRef]); + } const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionIDs); From cb54fde75ddfc7e1ea225cf54b6c627af5bc7901 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Tue, 29 Jul 2025 00:18:02 +0000 Subject: [PATCH 30/34] lint --- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 47c2a6c53dc3..c51234aae076 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -279,12 +279,12 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef Date: Wed, 30 Jul 2025 00:07:13 +0000 Subject: [PATCH 31/34] applied suggestions --- .../useDuplicateTransactionsAndViolations.ts | 2 +- src/libs/TransactionUtils/index.ts | 23 +++++-------------- src/libs/actions/IOU.ts | 8 +++---- .../PopoverReportActionContextMenu.tsx | 1 - 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/hooks/useDuplicateTransactionsAndViolations.ts b/src/hooks/useDuplicateTransactionsAndViolations.ts index 979c53d4b8de..8fa3ca82775c 100644 --- a/src/hooks/useDuplicateTransactionsAndViolations.ts +++ b/src/hooks/useDuplicateTransactionsAndViolations.ts @@ -8,7 +8,7 @@ import useOnyx from './useOnyx'; /** * Selects violations related to provided transaction IDs and if present, the violations of their duplicates. * @param transactionIDs - An array of transaction IDs to fetch their violations for. - * @param allTransactionsViolations - A collection of all transaction violations currently in the onyx. + * @param allTransactionsViolations - A collection of all transaction violations currently in the onyx db. * @returns - A collection of violations related to the transaction IDs and if present, the violations of their duplicates. * @private */ diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 54841534bfc2..f9c63f67dca7 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1419,36 +1419,25 @@ function getValidDuplicateTransactionIDs(transactionID: string, transactionColle // Skip further violations if (foundDuplicateViolation) { - Log.warn(`Multiple duplicate violations found for transaction ${transactionID}. Only one expected.`); + Log.warn(`Multiple duplicate violations found for transaction. Only one expected.`, {transactionID}); continue; } foundDuplicateViolation = true; - - const duplicatesIDs = violation?.data?.duplicates; - if (!duplicatesIDs || duplicatesIDs.length === 0) { - Log.warn(`Violation ${violation.name} lacks duplicates. Transaction ID: ${transactionID}`); - break; - } + const duplicatesIDs = violation.data?.duplicates ?? []; const validTransactions: Transaction[] = []; for (const duplicateID of duplicatesIDs) { // Skip self-reference - if (duplicateID === transactionID) { - continue; - } - - if (seen.has(duplicateID)) { - Log.warn(`Duplicate ID ${duplicateID} is repeated in violation for transaction ${transactionID}.`); + if (duplicateID === transactionID || seen.has(duplicateID)) { continue; } - seen.add(duplicateID); const transaction = transactionCollection?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${duplicateID}`]; if (!transaction?.transactionID) { - Log.warn(`Transaction ${duplicateID} does not exist or is invalid. Found in transaction ${transactionID}.`); + Log.warn(`Transaction does not exist or is invalid. Found in transaction.`, {duplicateID, transactionID}); continue; } @@ -1475,7 +1464,7 @@ function getValidDuplicateTransactionIDs(transactionID: string, transactionColle * @param transactionViolations - The collection of the transaction violations including the duplicates violations. * */ -function updateDuplicatesTransactionsViolations( +function removeTransactionFromDuplicateTransactionViolation( onyxData: OnyxData, transactionID: string, transactions: OnyxCollection, @@ -1947,7 +1936,7 @@ export { getReimbursable, isPayAtEndExpense, removeSettledAndApprovedTransactions, - updateDuplicatesTransactionsViolations, + removeTransactionFromDuplicateTransactionViolation, getCardName, hasReceiptSource, shouldShowAttendees, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a2cd52faa166..f3ebbf769f4d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -203,7 +203,7 @@ import { isPerDiemRequest as isPerDiemRequestTransactionUtils, isScanning, isScanRequest as isScanRequestTransactionUtils, - updateDuplicatesTransactionsViolations, + removeTransactionFromDuplicateTransactionViolation, } from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUActionParams, IOUType} from '@src/CONST'; @@ -4586,7 +4586,7 @@ function updateMoneyRequestDate( data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); - updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactions, transactionViolations); + removeTransactionFromDuplicateTransactionViolation(data.onyxData, transactionID, transactions, transactionViolations); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); @@ -7560,7 +7560,7 @@ function updateMoneyRequestAmountAndCurrency({ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, policy); } else { data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList ?? null, policyCategories ?? null); - updateDuplicatesTransactionsViolations(data.onyxData, transactionID, transactions, transactionViolations); + removeTransactionFromDuplicateTransactionViolation(data.onyxData, transactionID, transactions, transactionViolations); } const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY, params, onyxData); @@ -7990,7 +7990,7 @@ function deleteMoneyRequest( }, ]; - updateDuplicatesTransactionsViolations({optimisticData, failureData}, transactionID, transactions, violations); + removeTransactionFromDuplicateTransactionViolation({optimisticData, failureData}, transactionID, transactions, violations); if (shouldDeleteTransactionThread) { optimisticData.push( diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index c51234aae076..2f42b855cb73 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -279,7 +279,6 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef Date: Wed, 30 Jul 2025 00:20:37 +0000 Subject: [PATCH 32/34] typecheck --- src/components/MoneyReportHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 573d03083328..5b180bbebca5 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -184,7 +184,6 @@ function MoneyReportHeader({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`, { canBeMissing: true, }); - const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID)); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const [invoiceReceiverPolicy] = useOnyx( @@ -192,6 +191,7 @@ function MoneyReportHeader({ {canBeMissing: true}, ); + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactions.map((t) => t.transactionID)); const isExported = useMemo(() => isExportedUtils(reportActions), [reportActions]); // wrapped in useMemo to improve performance because this is an operation on array const integrationNameFromExportMessage = useMemo(() => { From 2e4ae6ea25118cc930d86c0744b3a810fd1c0358 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Thu, 31 Jul 2025 19:16:27 +0000 Subject: [PATCH 33/34] refactoring getValidDuplicateTransactionIDs --- src/libs/TransactionUtils/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index f9c63f67dca7..1397c71c084b 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1420,7 +1420,7 @@ function getValidDuplicateTransactionIDs(transactionID: string, transactionColle // Skip further violations if (foundDuplicateViolation) { Log.warn(`Multiple duplicate violations found for transaction. Only one expected.`, {transactionID}); - continue; + break; } foundDuplicateViolation = true; @@ -1495,7 +1495,7 @@ function removeTransactionFromDuplicateTransactionViolation( } if (duplicateTransactionViolations.length > 1) { - Log.warn(`There are ${duplicateTransactionViolations.length} duplicate transaction violations for transactionID: ${duplicateID}. This should not happen.`); + Log.warn(`There are duplicate transaction violations for transactionID. This should not happen.`, {duplicateTransactionViolations, duplicateID}); continue; } From daa8de56f7fdb93cfd45cccba5032802d1a956ea Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 1 Aug 2025 03:46:49 +0300 Subject: [PATCH 34/34] reverting line --- src/components/MoneyReportHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 78d052ba069a..3b794ca46aa7 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -185,7 +185,7 @@ function MoneyReportHeader({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`, { canBeMissing: true, }); - const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true, canBeMissing: true}); + const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {canBeMissing: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const [invoiceReceiverPolicy] = useOnyx( `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined}`,