From a789e032a432ff32fded996691c117bbceda5d99 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 13 Mar 2025 23:43:51 +0700 Subject: [PATCH 01/10] fix: Update duplicate detection to be treated as any other violation --- src/components/MoneyReportHeader.tsx | 15 +++++++++++---- src/libs/TransactionUtils/index.ts | 12 ++++++++++-- src/libs/actions/IOU.ts | 6 +++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 06d679d98207..186841c82c3a 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -37,6 +37,7 @@ import { import { allHavePendingRTERViolation, checkIfShouldShowMarkAsCashButton, + hasDuplicateTransactions, isDuplicate as isDuplicateTransactionUtils, isExpensifyCardTransaction, isOnHold as isOnHoldTransactionUtils, @@ -207,8 +208,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; + + const hasDuplicate = hasDuplicateTransactions(moneyRequestReport?.reportID); const shouldShowStatusBar = - hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; + hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions || hasDuplicate; // When prevent self-approval is enabled & the current user is submitter AND they're submitting to theirself, we need to show the optimistic next step // We should always show this optimistic message for policies with preventSelfApproval @@ -247,7 +250,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY); if (isDelegateAccessRestricted) { setIsNoDelegateAccessMenuVisible(true); - } else if (isAnyTransactionOnHold) { + } else if (isAnyTransactionOnHold || hasDuplicate) { setIsHoldMenuVisible(true); } else if (isInvoiceReport(moneyRequestReport)) { startAnimation(); @@ -257,14 +260,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea payMoneyRequest(type, chatReport, moneyRequestReport, true); } }, - [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation], + [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation, hasDuplicate], ); const confirmApproval = () => { setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE); if (isDelegateAccessRestricted) { setIsNoDelegateAccessMenuVisible(true); - } else if (isAnyTransactionOnHold) { + } else if (isAnyTransactionOnHold || hasDuplicate) { setIsHoldMenuVisible(true); } else { startApprovedAnimation(); @@ -340,6 +343,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (hasScanningReceipt) { return {icon: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')}; } + + if (hasDuplicate) { + return {icon: getStatusIcon(Expensicons.Hourglass), description: 'Potential duplicate expenses identified. Review duplicates to enable submission.'}; + } }; const statusBarProps = getStatusBarProps(); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 771309fc4195..2eba3fc9a436 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -43,7 +43,7 @@ import type {IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; -import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults'; +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 type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -983,7 +983,7 @@ function isOnHold(transaction: OnyxEntry): boolean { return false; } - return !!transaction.comment?.hold || isDuplicate(transaction.transactionID, true); + return !!transaction.comment?.hold; } /** @@ -1037,6 +1037,13 @@ function hasViolation(transaction: Transaction | undefined, transactionViolation ); } +function hasDuplicateTransactions(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean { + const transactionsByIouReportID = getReportTransactions(iouReportID); + const reportTransactions = allReportTransactions ?? transactionsByIouReportID; + + return reportTransactions.length > 0 && reportTransactions.some((transaction) => isDuplicate(transaction?.transactionID, true)); +} + /** * Checks if any violations for the provided transaction are of type 'notice' */ @@ -1524,6 +1531,7 @@ export { getRecentTransactions, hasReservationList, hasViolation, + hasDuplicateTransactions, hasBrokenConnectionViolation, shouldShowBrokenConnectionViolation, shouldShowBrokenConnectionViolationForMultipleTransactions, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bedaebe90564..5faabe7b3523 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -157,6 +157,7 @@ import { isAmountMissing, isCustomUnitRateIDForP2P, isDistanceRequest as isDistanceRequestTransactionUtils, + isDuplicate, isExpensifyCardTransaction, isFetchingWaypointsFromServer, isOnHold, @@ -7621,8 +7622,11 @@ function getHoldReportActionsAndTransactions(reportID: string | undefined) { Object.values(iouReportActions).forEach((action) => { const transactionID = isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined; const transaction = getTransaction(transactionID); + if (!transaction) { + return; + } - if (transaction?.comment?.hold) { + if (!!transaction?.comment?.hold || isDuplicate(transactionID, true)) { holdReportActions.push(action as OnyxTypes.ReportAction); holdTransactions.push(transaction); } From f5c74e8f0db3283051d5d21429a18fe8d26d88d5 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Mar 2025 15:02:29 +0700 Subject: [PATCH 02/10] update duplicate logic --- src/components/MoneyReportHeader.tsx | 13 +++++++------ src/components/MoneyRequestHeader.tsx | 6 +++++- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/IOU.ts | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index eff4220b754e..1a5282862958 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -252,7 +252,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setRequestType(CONST.IOU.REPORT_ACTION_TYPE.PAY); if (isDelegateAccessRestricted) { setIsNoDelegateAccessMenuVisible(true); - } else if (isAnyTransactionOnHold || hasDuplicate) { + } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); } else if (isInvoiceReport(moneyRequestReport)) { startAnimation(); @@ -262,7 +262,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea payMoneyRequest(type, chatReport, moneyRequestReport, true); } }, - [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation, hasDuplicate], + [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation], ); const confirmApproval = () => { @@ -324,6 +324,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (hasOnlyHeldExpenses) { return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')}; } + + if (hasDuplicate) { + return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.duplicateTransaction')}; + } + if (!!transaction?.transactionID && shouldShowBrokenConnectionViolation) { return { icon: getStatusIcon(Expensicons.Hourglass), @@ -345,10 +350,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (hasScanningReceipt) { return {icon: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')}; } - - if (hasDuplicate) { - return {icon: getStatusIcon(Expensicons.Hourglass), description: 'Potential duplicate expenses identified. Review duplicates to enable submission.'}; - } }; const statusBarProps = getStatusBarProps(); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 6d7ca116a782..aebdd47046fc 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -102,7 +102,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const getStatusBarProps: () => MoneyRequestHeaderStatusBarProps | undefined = () => { if (isOnHold) { - return {icon: getStatusIcon(Expensicons.Stopwatch), description: isDuplicate ? translate('iou.expenseDuplicate') : translate('iou.expenseOnHold')}; + return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expenseOnHold')}; + } + + if (isDuplicate) { + return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expenseDuplicate')}; } if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { diff --git a/src/languages/en.ts b/src/languages/en.ts index 9386978f77c4..590de4eedb6f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -910,6 +910,7 @@ const translations = { }), receiptScanInProgress: 'Receipt scan in progress', receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.', + duplicateTransaction: 'Potential duplicate expenses identified. Review duplicates to enable submission.', receiptIssuesFound: () => ({ one: 'Issue found', other: 'Issues found', diff --git a/src/languages/es.ts b/src/languages/es.ts index e568c3012d34..380bcaaeecda 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -909,6 +909,7 @@ const translations = { }), receiptScanInProgress: 'Escaneado de recibo en proceso', receiptScanInProgressDescription: 'Escaneado de recibo en proceso. Vuelve a comprobarlo más tarde o introduce los detalles ahora.', + duplicateTransaction: 'Se han identificado posibles gastos duplicados. Revisa los duplicados para habilitar el envío.', defaultRate: 'Tasa predeterminada', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 21673416490a..bc8628a3b424 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7686,7 +7686,7 @@ function getHoldReportActionsAndTransactions(reportID: string | undefined) { return; } - if (!!transaction?.comment?.hold || isDuplicate(transactionID, true)) { + if (transaction?.comment?.hold) { holdReportActions.push(action as OnyxTypes.ReportAction); holdTransactions.push(transaction); } From 60c2177d7d204bafc3e476495c5e93a3a5c19cff Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Mar 2025 15:03:47 +0700 Subject: [PATCH 03/10] remove unused change --- src/libs/actions/IOU.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bc8628a3b424..9377dbe7166a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7682,9 +7682,6 @@ function getHoldReportActionsAndTransactions(reportID: string | undefined) { Object.values(iouReportActions).forEach((action) => { const transactionID = isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined; const transaction = getTransaction(transactionID); - if (!transaction) { - return; - } if (transaction?.comment?.hold) { holdReportActions.push(action as OnyxTypes.ReportAction); From 10803a5b2d1893a111a7d86ff66215da7330c41c Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Mar 2025 15:04:35 +0700 Subject: [PATCH 04/10] fix lint --- src/libs/actions/IOU.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9377dbe7166a..8418d2150c9f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -159,7 +159,6 @@ import { isAmountMissing, isCustomUnitRateIDForP2P, isDistanceRequest as isDistanceRequestTransactionUtils, - isDuplicate, isExpensifyCardTransaction, isFetchingWaypointsFromServer, isOnHold, From 7969b5acdf31b5dd9c01220d316ba1ff78aa4ea3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Mar 2025 16:03:42 +0700 Subject: [PATCH 05/10] re-run test --- tests/unit/SubscriptionUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 4596f8436dcc..0123a12f4670 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -553,7 +553,7 @@ describe('SubscriptionUtils', () => { }); }); - it('should return the discount info if the user is on a free trial and trial was started more than 24 hours before', async () => { + it('should return the discount info if the user is on a free trial and trial was started more than 24 hours before', async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 10), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), From a6fdda57f86bb06ee3d6781b04c3da8ea3191c72 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Mar 2025 16:03:57 +0700 Subject: [PATCH 06/10] run lint --- tests/unit/SubscriptionUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 0123a12f4670..4596f8436dcc 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -553,7 +553,7 @@ describe('SubscriptionUtils', () => { }); }); - it('should return the discount info if the user is on a free trial and trial was started more than 24 hours before', async () => { + it('should return the discount info if the user is on a free trial and trial was started more than 24 hours before', async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 10), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), From 0cdfc3a875ecced87ae57db881b909c47ed1cd34 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 19 Mar 2025 23:19:52 +0700 Subject: [PATCH 07/10] update variable --- src/components/MoneyReportHeader.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 1a5282862958..d47b211fdd41 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -210,9 +210,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const hasDuplicate = hasDuplicateTransactions(moneyRequestReport?.reportID); + const hasDuplicates = hasDuplicateTransactions(moneyRequestReport?.reportID); const shouldShowStatusBar = - hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions || hasDuplicate; + hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions || hasDuplicates; // When prevent self-approval is enabled & the current user is submitter AND they're submitting to theirself, we need to show the optimistic next step // We should always show this optimistic message for policies with preventSelfApproval @@ -269,7 +269,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setRequestType(CONST.IOU.REPORT_ACTION_TYPE.APPROVE); if (isDelegateAccessRestricted) { setIsNoDelegateAccessMenuVisible(true); - } else if (isAnyTransactionOnHold || hasDuplicate) { + } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); } else { startApprovedAnimation(); @@ -325,7 +325,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')}; } - if (hasDuplicate) { + if (hasDuplicates) { return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.duplicateTransaction')}; } From d442c6341d870fda155f828d333425ece8116037 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 26 Mar 2025 12:03:34 +0700 Subject: [PATCH 08/10] update duplicate text and icon --- src/components/MoneyReportHeader.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 53754ebc226f..cd1141e59701 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -330,7 +330,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea } if (hasDuplicates) { - return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.duplicateTransaction')}; + return {icon: getStatusIcon(Expensicons.Exclamation), description: translate('iou.duplicateTransaction')}; } if (!!transaction?.transactionID && shouldShowBrokenConnectionViolation) { diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index aebdd47046fc..af909c3f9c6a 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -106,7 +106,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre } if (isDuplicate) { - return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expenseDuplicate')}; + return {icon: getStatusIcon(Expensicons.Exclamation), description: translate('iou.expenseDuplicate')}; } if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { diff --git a/src/languages/en.ts b/src/languages/en.ts index ad9eee918b5d..7ebf456b1010 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1083,7 +1083,7 @@ const translations = { expenseWasPutOnHold: 'Expense was put on hold', expenseOnHold: 'This expense was put on hold. Please review the comments for next steps.', expensesOnHold: 'All expenses were put on hold. Please review the comments for next steps.', - expenseDuplicate: 'This expense has the same details as another one. Please review the duplicates to remove the hold.', + expenseDuplicate: 'This expense has similar details to another one. Please review the duplicates to continue.', someDuplicatesArePaid: 'Some of these duplicates have been approved or paid already.', reviewDuplicates: 'Review duplicates', keepAll: 'Keep all', diff --git a/src/languages/es.ts b/src/languages/es.ts index a467b3171bb6..0725cd184916 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1079,7 +1079,7 @@ const translations = { expenseWasPutOnHold: 'Este gasto está retenido', expenseOnHold: 'Este gasto está retenido. Revisa los comentarios para saber como proceder.', expensesOnHold: 'Todos los gastos están retenidos. Revisa los comentarios para saber como proceder.', - expenseDuplicate: 'Esta solicitud tiene los mismos detalles que otra. Revisa los duplicados para eliminar la retención.', + expenseDuplicate: 'Este gasto tiene detalles similares a otro. Por favor, revisa los duplicados para continuar.', someDuplicatesArePaid: 'Algunos de estos duplicados ya han sido aprobados o pagados.', reviewDuplicates: 'Revisar duplicados', keepAll: 'Mantener todos', From ff28ee3a0104314b12a3fc537a251b557eb0c4ce Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 27 Mar 2025 15:54:16 +0700 Subject: [PATCH 09/10] remove duplicate violation when approving the money request --- src/libs/actions/IOU.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5d12f417014c..18f36db20d26 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -157,10 +157,12 @@ import { getMerchant, getTransaction, getUpdatedTransaction, + hasDuplicateTransactions, hasReceipt as hasReceiptTransactionUtils, isAmountMissing, isCustomUnitRateIDForP2P, isDistanceRequest as isDistanceRequestTransactionUtils, + isDuplicate, isExpensifyCardTransaction, isFetchingWaypointsFromServer, isOnHold, @@ -197,7 +199,7 @@ import {buildOptimisticRecentlyUsedCurrencies, buildPolicyData, generatePolicyID import {buildOptimisticPolicyRecentlyUsedTags} from './Policy/Tag'; import {completeOnboarding, getCurrentUserAccountID, notifyNewAction} from './Report'; import {clearAllRelatedReportActionErrors} from './ReportActions'; -import {getRecentWaypoints, sanitizeRecentWaypoints} from './Transaction'; +import {getAllTransactions, getRecentWaypoints, sanitizeRecentWaypoints} from './Transaction'; import {removeDraftTransaction} from './TransactionEdit'; type IOURequestType = ValueOf; @@ -8662,6 +8664,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; let total = expenseReport.total ?? 0; const hasHeldExpenses = hasHeldExpensesReportUtils(expenseReport.reportID); + const hasDuplicates = hasDuplicateTransactions(expenseReport.reportID); if (hasHeldExpenses && !full && !!expenseReport.unheldTotal) { total = expenseReport.unheldTotal; } @@ -8804,6 +8807,30 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: optimisticHoldReportExpenseActionIDs = JSON.stringify(holdReportOnyxData.optimisticHoldReportExpenseActionIDs); } + // Remove duplicates violations if we approve the report + if (hasDuplicates) { + const transactions = getReportTransactions(expenseReport.reportID).filter((transaction) => isDuplicate(transaction.transactionID, true)); + if (!full) { + transactions.filter((transaction) => !isOnHold(transaction)); + } + + transactions.forEach((transaction) => { + const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] ?? []; + const newTransactionViolations = transactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: newTransactionViolations, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: transactionViolations, + }); + }); + } + const parameters: ApproveMoneyRequestParams = { reportID: expenseReport.reportID, approvedReportActionID: optimisticApprovedReportAction.reportActionID, From 8ce9cc3038596b1778aa503561744149128f7ced Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 27 Mar 2025 15:56:25 +0700 Subject: [PATCH 10/10] fix lint --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 18f36db20d26..3579908c0a09 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -199,7 +199,7 @@ import {buildOptimisticRecentlyUsedCurrencies, buildPolicyData, generatePolicyID import {buildOptimisticPolicyRecentlyUsedTags} from './Policy/Tag'; import {completeOnboarding, getCurrentUserAccountID, notifyNewAction} from './Report'; import {clearAllRelatedReportActionErrors} from './ReportActions'; -import {getAllTransactions, getRecentWaypoints, sanitizeRecentWaypoints} from './Transaction'; +import {getRecentWaypoints, sanitizeRecentWaypoints} from './Transaction'; import {removeDraftTransaction} from './TransactionEdit'; type IOURequestType = ValueOf;