Skip to content
10 changes: 9 additions & 1 deletion src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import {
allHavePendingRTERViolation,
checkIfShouldShowMarkAsCashButton,
hasDuplicateTransactions,
isDuplicate as isDuplicateTransactionUtils,
isExpensifyCardTransaction,
isOnHold as isOnHoldTransactionUtils,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 5 additions & 1 deletion src/components/MoneyRequestHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
3 changes: 2 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Comment thread
nkdengineer marked this conversation as resolved.
defaultRate: 'Tasa predeterminada',
receiptMissingDetails: 'Recibo con campos vacíos',
missingAmount: 'Falta importe',
Expand Down Expand Up @@ -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',
Expand Down
12 changes: 10 additions & 2 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1030,7 +1030,7 @@ function isOnHold(transaction: OnyxEntry<Transaction>): boolean {
return false;
}

return !!transaction.comment?.hold || isDuplicate(transaction.transactionID, true);
return !!transaction.comment?.hold;
}

/**
Expand Down Expand Up @@ -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'
*/
Expand Down Expand Up @@ -1572,6 +1579,7 @@ export {
getRecentTransactions,
hasReservationList,
hasViolation,
hasDuplicateTransactions,
hasBrokenConnectionViolation,
shouldShowBrokenConnectionViolation,
shouldShowBrokenConnectionViolationForMultipleTransactions,
Expand Down
27 changes: 27 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,12 @@ import {
getMerchant,
getTransaction,
getUpdatedTransaction,
hasDuplicateTransactions,
hasReceipt as hasReceiptTransactionUtils,
isAmountMissing,
isCustomUnitRateIDForP2P,
isDistanceRequest as isDistanceRequestTransactionUtils,
isDuplicate,
isExpensifyCardTransaction,
isFetchingWaypointsFromServer,
isOnHold,
Expand Down Expand Up @@ -8662,6 +8664,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, 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;
}
Expand Down Expand Up @@ -8804,6 +8807,30 @@ function approveMoneyRequest(expenseReport: OnyxEntry<OnyxTypes.Report>, 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,
Expand Down