diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 6a8d122e581e..bd5cc1a930db 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -324,9 +324,12 @@ function MoneyReportHeader({ // Check if there is pending rter violation in all transactionViolations with given transactionIDs. // wrapped in useMemo to avoid unnecessary re-renders and for better performance (array operation inside of function) - const hasAllPendingRTERViolations = useMemo(() => allHavePendingRTERViolation(transactions, violations), [transactions, violations]); + const hasAllPendingRTERViolations = useMemo( + () => allHavePendingRTERViolation(transactions, violations, email ?? '', moneyRequestReport, policy), + [transactions, violations, email, moneyRequestReport, policy], + ); // Check if user should see broken connection violation warning. - const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, moneyRequestReport, policy, violations); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactions, moneyRequestReport, policy, violations, email ?? ''); const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID); const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction); const isArchivedReport = useReportIsArchived(moneyRequestReport?.reportID); @@ -415,7 +418,7 @@ function MoneyReportHeader({ const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const hasDuplicates = hasDuplicateTransactions(moneyRequestReport?.reportID); + const hasDuplicates = hasDuplicateTransactions(email ?? '', moneyRequestReport, policy); const shouldShowMarkAsResolved = isMarkAsResolvedAction(moneyRequestReport, transactionViolations); const shouldShowStatusBar = hasAllPendingRTERViolations || @@ -575,7 +578,9 @@ function MoneyReportHeader({ return {icon: getStatusIcon(expensifyIcons.Flag), description: translate('iou.duplicateTransaction', {isSubmitted: isProcessingReport(moneyRequestReport)})}; } - if (!!transaction?.transactionID && shouldShowBrokenConnectionViolation) { + // Show the broken connection violation message only if it's part of transactionViolations (i.e., visible to the user). + // This prevents displaying an empty message. + if (!!transaction?.transactionID && !!transactionViolations.length && shouldShowBrokenConnectionViolation) { return { icon: getStatusIcon(expensifyIcons.Hourglass), description: ( @@ -599,7 +604,7 @@ function MoneyReportHeader({ }; const getFirstDuplicateThreadID = (transactionsList: OnyxTypes.Transaction[], allReportActions: OnyxTypes.ReportAction[]) => { - const duplicateTransaction = transactionsList.find((reportTransaction) => isDuplicate(reportTransaction)); + const duplicateTransaction = transactionsList.find((reportTransaction) => isDuplicate(reportTransaction, email ?? '', moneyRequestReport, policy)); if (!duplicateTransaction) { return null; } @@ -890,7 +895,7 @@ function MoneyReportHeader({ onPress={() => { let threadID = transactionThreadReportID ?? getFirstDuplicateThreadID(transactions, reportActions); if (!threadID) { - const duplicateTransaction = transactions.find((reportTransaction) => isDuplicate(reportTransaction)); + const duplicateTransaction = transactions.find((reportTransaction) => isDuplicate(reportTransaction, email ?? '', moneyRequestReport, policy)); const transactionID = duplicateTransaction?.transactionID; const iouAction = getIOUActionForReportID(moneyRequestReport?.reportID, transactionID); const createdTransactionThreadReport = createTransactionThreadReport(moneyRequestReport, iouAction); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 73542310819b..661c4415e5df 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -113,9 +113,9 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + const {login: currentUserLogin, email} = useCurrentUserPersonalDetails(); const isOnHold = isOnHoldTransactionUtils(transaction); - const isDuplicate = isDuplicateTransactionUtils(transaction); + const isDuplicate = isDuplicateTransactionUtils(transaction, email ?? '', report, policy); const reportID = report?.reportID; const {removeTransaction, currentSearchHash} = useSearchContext(); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); @@ -183,7 +183,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { return {icon: getStatusIcon(expensifyIcons.CreditCardHourglass), description: translate('iou.transactionPendingDescription')}; } - if (shouldShowBrokenConnectionViolation) { + if (!!transaction?.transactionID && !!transactionViolations.length && shouldShowBrokenConnectionViolation) { return { icon: getStatusIcon(expensifyIcons.Hourglass), description: ( diff --git a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx index d78ab2c80762..348c9675d469 100644 --- a/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionGroupListExpanded.tsx @@ -190,7 +190,7 @@ function TransactionGroupListExpanded({ ({ const transactionViolations = useMemo(() => { return (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( (violation: TransactionViolation) => - !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '') && + !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', snapshotReport, snapshotPolicy) && shouldShowViolation(snapshotReport, snapshotPolicy, violation.name, currentUserDetails.email ?? '', false), ); }, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email]); diff --git a/src/hooks/useTransactionViolations.ts b/src/hooks/useTransactionViolations.ts index 724ca3945dcc..f5854f182ec1 100644 --- a/src/hooks/useTransactionViolations.ts +++ b/src/hooks/useTransactionViolations.ts @@ -27,7 +27,7 @@ function useTransactionViolations(transactionID?: string, shouldShowRterForSettl mergeProhibitedViolations( transactionViolations.filter( (violation: TransactionViolation) => - !isViolationDismissed(transaction, violation, currentUserDetails.email ?? '') && + !isViolationDismissed(transaction, violation, currentUserDetails.email ?? '', iouReport, policy) && shouldShowViolation(iouReport, policy, violation.name, currentUserDetails.email ?? '', shouldShowRterForSettledReport), ), ), diff --git a/src/hooks/useTransactionsAndViolationsForReport.ts b/src/hooks/useTransactionsAndViolationsForReport.ts index a2e8ff0f91d9..c25ca002d4c3 100644 --- a/src/hooks/useTransactionsAndViolationsForReport.ts +++ b/src/hooks/useTransactionsAndViolationsForReport.ts @@ -5,12 +5,15 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {TransactionViolations} from '@src/types/onyx'; import type {ReportTransactionsAndViolations} from '@src/types/onyx/DerivedValues'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useOnyx from './useOnyx'; const DEFAULT_RETURN_VALUE: ReportTransactionsAndViolations = {transactions: {}, violations: {}}; function useTransactionsAndViolationsForReport(reportID?: string) { const allReportsTransactionsAndViolations = useAllReportsTransactionsAndViolations(); const currentUserDetails = useCurrentUserPersonalDetails(); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true}); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); const {transactions, violations} = reportID ? (allReportsTransactionsAndViolations?.[reportID] ?? DEFAULT_RETURN_VALUE) : DEFAULT_RETURN_VALUE; @@ -22,14 +25,14 @@ function useTransactionsAndViolationsForReport(reportID?: string) { // This is our accumulator, it's okay to reassign // eslint-disable-next-line no-param-reassign - filteredTransactionViolations[transactionViolationKey] = getTransactionViolations(transaction, violations, currentUserDetails.email ?? '') ?? []; + filteredTransactionViolations[transactionViolationKey] = getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', report, policy) ?? []; return filteredTransactionViolations; }, {} as Record, ); return {transactions, violations: filteredViolations}; - }, [transactions, violations, currentUserDetails?.email]); + }, [transactions, violations, currentUserDetails?.email, report, policy]); return transactionsAndViolations; } diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index 93052e34ad9a..2b37a1936a0c 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -45,6 +45,7 @@ function canSubmit(report: Report, violations: OnyxCollection isScanning(transaction)); const submitToAccountID = getSubmitToAccountID(policy, report); @@ -56,22 +57,32 @@ function canSubmit(report: Report, violations: OnyxCollection 0; } -function canApprove(report: Report, violations: OnyxCollection, policy?: Policy, transactions?: Transaction[], shouldConsiderViolations = true) { +function canApprove( + report: Report, + violations: OnyxCollection, + currentUserEmail: string, + policy: Policy | undefined, + transactions: Transaction[], + shouldConsiderViolations = true, +) { const currentUserID = getCurrentUserAccountID(); const isExpense = isExpenseReport(report); const isProcessing = isProcessingReport(report); - const isApprovalEnabled = policy ? policy.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL : false; - const managerID = report?.managerID ?? CONST.DEFAULT_NUMBER_ID; + const isApprovalEnabled = policy?.approvalMode && policy.approvalMode !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; + const managerID = report.managerID ?? CONST.DEFAULT_NUMBER_ID; const isCurrentUserManager = managerID === currentUserID; - const hasAnyViolations = hasMissingSmartscanFields(report.reportID, transactions) || hasAnyViolationsUtil(report.reportID, violations); - const reportTransactions = transactions ?? getReportTransactions(report?.reportID); - const isAnyReceiptBeingScanned = transactions?.some((transaction) => isScanning(transaction)); + + // We should consider only visible violations for the approver, invisible violations should not block approval + const reportTransactions = transactions.length ? transactions : getReportTransactions(report?.reportID); + const hasAnyVisibleViolations = + hasMissingSmartscanFields(report.reportID, reportTransactions) || ViolationsUtils.hasVisibleViolationsForUser(report, violations, currentUserEmail, policy, reportTransactions); + const isAnyReceiptBeingScanned = reportTransactions?.some((transaction) => isScanning(transaction)); if (isAnyReceiptBeingScanned) { return false; } - if (!!transactions && transactions?.length > 0 && transactions.every((transaction) => isPending(transaction))) { + if (reportTransactions.length > 0 && reportTransactions.every((transaction) => isPending(transaction))) { return false; } @@ -82,7 +93,7 @@ function canApprove(report: Report, violations: OnyxCollection 0 && isCurrentUserManager; + return isExpense && isProcessing && !!isApprovalEnabled && (!hasAnyVisibleViolations || !shouldConsiderViolations) && reportTransactions.length > 0 && isCurrentUserManager; } function canPay( @@ -170,7 +181,14 @@ function canExport(report: Report, violations: OnyxCollection, isReportArchived: boolean, currentUserEmail: string, policy?: Policy, transactions?: Transaction[]) { +function canReview( + report: Report, + violations: OnyxCollection, + isReportArchived: boolean, + currentUserEmail: string, + policy: Policy | undefined, + transactions: Transaction[], +) { const hasAnyViolations = hasMissingSmartscanFields(report.reportID, transactions) || hasAnyViolationsUtil(report.reportID, violations); const hasVisibleViolations = hasAnyViolations && ViolationsUtils.hasVisibleViolationsForUser(report, violations, currentUserEmail, policy, transactions); const isSubmitter = isCurrentUserSubmitter(report); @@ -181,7 +199,7 @@ function canReview(report: Report, violations: OnyxCollection transaction.transactionID) ?? []; - const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactions, violations); - const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactions, violations, currentUserEmail, report, policy); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactions, report, policy, violations, currentUserEmail); if (hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isAdmin || isSubmitter) && !isReportApproved({report}) && !isReportManuallyReimbursed(report))) { return true; @@ -209,9 +226,9 @@ function getReportPreviewAction( violations: OnyxCollection, isReportArchived: boolean, currentUserEmail: string, - report?: Report, - policy?: Policy, - transactions?: Transaction[], + report: Report | undefined, + policy: Policy | undefined, + transactions: Transaction[], invoiceReceiverPolicy?: Policy, isPaidAnimationRunning?: boolean, isApprovedAnimationRunning?: boolean, @@ -231,7 +248,7 @@ function getReportPreviewAction( if (isSubmittingAnimationRunning) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT; } - if (isAddExpenseAction(report, transactions ?? [], isReportArchived)) { + if (isAddExpenseAction(report, transactions, isReportArchived)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.ADD_EXPENSE; } @@ -244,7 +261,7 @@ function getReportPreviewAction( if (canSubmit(report, violations, isReportArchived, policy, transactions)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT; } - if (canApprove(report, violations, policy, transactions)) { + if (canApprove(report, violations, currentUserEmail, policy, transactions)) { return CONST.REPORT.REPORT_PREVIEW_ACTIONS.APPROVE; } if (canPay(report, violations, isReportArchived, policy, invoiceReceiverPolicy)) { diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index a1414b8f1de0..9bcbc47c2139 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -248,8 +248,8 @@ function isRemoveHoldAction(report: Report, chatReport: OnyxEntry, repor return isHolder; } -function isReviewDuplicatesAction(report: Report, reportTransactions: Transaction[]) { - const hasDuplicates = reportTransactions.some((transaction) => isDuplicate(transaction)); +function isReviewDuplicatesAction(report: Report, reportTransactions: Transaction[], currentUserEmail: string, policy: Policy | undefined) { + const hasDuplicates = reportTransactions.some((transaction) => isDuplicate(transaction, currentUserEmail, report, policy)); if (!hasDuplicates) { return false; @@ -277,8 +277,7 @@ function isMarkAsCashAction(currentUserEmail: string, report: Report, reportTran return false; } - const transactionIDs = reportTransactions.map((t) => t.transactionID); - const hasAllPendingRTERViolations = allHavePendingRTERViolation(reportTransactions, violations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(reportTransactions, violations, currentUserEmail, report, policy); if (hasAllPendingRTERViolations) { return true; @@ -288,7 +287,7 @@ function isMarkAsCashAction(currentUserEmail: string, report: Report, reportTran const isReportApprover = isApproverUtils(policy, currentUserEmail); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(reportTransactions, report, policy, violations, currentUserEmail); const userControlsReport = isReportSubmitter || isReportApprover || isAdmin; return userControlsReport && shouldShowBrokenConnectionViolation; } @@ -307,12 +306,12 @@ function isMarkAsResolvedAction(report?: Report, violations?: TransactionViolati return violations?.some((violation) => violation.name === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE); } -function isPrimaryMarkAsResolvedAction(report?: Report, reportTransactions?: Transaction[], violations?: OnyxCollection, policy?: Policy) { +function isPrimaryMarkAsResolvedAction(currentUserEmail: string, report?: Report, reportTransactions?: Transaction[], violations?: OnyxCollection, policy?: Policy) { if (!reportTransactions || reportTransactions.length !== 1) { return false; } - const transactionViolations = getTransactionViolations(reportTransactions.at(0), violations); + const transactionViolations = getTransactionViolations(reportTransactions.at(0), violations, currentUserEmail, report, policy); return isExpenseReportUtils(report) && isMarkAsResolvedAction(report, transactionViolations, policy); } @@ -368,7 +367,7 @@ function getReportPrimaryAction(params: GetReportPrimaryActionParams): ValueOf isDuplicate(transaction)); + const reportHasDuplicatedTransactions = reportTransactions.some((transaction) => isDuplicate(transaction, currentUserLogin, report, policy)); if (isExpenseReport && isProcessingReport && reportHasDuplicatedTransactions) { return true; @@ -235,9 +235,7 @@ function isApproveAction(currentUserLogin: string, report: Report, reportTransac return false; } - const transactionIDs = reportTransactions.map((t) => t.transactionID); - - const hasAllPendingRTERViolations = allHavePendingRTERViolation(reportTransactions, violations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(reportTransactions, violations, currentUserLogin, report, policy); if (hasAllPendingRTERViolations) { return true; @@ -245,7 +243,7 @@ function isApproveAction(currentUserLogin: string, report: Report, reportTransac const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, report, policy, violations); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(reportTransactions, report, policy, violations, currentUserLogin); const isReportApprover = isApproverUtils(policy, currentUserLogin); const userControlsReport = isReportApprover || isAdmin; return userControlsReport && shouldShowBrokenConnectionViolation; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 507681f3023d..dbe5b9b0c1b4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8893,9 +8893,9 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV return ( !isInvoiceReport(potentialReport) && ViolationsUtils.hasVisibleViolationsForUser(potentialReport, transactionViolations, currentUserEmail ?? '', policy, transactions) && - (hasViolations(potentialReport.reportID, transactionViolations, true) || - hasWarningTypeViolations(potentialReport.reportID, transactionViolations, true) || - hasNoticeTypeViolations(potentialReport.reportID, transactionViolations, true)) + (hasViolations(potentialReport.reportID, transactionViolations, true, transactions, currentUserEmail, potentialReport, policy) || + hasWarningTypeViolations(potentialReport.reportID, transactionViolations, true, transactions, currentUserEmail, potentialReport, policy) || + hasNoticeTypeViolations(potentialReport.reportID, transactionViolations, true, transactions, currentUserEmail, potentialReport, policy)) ); }); } @@ -8908,9 +8908,12 @@ function hasViolations( transactionViolations: OnyxCollection, shouldShowInReview?: boolean, reportTransactions?: Transaction[], + currentUserEmailParam?: string, + report?: OnyxEntry, + policy?: OnyxEntry, ): boolean { const transactions = reportTransactions ?? getReportTransactions(reportID); - return transactions.some((transaction) => hasViolation(transaction, transactionViolations, shouldShowInReview)); + return transactions.some((transaction) => hasViolation(transaction, transactionViolations, currentUserEmailParam ?? '', report, policy, shouldShowInReview)); } /** @@ -8921,9 +8924,12 @@ function hasWarningTypeViolations( transactionViolations: OnyxCollection, shouldShowInReview?: boolean, reportTransactions?: Transaction[], + currentUserEmailParam?: string, + report?: OnyxEntry, + policy?: OnyxEntry, ): boolean { const transactions = reportTransactions ?? getReportTransactions(reportID); - return transactions.some((transaction) => hasWarningTypeViolation(transaction, transactionViolations, shouldShowInReview)); + return transactions.some((transaction) => hasWarningTypeViolation(transaction, transactionViolations, currentUserEmailParam ?? '', report, policy, shouldShowInReview)); } /** @@ -8954,19 +8960,29 @@ function hasNoticeTypeViolations( transactionViolations: OnyxCollection, shouldShowInReview?: boolean, reportTransactions?: Transaction[], + currentUserEmailParam?: string, + report?: OnyxEntry, + policy?: OnyxEntry, ): boolean { const transactions = reportTransactions ?? getReportTransactions(reportID); - return transactions.some((transaction) => hasNoticeTypeViolation(transaction, transactionViolations, shouldShowInReview)); + return transactions.some((transaction) => hasNoticeTypeViolation(transaction, transactionViolations, currentUserEmailParam ?? '', report, policy, shouldShowInReview)); } /** * Checks to see if a report contains any type of violation */ -function hasAnyViolations(reportID: string | undefined, transactionViolations: OnyxCollection, reportTransactions?: Transaction[]) { +function hasAnyViolations( + reportID: string | undefined, + transactionViolations: OnyxCollection, + reportTransactions?: Transaction[], + currentUserEmailParam?: string, + report?: OnyxEntry, + policy?: OnyxEntry, +) { return ( - hasViolations(reportID, transactionViolations, undefined, reportTransactions) || - hasNoticeTypeViolations(reportID, transactionViolations, true, reportTransactions) || - hasWarningTypeViolations(reportID, transactionViolations, true, reportTransactions) + hasViolations(reportID, transactionViolations, undefined, reportTransactions, currentUserEmailParam, report, policy) || + hasNoticeTypeViolations(reportID, transactionViolations, true, reportTransactions, currentUserEmailParam, report, policy) || + hasWarningTypeViolations(reportID, transactionViolations, true, reportTransactions, currentUserEmailParam, report, policy) ); } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index ad27a0ee2312..f0b1ec4d1725 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -915,12 +915,14 @@ function getTransactionViolations( // eslint-disable-next-line @typescript-eslint/no-deprecated transaction: SearchTransaction, currentUserEmail: string, + report: OnyxEntry, + policy: OnyxEntry, ): OnyxTypes.TransactionViolation[] { const transactionViolations = allViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]; if (!transactionViolations) { return []; } - return transactionViolations.filter((violation) => !isViolationDismissed(transaction, violation, currentUserEmail)); + return transactionViolations.filter((violation) => !isViolationDismissed(transaction, violation, currentUserEmail, report, policy)); } /** @@ -1069,7 +1071,7 @@ function getTransactionsSections( const reportAction = moneyRequestReportActionsByTransactionID.get(transactionItem.transactionID); const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const shouldShowBlankTo = !report || isOpenExpenseReport(report); - const transactionViolations = getTransactionViolations(allViolations, transactionItem, currentUserEmail); + const transactionViolations = getTransactionViolations(allViolations, transactionItem, currentUserEmail, report, policy); // Use Map.get() for faster lookups with default values const from = reportAction?.actorAccountID ? (personalDetailsMap.get(reportAction.actorAccountID.toString()) ?? emptyPersonalDetails) : emptyPersonalDetails; const to = getToFieldValueForTransaction(transactionItem, report, data.personalDetailsList, reportAction); @@ -1256,7 +1258,7 @@ function getActions( const chatReportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.chatReportID}`] ?? undefined; const isChatReportArchived = isArchivedReport(chatReportRNVP); - const hasAnyViolationsForReport = hasAnyViolations(report.reportID, allViolations, allReportTransactions); + const hasAnyViolationsForReport = hasAnyViolations(report.reportID, allViolations, allReportTransactions, currentUserEmail, report, policy); const hasVisibleViolationsForReport = hasAnyViolationsForReport && ViolationsUtils.hasVisibleViolationsForUser(report, allViolations, currentUserEmail, policy, allReportTransactions); // Only check for violations if we need to (when user has permission to review) @@ -1555,7 +1557,7 @@ function getReportSections( const report = data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`] as SearchReport | undefined; const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const shouldShowBlankTo = !report || isOpenExpenseReport(report); - const transactionViolations = getTransactionViolations(allViolations, transactionItem, currentUserEmail); + const transactionViolations = getTransactionViolations(allViolations, transactionItem, currentUserEmail, report, policy); const actions = Object.values(reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionItem.reportID}`] ?? {}); const from = reportAction?.actorAccountID ? (data.personalDetailsList?.[reportAction.actorAccountID] ?? emptyPersonalDetails) : emptyPersonalDetails; const to = getToFieldValueForTransaction(transactionItem, report, data.personalDetailsList, reportAction); diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index 708a36518d83..0285cfd4a172 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -10,6 +10,8 @@ import {abandonReviewDuplicateTransactions, setReviewDuplicatesKey} from './acti import {isCategoryMissing} from './CategoryUtils'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; +import {getCurrentUserEmail} from './Network/NetworkStore'; +import {getPolicy} from './PolicyUtils'; import {getOriginalMessage, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils'; import { hasActionWithErrorsForTransaction, @@ -212,7 +214,12 @@ function getTransactionPreviewTextAndTranslationPaths({ const isTransactionScanning = isScanning(transaction); const hasFieldErrors = hasMissingSmartscanFields(transaction); const isPaidGroupPolicy = isPaidGroupPolicyUtil(iouReport); - const hasViolationsOfTypeNotice = hasNoticeTypeViolation(transaction, violations, true) && isPaidGroupPolicy; + const currentUserEmail = getCurrentUserEmail(); + + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line @typescript-eslint/no-deprecated + const policy = getPolicy(iouReport?.policyID); + const hasViolationsOfTypeNotice = hasNoticeTypeViolation(transaction, violations, currentUserEmail ?? '', iouReport, policy, true) && isPaidGroupPolicy; const hasActionWithErrors = hasActionWithErrorsForTransaction(iouReport?.reportID, transaction); const {amount: requestAmount, currency: requestCurrency} = transactionDetails; @@ -359,7 +366,13 @@ function createTransactionPreviewConditionals({ const isApproved = isReportApproved({report: iouReport}); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; - const hasViolationsOfTypeNotice = hasNoticeTypeViolation(transaction, violations) && iouReport && isPaidGroupPolicyUtil(iouReport); + const currentUserEmail = getCurrentUserEmail(); + + // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 + // eslint-disable-next-line @typescript-eslint/no-deprecated + const policy = getPolicy(iouReport?.policyID); + const hasViolationsOfTypeNotice = + hasNoticeTypeViolation(transaction, violations, currentUserEmail ?? '', iouReport ?? undefined, policy, true) && iouReport && isPaidGroupPolicyUtil(iouReport); const hasFieldErrors = hasMissingSmartscanFields(transaction); const isFetchingWaypoints = isFetchingWaypointsFromServer(transaction); @@ -379,8 +392,8 @@ function createTransactionPreviewConditionals({ isUnreportedAndHasInvalidDistanceRateTransaction(transaction) || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing hasViolationsOfTypeNotice || - hasWarningTypeViolation(transaction, violations) || - hasViolation(transaction, violations, true) || + hasWarningTypeViolation(transaction, violations, currentUserEmail ?? '', iouReport ?? undefined, policy) || + hasViolation(transaction, violations, currentUserEmail ?? '', iouReport ?? undefined, policy, true) || (isDistanceRequest(transaction) && violations?.some( (violation) => violation.name === CONST.VIOLATIONS.MODIFIED_AMOUNT && (violation.type === CONST.VIOLATION_TYPES.VIOLATION || violation.type === CONST.VIOLATION_TYPES.NOTICE), diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index abcdb26bb148..da2cd6b5f4ca 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -20,7 +20,7 @@ import {toLocaleDigit} from '@libs/LocaleDigitUtils'; import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils'; -import {getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; +import {getLoginsByAccountIDs, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; import { getCommaSeparatedTagNameWithSanitizedColons, getDistanceRateCustomUnit, @@ -68,6 +68,7 @@ import type { 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'; +// eslint-disable-next-line @typescript-eslint/no-deprecated import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type { Comment, @@ -1095,16 +1096,19 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry, r * Get all transaction violations of the transaction with given transactionID. */ function getTransactionViolations( + // eslint-disable-next-line @typescript-eslint/no-deprecated transaction: OnyxEntry, transactionViolations: OnyxCollection, - currentUserEmail?: string, + currentUserEmail: string, + iouReport: OnyxEntry, + policy: OnyxEntry, ): TransactionViolations | undefined { if (!transaction || !transactionViolations) { return undefined; } return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transaction.transactionID]?.filter( - (violation) => !isViolationDismissed(transaction, violation, currentUserEmail), + (violation) => !isViolationDismissed(transaction, violation, currentUserEmail, iouReport, policy), ); } @@ -1128,8 +1132,15 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if there is broken connection violation. */ -function hasBrokenConnectionViolation(transaction: Transaction | SearchTransaction, transactionViolations: OnyxCollection | undefined): boolean { - const violations = getTransactionViolations(transaction, transactionViolations); +function hasBrokenConnectionViolation( + // eslint-disable-next-line @typescript-eslint/no-deprecated + transaction: Transaction | SearchTransaction, + transactionViolations: OnyxCollection | undefined, + currentUserEmail: string, + report: OnyxEntry, + policy: OnyxEntry, +): boolean { + const violations = getTransactionViolations(transaction, transactionViolations, currentUserEmail, report, policy); return !!violations?.find((violation) => isBrokenConnectionViolation(violation)); } @@ -1171,15 +1182,32 @@ function shouldShowBrokenConnectionViolation(report: OnyxEntry | SearchR * Check if user should see broken connection violation warning based on selected transactions. */ function shouldShowBrokenConnectionViolationForMultipleTransactions( - transactionIDs: string[], + transactions: Transaction[], // eslint-disable-next-line @typescript-eslint/no-deprecated report: OnyxEntry | SearchReport, policy: OnyxEntry, transactionViolations: OnyxCollection, + currentUserEmail: string, ): boolean { - const violations = transactionIDs.flatMap((id) => transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []); + const brokenConnectionViolations = transactions.flatMap((transaction) => { + const violations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] ?? []; - const brokenConnectionViolations = violations.filter((violation) => isBrokenConnectionViolation(violation)); + if (!transaction) { + return []; + } + + return violations.filter((violation) => { + if (!isBrokenConnectionViolation(violation)) { + return false; + } + + if (isViolationDismissed(transaction, violation, currentUserEmail, report, policy)) { + return false; + } + + return shouldShowViolation(report, policy, violation.name, currentUserEmail); + }); + }); return shouldShowBrokenConnectionViolationInternal(brokenConnectionViolations, report, policy); } @@ -1243,13 +1271,25 @@ function shouldShowViolation( /** * Check if there is pending rter violation in all transactionViolations with given transactionIDs. */ -function allHavePendingRTERViolation(transactions: OnyxEntry, transactionViolations: OnyxCollection | undefined): boolean { +function allHavePendingRTERViolation( + // eslint-disable-next-line @typescript-eslint/no-deprecated + transactions: OnyxEntry, + transactionViolations: OnyxCollection | undefined, + currentUserEmail: string, + report: OnyxEntry, + policy: OnyxEntry, +): boolean { if (!transactions) { return false; } const transactionsWithRTERViolations = transactions.map((transaction) => { - const filteredTransactionViolations = getTransactionViolations(transaction, transactionViolations); + // Get violations not dismissed by current user + const filteredTransactionViolations = getTransactionViolations(transaction, transactionViolations, currentUserEmail, report, policy)?.filter((violation) => + // Further filter to only violations visible to the current user + shouldShowViolation(report, policy, violation.name, currentUserEmail), + ); + // Check if there is pending rter violation in the filtered violations return hasPendingRTERViolation(filteredTransactionViolations); }); return transactionsWithRTERViolations.length > 0 && transactionsWithRTERViolations.every((value) => value === true); @@ -1265,11 +1305,18 @@ function checkIfShouldShowMarkAsCashButton(hasRTERPendingViolation: boolean, sho /** * Check if there is any transaction without RTER violation within the given transactionIDs. */ -function hasAnyTransactionWithoutRTERViolation(transactions: Transaction[] | SearchTransaction[], transactionViolations: OnyxCollection | undefined): boolean { +function hasAnyTransactionWithoutRTERViolation( + // eslint-disable-next-line @typescript-eslint/no-deprecated + transactions: Transaction[] | SearchTransaction[], + transactionViolations: OnyxCollection | undefined, + currentUserEmail: string, + report: OnyxEntry, + policy: OnyxEntry, +): boolean { return ( transactions.length > 0 && transactions.some((transaction) => { - return !hasBrokenConnectionViolation(transaction, transactionViolations); + return !hasBrokenConnectionViolation(transaction, transactionViolations, currentUserEmail, report, policy); }) ); } @@ -1359,7 +1406,7 @@ function getRecentTransactions(transactions: Record, size = 2): * Check if transaction has duplicatedTransaction violation. * @param transactionID - the transaction to check */ -function isDuplicate(transaction: OnyxEntry): boolean { +function isDuplicate(transaction: OnyxEntry, currentUserEmail: string, iouReport: OnyxEntry, policy: OnyxEntry): boolean { if (!transaction) { return false; } @@ -1367,7 +1414,7 @@ function isDuplicate(transaction: OnyxEntry): boolean { (violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, ); const hasDuplicatedTransactionViolation = !!duplicatedTransactionViolation; - const isDuplicatedTransactionViolationDismissed = isViolationDismissed(transaction, duplicatedTransactionViolation); + const isDuplicatedTransactionViolationDismissed = isViolationDismissed(transaction, duplicatedTransactionViolation, currentUserEmail, iouReport, policy); return hasDuplicatedTransactionViolation && !isDuplicatedTransactionViolationDismissed; } @@ -1384,13 +1431,53 @@ function isOnHold(transaction: OnyxEntry): boolean { } /** - * Checks if a violation is dismissed for the given transaction + * Checks if a violation is dismissed for the given transaction. */ -function isViolationDismissed(transaction: OnyxEntry, violation: TransactionViolation | undefined, currentUserEmail?: string): boolean { +function isViolationDismissed( + transaction: OnyxEntry, + violation: TransactionViolation | undefined, + currentUserEmail: string, + iouReport: OnyxEntry, + policy: OnyxEntry, +): boolean { if (!transaction || !violation) { return false; } - return !!transaction?.comment?.dismissedViolations?.[violation.name]?.[currentUserEmail ?? deprecatedCurrentUserEmail]; + + const violationDismissals = transaction.comment?.dismissedViolations?.[violation.name]; + if (!violationDismissals) { + return false; + } + + const dismissedByEmails = Object.keys(violationDismissals); + + // Current user dismissed it themselves + if (dismissedByEmails.includes(currentUserEmail || deprecatedCurrentUserEmail)) { + return true; + } + + // RTER violations on instant submit reports only need to be dismissed by one person to be considered dismissed + if (violation.name === CONST.VIOLATIONS.RTER && policy && isInstantSubmitEnabled(policy)) { + return dismissedByEmails.length > 0; + } + + // If the admin is looking at an open report, we check for both, submitter and admin. + if (!iouReport) { + return false; + } + + const currentUserAccountID = deprecatedCurrentUserAccountID; + const isSubmitter = iouReport.ownerAccountID === currentUserAccountID; + const shouldViewAsSubmitter = !isSubmitter && isOpenExpenseReport(iouReport); + + if (shouldViewAsSubmitter && iouReport.ownerAccountID) { + const reportOwnerEmail = getLoginsByAccountIDs([iouReport.ownerAccountID]).at(0); + if (reportOwnerEmail && dismissedByEmails.includes(reportOwnerEmail)) { + return true; + } + } + + return false; } /** @@ -1406,7 +1493,14 @@ function doesTransactionSupportViolations(transaction: Transaction | undefined): /** * Checks if any violations for the provided transaction are of type 'violation' */ -function hasViolation(transaction: Transaction | undefined, transactionViolations: TransactionViolation[] | OnyxCollection, showInReview?: boolean): boolean { +function hasViolation( + transaction: Transaction | undefined, + transactionViolations: TransactionViolation[] | OnyxCollection, + currentUserEmail: string, + iouReport: OnyxEntry, + policy: OnyxEntry, + showInReview?: boolean, +): boolean { if (!doesTransactionSupportViolations(transaction)) { return false; } @@ -1416,15 +1510,21 @@ function hasViolation(transaction: Transaction | undefined, transactionViolation (violation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && - !isViolationDismissed(transaction, violation), + !isViolationDismissed(transaction, violation, currentUserEmail, iouReport, policy), ); } -function hasDuplicateTransactions(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean { - const transactionsByIouReportID = getReportTransactions(iouReportID); +function hasDuplicateTransactions( + currentUserEmail: string, + iouReport: OnyxEntry, + policy: OnyxEntry, + // eslint-disable-next-line @typescript-eslint/no-deprecated + allReportTransactions?: SearchTransaction[], +): boolean { + const transactionsByIouReportID = getReportTransactions(iouReport?.reportID); const reportTransactions = allReportTransactions ?? transactionsByIouReportID; - return reportTransactions.length > 0 && reportTransactions.some((transaction) => isDuplicate(transaction)); + return reportTransactions.length > 0 && reportTransactions.some((transaction) => isDuplicate(transaction, currentUserEmail, iouReport, policy)); } /** @@ -1433,6 +1533,9 @@ function hasDuplicateTransactions(iouReportID?: string, allReportTransactions?: function hasNoticeTypeViolation( transaction: OnyxEntry, transactionViolations: TransactionViolation[] | OnyxCollection, + currentUserEmail: string, + iouReport: OnyxEntry, + policy: OnyxEntry, showInReview?: boolean, ): boolean { if (!doesTransactionSupportViolations(transaction)) { @@ -1444,7 +1547,7 @@ function hasNoticeTypeViolation( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && - !isViolationDismissed(transaction, violation), + !isViolationDismissed(transaction, violation, currentUserEmail, iouReport, policy), ); } @@ -1454,18 +1557,22 @@ function hasNoticeTypeViolation( function hasWarningTypeViolation( transaction: OnyxEntry, transactionViolations: TransactionViolation[] | OnyxCollection, + currentUserEmail: string, + iouReport: OnyxEntry, + policy: OnyxEntry, showInReview?: boolean, ): boolean { if (!doesTransactionSupportViolations(transaction)) { return false; } const violations = Array.isArray(transactionViolations) ? transactionViolations : transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`]; + const warningTypeViolations = violations?.filter( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.WARNING && (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && - !isViolationDismissed(transaction, violation), + !isViolationDismissed(transaction, violation, currentUserEmail, iouReport, policy), ) ?? []; return warningTypeViolations.length > 0; @@ -1997,8 +2104,8 @@ function getAllSortedTransactions(iouReportID?: string): Array, policy: OnyxEntry) { + return transactions?.length === 1 && hasPendingUI(transactions?.at(0), getTransactionViolations(transactions?.at(0), allTransactionViolations, currentUserEmail, report, policy)); } function isExpenseSplit(transaction: OnyxEntry, originalTransaction: OnyxEntry): boolean { diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 0298462bb3aa..7b23792f046c 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -11,7 +11,7 @@ import {isReceiptError} from '@libs/ErrorUtils'; import Parser from '@libs/Parser'; import {getDistanceRateCustomUnitRate, getPerDiemRateCustomUnitRate, getSortedTagKeys, isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import {shouldShowViolation} from '@libs/TransactionUtils'; +import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, Report, ReportAction, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; @@ -619,13 +619,14 @@ const ViolationsUtils = { /** * Checks if any transactions in the report have violations that should be visible to the current user. * Filters violations based on user role (submitter, admin, policy member) and report state. + * Also filters out dismissed violations. */ hasVisibleViolationsForUser( report: OnyxEntry, violations: OnyxCollection, currentUserEmail: string, - policy?: OnyxEntry, - transactions?: Transaction[], + policy: OnyxEntry, + transactions: Transaction[], ): boolean { if (!report || !violations || !transactions) { return false; @@ -638,9 +639,9 @@ const ViolationsUtils = { return false; } - // Check if any violation should be shown based on user role and violation type + // Check if any violation is not dismissed and should be shown based on user role and violation type return transactionViolations.some((violation: TransactionViolation) => { - return shouldShowViolation(report, policy, violation.name, currentUserEmail); + return !isViolationDismissed(transaction, violation, currentUserEmail, report, policy) && shouldShowViolation(report, policy, violation.name, currentUserEmail); }); }); }, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e53d9691252a..825e8796bfc9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -261,7 +261,7 @@ import {buildAddMembersToWorkspaceOnyxData, buildUpdateWorkspaceMembersRoleOnyxD import {buildOptimisticRecentlyUsedCurrencies, buildPolicyData, generatePolicyID} from './Policy/Policy'; import {buildOptimisticPolicyRecentlyUsedTags, getPolicyTagsData} from './Policy/Tag'; import type {GuidedSetupData} from './Report'; -import {buildInviteToRoomOnyxData, completeOnboarding, getCurrentUserAccountID, notifyNewAction, optimisticReportLastData} from './Report'; +import {buildInviteToRoomOnyxData, completeOnboarding, getCurrentUserAccountID, getCurrentUserEmail, notifyNewAction, optimisticReportLastData} from './Report'; import {clearAllRelatedReportActionErrors} from './ReportActions'; import {sanitizeRecentWaypoints} from './Transaction'; import {removeDraftSplitTransaction, removeDraftTransaction, removeDraftTransactions} from './TransactionEdit'; @@ -10278,10 +10278,11 @@ function canSubmitReport( isReportArchived: boolean, ) { const currentUserAccountID = getCurrentUserAccountID(); + const currentUserEmailValue = getCurrentUserEmail() ?? ''; const isOpenExpenseReport = isOpenExpenseReportReportUtils(report); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactions, allViolations); - const hasTransactionWithoutRTERViolation = hasAnyTransactionWithoutRTERViolation(transactions, allViolations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactions, allViolations, currentUserEmail, report, policy); + const hasTransactionWithoutRTERViolation = hasAnyTransactionWithoutRTERViolation(transactions, allViolations, currentUserEmailValue, report, policy); const hasOnlyPendingCardOrScanFailTransactions = transactions.length > 0 && transactions.every((t) => isPendingCardOrScanningTransaction(t)); return ( @@ -10332,7 +10333,7 @@ function approveMoneyRequest( const currentNextStepDeprecated = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; let total = expenseReport.total ?? 0; const hasHeldExpenses = hasHeldExpensesReportUtils(expenseReport.reportID); - const hasDuplicates = hasDuplicateTransactions(expenseReport.reportID); + const hasDuplicates = hasDuplicateTransactions(currentUserEmailParam, expenseReport, policy); if (hasHeldExpenses && !full && !!expenseReport.unheldTotal) { total = expenseReport.unheldTotal; } @@ -10506,7 +10507,7 @@ function approveMoneyRequest( // Remove duplicates violations if we approve the report if (hasDuplicates) { - const transactions = getReportTransactions(expenseReport.reportID).filter((transaction) => isDuplicate(transaction)); + const transactions = getReportTransactions(expenseReport.reportID).filter((transaction) => isDuplicate(transaction, currentUserEmailParam, expenseReport, policy)); if (!full) { transactions.filter((transaction) => !isOnHold(transaction)); } diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index 1dcab6828c18..215fd56c2887 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -73,7 +73,8 @@ describe('getReportPreviewAction', () => { } as unknown as Report; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); - expect(getReportPreviewAction(VIOLATIONS, false, CURRENT_USER_EMAIL, report, undefined, [])).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.ADD_EXPENSE); + const policy = createRandomPolicy(0, CONST.POLICY.TYPE.PERSONAL); + expect(getReportPreviewAction(VIOLATIONS, false, CURRENT_USER_EMAIL, report, policy, [])).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.ADD_EXPENSE); }); it('canSubmit should return true for expense preview report with manual submit', async () => { diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index 45bc4ce1ff36..45f48ba6374b 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -248,7 +248,7 @@ describe('hasRTERWithoutViolation', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithViolation}`, transactionWithViolation); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithoutViolation}`, transactionWithoutViolation); - expect(hasAnyTransactionWithoutRTERViolation([transactionWithoutViolation, transactionWithViolation], violations)).toBe(true); + expect(hasAnyTransactionWithoutRTERViolation([transactionWithoutViolation, transactionWithViolation], violations, '', undefined, undefined)).toBe(true); }); test('Return false if there is no rter without violation in all transactionViolations with given transactionIDs.', async () => { @@ -277,7 +277,7 @@ describe('hasRTERWithoutViolation', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionIDWithViolation}`, transactionWithViolation); - expect(hasAnyTransactionWithoutRTERViolation([transactionWithViolation], violations)).toBe(false); + expect(hasAnyTransactionWithoutRTERViolation([transactionWithViolation], violations, '', undefined, undefined)).toBe(false); }); }); diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index befac8f8b520..bfb399f1dcf5 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -753,7 +753,7 @@ describe('isReviewDuplicatesAction', () => { } as TransactionViolation, ]); - expect(isReviewDuplicatesAction(report, [transaction])).toBe(true); + expect(isReviewDuplicatesAction(report, [transaction], CURRENT_USER_EMAIL, undefined)).toBe(true); }); it('should return false when report approver has no duplicated transactions', async () => { @@ -773,7 +773,7 @@ describe('isReviewDuplicatesAction', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${TRANSACTION_ID}`, transaction); - expect(isReviewDuplicatesAction(report, [transaction])).toBe(false); + expect(isReviewDuplicatesAction(report, [transaction], CURRENT_USER_EMAIL, undefined)).toBe(false); }); it('should return false when current user is neither the report submitter nor approver', async () => { @@ -798,7 +798,7 @@ describe('isReviewDuplicatesAction', () => { } as TransactionViolation, ]); - expect(isReviewDuplicatesAction(report, [transaction])).toBe(false); + expect(isReviewDuplicatesAction(report, [transaction], CURRENT_USER_EMAIL, undefined)).toBe(false); }); }); @@ -1191,7 +1191,7 @@ describe('getTransactionThreadPrimaryAction', () => { } as unknown as Transaction, ]; - const result = isPrimaryMarkAsResolvedAction(report, reportTransactions, violations, policy); + const result = isPrimaryMarkAsResolvedAction(CURRENT_USER_EMAIL, report, reportTransactions, violations, policy); expect(result).toBe(true); }); @@ -1224,7 +1224,7 @@ describe('getTransactionThreadPrimaryAction', () => { } as unknown as Transaction, ]; - const result = isPrimaryMarkAsResolvedAction(report, reportTransactions, violations, policy); + const result = isPrimaryMarkAsResolvedAction(CURRENT_USER_EMAIL, report, reportTransactions, violations, policy); expect(result).toBe(false); }); @@ -1254,7 +1254,7 @@ describe('getTransactionThreadPrimaryAction', () => { } as unknown as Transaction, ]; - const result = isPrimaryMarkAsResolvedAction(report, reportTransactions, violations, policy); + const result = isPrimaryMarkAsResolvedAction(CURRENT_USER_EMAIL, report, reportTransactions, violations, policy); expect(result).toBe(false); }); @@ -1284,7 +1284,7 @@ describe('getTransactionThreadPrimaryAction', () => { } as unknown as Transaction, ]; - const result = isPrimaryMarkAsResolvedAction(report, reportTransactions, violations, policy); + const result = isPrimaryMarkAsResolvedAction(CURRENT_USER_EMAIL, report, reportTransactions, violations, policy); expect(result).toBe(false); }); }); diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 5f17e748c59a..f5bf07ec05d7 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -38,6 +38,7 @@ function generateTransaction(values: Partial = {}): Transaction { const CURRENT_USER_ID = 1; const CURRENT_USER_EMAIL = 'test@example.com'; +const OTHER_USER_EMAIL = 'other@example.com'; const SECOND_USER_ID = 2; const FAKE_OPEN_REPORT_ID = 'FAKE_OPEN_REPORT_ID'; const FAKE_OPEN_REPORT_SECOND_USER_ID = 'FAKE_OPEN_REPORT_SECOND_USER_ID'; @@ -459,7 +460,7 @@ describe('TransactionUtils', () => { state: CONST.IOU.RECEIPT_STATE.SCAN_READY, }, }); - expect(TransactionUtils.shouldShowRTERViolationMessage([transaction])).toBe(true); + expect(TransactionUtils.shouldShowRTERViolationMessage([transaction], '', undefined, undefined)).toBe(true); }); }); @@ -507,10 +508,13 @@ describe('TransactionUtils', () => { }); it('should return true when a broken connection violation exists for any of the provided transactions and the user is the policy member', () => { - const policy = {role: CONST.POLICY.ROLE.USER} as Policy; + const policy = { + role: CONST.POLICY.ROLE.USER, + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + } as Policy; const transaction1 = generateTransaction(); const transaction2 = generateTransaction(); - const transactionIDs = [transaction1.transactionID, transaction2.transactionID]; const transactionViolations = { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [ { @@ -520,7 +524,13 @@ describe('TransactionUtils', () => { }, ], }; - const showBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactionIDs, undefined, policy, transactionViolations); + const showBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions( + [transaction1, transaction2], + undefined, + policy, + transactionViolations, + CURRENT_USER_EMAIL, + ); expect(showBrokenConnectionViolation).toBe(true); }); @@ -770,19 +780,309 @@ describe('TransactionUtils', () => { }); describe('isViolationDismissed', () => { - it('should return true when violation is dismissed for current user', () => { - const transaction = generateTransaction({ - comment: { - dismissedViolations: { - [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { - [CURRENT_USER_EMAIL]: DateUtils.getDBTime(), + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: { + email: CURRENT_USER_EMAIL, + accountID: CURRENT_USER_ID, + }, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + [CURRENT_USER_ID]: { + accountID: CURRENT_USER_ID, + login: CURRENT_USER_EMAIL, + displayName: 'Current User', + }, + [SECOND_USER_ID]: { + accountID: SECOND_USER_ID, + login: OTHER_USER_EMAIL, + displayName: 'Second User', }, }, }, }); - const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; - const result = TransactionUtils.isViolationDismissed(transaction, violation); - expect(result).toBe(true); + }); + + afterEach(() => Onyx.clear()); + + describe('Current user dismissed it themselves', () => { + it('should return true when current user dismissed the violation', () => { + // Given a transaction with a violation dismissed by current user + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { + [CURRENT_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When checking if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, undefined); + + // Then it should return true + expect(result).toBe(true); + }); + + it('should return false when violation is not dismissed at all', () => { + // Given a transaction with no dismissed violations + const transaction = generateTransaction({ + comment: {}, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When checking if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, undefined); + + // Then it should return false + expect(result).toBe(false); + }); + + it('should return false when violation was dismissed by someone else only', () => { + // Given a transaction with a violation dismissed by another user + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When checking if violation is dismissed for current user + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, undefined); + + // Then it should return false since current user hasn't dismissed it + expect(result).toBe(false); + }); + }); + + describe('Admin viewing OPEN report AND report owner dismissed it', () => { + it('should return true when admin views open report and owner dismissed violation', () => { + // Given an OPEN report owned by user 2 + const iouReport: Report = { + ...openReport, + ownerAccountID: SECOND_USER_ID, + }; + + // And a transaction where owner (user 2) dismissed a violation + const transaction = generateTransaction({ + reportID: iouReport.reportID, + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When current user (admin, not the owner) checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, iouReport, undefined); + + // Then it should return true because admin sees owner's perspective on open reports + expect(result).toBe(true); + }); + + it('should return false when admin views PROCESSING report and only owner dismissed violation', () => { + // Given a PROCESSING report (not OPEN) owned by user 2 + const iouReport: Report = { + ...processingReport, + ownerAccountID: SECOND_USER_ID, + }; + + // And a transaction where owner dismissed a violation + const transaction = generateTransaction({ + reportID: iouReport.reportID, + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When current user (admin, not the owner) checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, iouReport, undefined); + + // Then it should return false because on processing reports, admin must dismiss separately + expect(result).toBe(false); + }); + + it('should return false when submitter views their own open report (not condition 2)', () => { + // Given an OPEN report owned by current user + const iouReport: Report = { + ...openReport, + ownerAccountID: CURRENT_USER_ID, + }; + + // And a transaction where someone else dismissed a violation + const transaction = generateTransaction({ + reportID: iouReport.reportID, + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When current user (the submitter) checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, iouReport, undefined); + + // Then it should return false (condition 2 doesn't apply to submitters) + expect(result).toBe(false); + }); + }); + + describe('RTER violation on instant submit policy - dismissed by anyone', () => { + it('should return true when RTER violation dismissed by anyone on instant submit policy', () => { + // Given an instant submit policy + const policy: Policy = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + }; + + // And a transaction with RTER violation dismissed by someone else + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.RTER]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; + + // When current user checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, policy); + + // Then it should return true because on instant submit, anyone's dismissal counts + expect(result).toBe(true); + }); + + it('should return false when RTER violation dismissed by anyone on NON-instant submit policy', () => { + // Given a non-instant submit policy + const policy: Policy = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, + }; + + // And a transaction with RTER violation dismissed by someone else + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.RTER]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; + + // When current user checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, policy); + + // Then it should return false because on non-instant submit, each person must dismiss separately + expect(result).toBe(false); + }); + + it('should return false when non-RTER violation dismissed by anyone on instant submit policy', () => { + // Given an instant submit policy + const policy: Policy = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + }; + + // And a transaction with non-RTER violation dismissed by someone else + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + + // When current user checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, policy); + + // Then it should return false because condition 3 only applies to RTER violations + expect(result).toBe(false); + }); + + it('should return true when RTER violation dismissed by multiple people on instant submit policy', () => { + // Given an instant submit policy + const policy: Policy = { + ...createRandomPolicy(0, CONST.POLICY.TYPE.TEAM), + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + }; + + // And a transaction with RTER violation dismissed by multiple people + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.RTER]: { + [OTHER_USER_EMAIL]: DateUtils.getDBTime(), + [CURRENT_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}; + + // When current user checks if violation is dismissed + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, policy); + + // Then it should return true + expect(result).toBe(true); + }); + }); + + describe('Edge cases and data validation', () => { + it('should return false when transaction is null', () => { + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const result = TransactionUtils.isViolationDismissed(undefined, violation, CURRENT_USER_EMAIL, undefined, undefined); + expect(result).toBe(false); + }); + + it('should return false when violation is null', () => { + const transaction = generateTransaction({}); + const result = TransactionUtils.isViolationDismissed(transaction, undefined, CURRENT_USER_EMAIL, undefined, undefined); + expect(result).toBe(false); + }); + + it('should return false when violation name does not exist in dismissedViolations', () => { + const transaction = generateTransaction({ + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.SMARTSCAN_FAILED]: { + [CURRENT_USER_EMAIL]: DateUtils.getDBTime(), + }, + }, + }, + }); + const violation = {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}; + const result = TransactionUtils.isViolationDismissed(transaction, violation, CURRENT_USER_EMAIL, undefined, undefined); + expect(result).toBe(false); + }); }); }); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 02d605138b94..9762b504d450 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -602,8 +602,47 @@ describe('getViolations', () => { await Onyx.multiSet({...transactionCollectionDataSet}); - const isSmartScanDismissed = isViolationDismissed(transaction, smartScanFailedViolation); - const isDuplicateViolationDismissed = isViolationDismissed(transaction, duplicatedTransactionViolation); + const isSmartScanDismissed = isViolationDismissed(transaction, smartScanFailedViolation, CARLOS_EMAIL, undefined, undefined); + const isDuplicateViolationDismissed = isViolationDismissed(transaction, duplicatedTransactionViolation, CARLOS_EMAIL, undefined, undefined); + + expect(isSmartScanDismissed).toBeTruthy(); + expect(isDuplicateViolationDismissed).toBeFalsy(); + }); + + it('should check if violation is dismissed or not (with report and policy params)', async () => { + const policy: Policy = { + id: 'test-policy-id', + name: 'Test Policy', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + owner: CARLOS_EMAIL, + isPolicyExpenseChatEnabled: false, + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, + outputCurrency: CONST.CURRENCY.USD, + }; + + const report: Report = { + reportID: 'test-report-id', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CARLOS_ACCOUNT_ID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const transaction = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + + await Onyx.multiSet({...transactionCollectionDataSet}); + + const isSmartScanDismissed = isViolationDismissed(transaction, smartScanFailedViolation, CARLOS_EMAIL, report, policy); + const isDuplicateViolationDismissed = isViolationDismissed(transaction, duplicatedTransactionViolation, CARLOS_EMAIL, report, policy); expect(isSmartScanDismissed).toBeTruthy(); expect(isDuplicateViolationDismissed).toBeFalsy(); @@ -625,7 +664,48 @@ describe('getViolations', () => { await Onyx.multiSet({...transactionCollectionDataSet}); // Should filter out the smartScanFailedViolation - const filteredViolations = getTransactionViolations(transaction, transactionViolationsCollection); + const filteredViolations = getTransactionViolations(transaction, transactionViolationsCollection, CARLOS_EMAIL, undefined, undefined); + expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); + }); + + it('should return filtered out dismissed violations (with report and policy params)', async () => { + const policy: Policy = { + id: 'test-policy-id', + name: 'Test Policy', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + owner: CARLOS_EMAIL, + isPolicyExpenseChatEnabled: false, + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + outputCurrency: CONST.CURRENCY.USD, + }; + + const report: Report = { + reportID: 'test-report-id', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CARLOS_ACCOUNT_ID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }; + + const transaction = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + + const transactionViolationsCollection = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + }; + + await Onyx.multiSet({...transactionCollectionDataSet}); + + // Should filter out the smartScanFailedViolation + const filteredViolations = getTransactionViolations(transaction, transactionViolationsCollection, CARLOS_EMAIL, report, policy); expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); }); @@ -643,7 +723,47 @@ describe('getViolations', () => { }; await Onyx.multiSet({...transactionCollectionDataSet}); - const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction, transactionViolationsCollection); + const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction, transactionViolationsCollection, '', undefined, undefined); + expect(hasWarningTypeViolationRes).toBeTruthy(); + }); + + it('checks if transaction has warning type violation after filtering dismissed violations (with report and policy params)', async () => { + const policy: Policy = { + id: 'test-policy-id', + name: 'Test Policy', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + owner: CARLOS_EMAIL, + isPolicyExpenseChatEnabled: false, + autoReporting: true, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, + outputCurrency: CONST.CURRENCY.USD, + pendingAction: undefined, + }; + + const report: Report = { + reportID: 'test-report-id', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CARLOS_ACCOUNT_ID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const transaction = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + + const transactionViolationsCollection = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + }; + + await Onyx.multiSet({...transactionCollectionDataSet}); + const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction, transactionViolationsCollection, CARLOS_EMAIL, report, policy); expect(hasWarningTypeViolationRes).toBeTruthy(); }); }); @@ -776,12 +896,12 @@ describe('hasVisibleViolationsForUser', () => { expect(result).toBe(false); }); - it('should return false when transactions is null', () => { + it('should return false when transactions is empty', () => { const violations = { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${testTransactionID}`]: [missingCategoryViolation], }; - const result = ViolationsUtils.hasVisibleViolationsForUser(mockReport, violations, '', mockPolicy); + const result = ViolationsUtils.hasVisibleViolationsForUser(mockReport, violations, '', mockPolicy, []); expect(result).toBe(false); });