diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index f47cf38cb463..cd1141e59701 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, @@ -212,8 +213,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 hasDuplicates = hasDuplicateTransactions(moneyRequestReport?.reportID); const shouldShowStatusBar = - hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; + 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 @@ -325,6 +328,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (hasOnlyHeldExpenses) { return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')}; } + + if (hasDuplicates) { + return {icon: getStatusIcon(Expensicons.Exclamation), description: translate('iou.duplicateTransaction')}; + } + if (!!transaction?.transactionID && shouldShowBrokenConnectionViolation) { return { icon: getStatusIcon(Expensicons.Hourglass), diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 6d7ca116a782..af909c3f9c6a 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.Exclamation), description: translate('iou.expenseDuplicate')}; } if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { diff --git a/src/languages/en.ts b/src/languages/en.ts index 6baefcb7ed9d..7ebf456b1010 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -926,6 +926,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', @@ -1082,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 5e62dfa6156b..0725cd184916 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -926,6 +926,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', @@ -1078,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', diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index a3b8a3a8148e..c3a156b4fb80 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -44,7 +44,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'; @@ -1030,7 +1030,7 @@ function isOnHold(transaction: OnyxEntry): boolean { return false; } - return !!transaction.comment?.hold || isDuplicate(transaction.transactionID, true); + return !!transaction.comment?.hold; } /** @@ -1084,6 +1084,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' */ @@ -1572,6 +1579,7 @@ export { getRecentTransactions, hasReservationList, hasViolation, + hasDuplicateTransactions, hasBrokenConnectionViolation, shouldShowBrokenConnectionViolation, shouldShowBrokenConnectionViolationForMultipleTransactions, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5d12f417014c..3579908c0a09 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, @@ -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,