diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 078cbed25631..ed5ecf41078a 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -1,13 +1,12 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; import {isCurrentUserSubmitter, isProcessingReport, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; import TextLink from './TextLink'; @@ -15,7 +14,6 @@ import TextLink from './TextLink'; type BrokenConnectionDescriptionProps = { /** Transaction id of the corresponding report */ transactionID: string | undefined; - /** Current report */ report: OnyxEntry; @@ -26,7 +24,7 @@ type BrokenConnectionDescriptionProps = { function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`); + const transactionViolations = getTransactionViolations(transactionID); const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); @@ -46,7 +44,12 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn {`${translate('violations.adminBrokenConnectionError')}`} Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id))} + onPress={() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id)); + }} >{`${translate('workspace.common.companyCards')}`} . diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 268ab770059e..0d0e862f36f5 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {markAsCash as markAsCashUtil} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyAdmin} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; @@ -25,7 +26,6 @@ import { shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, } from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -61,13 +61,12 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? CONST.DEFAULT_NUMBER_ID}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID : CONST.DEFAULT_NUMBER_ID }`, ); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); @@ -88,7 +87,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); const markAsCash = useCallback(() => { - markAsCashAction(transaction?.transactionID, reportID); + markAsCashUtil(transaction?.transactionID, reportID); }, [reportID, transaction?.transactionID]); const isScanning = hasReceipt(transaction) && isReceiptBeingScanned(transaction); @@ -122,7 +121,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre ), }; } - if (hasPendingRTERViolation(getTransactionViolations(transaction?.transactionID, transactionViolations))) { + if (hasPendingRTERViolation(getTransactionViolations(transaction?.transactionID))) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } if (isScanning) { diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index e20fe09058e1..c70bda657e67 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -112,7 +112,8 @@ function MoneyRequestPreviewContent({ const transactionID = isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : undefined; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [allViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const transactionViolations = getTransactionViolations(transaction?.transactionID); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID; @@ -145,9 +146,9 @@ function MoneyRequestPreviewContent({ const isOnHold = isOnHoldTransactionUtils(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = hasViolationTransactionUtils(transaction?.transactionID, transactionViolations, true); - const hasNoticeTypeViolations = hasNoticeTypeViolationTransactionUtils(transaction?.transactionID, transactionViolations, true) && isPaidGroupPolicy(iouReport); - const hasWarningTypeViolations = hasWarningTypeViolationTransactionUtils(transaction?.transactionID, transactionViolations, true); + const hasViolations = hasViolationTransactionUtils(transaction?.transactionID, allViolations, true); + const hasNoticeTypeViolations = hasNoticeTypeViolationTransactionUtils(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = hasWarningTypeViolationTransactionUtils(transaction?.transactionID, allViolations, true); const hasFieldErrors = hasMissingSmartscanFields(transaction); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const isPerDiemRequest = isPerDiemRequestTransactionUtils(transaction); @@ -163,11 +164,8 @@ function MoneyRequestPreviewContent({ // Get transaction violations for given transaction id from onyx, find duplicated transactions violations and get duplicates const allDuplicates = useMemo( - () => - transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`]?.find( - (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - )?.data?.duplicates ?? [], - [transaction?.transactionID, transactionViolations], + () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], + [transactionViolations], ); // Remove settled transactions from duplicates @@ -240,14 +238,13 @@ function MoneyRequestPreviewContent({ } if (shouldShowRBR && transaction) { - const violations = getTransactionViolations(transaction.transactionID, transactionViolations); if (shouldShowHoldMessage) { return `${message} ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; } - const firstViolation = violations?.at(0); + const firstViolation = transactionViolations?.at(0); if (firstViolation) { const violationMessage = ViolationsUtils.getViolationTranslation(firstViolation, translate); - const violationsCount = violations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; + const violationsCount = transactionViolations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; const isTooLong = violationsCount > 1 || violationMessage.length > 15; const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors; @@ -287,7 +284,7 @@ function MoneyRequestPreviewContent({ if (shouldShowBrokenConnectionViolation(transaction ? [transaction.transactionID] : [], iouReport, policy)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')}; } - if (hasPendingUI(transaction, getTransactionViolations(transaction?.transactionID, transactionViolations))) { + if (hasPendingUI(transaction, transactionViolations)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 10c72dbd8841..54f3a8b6907f 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -49,6 +49,7 @@ import { getDistanceInMeters, getTagForDisplay, getTaxName, + getTransactionViolations, hasMissingSmartscanFields, hasReceipt as hasReceiptTransactionUtils, hasReservationList, @@ -133,7 +134,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); + const transactionViolations = getTransactionViolations(linkedTransactionID); const { created: transactionDate, @@ -698,7 +699,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals {shouldShowTax && ( ({...getThumbnailAndImageURIs(transaction), transaction})); const transactionIDList = transactions?.map((reportTransaction) => reportTransaction.transactionID) ?? []; - const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID, transactionViolations)); + const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID)); const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(transactionIDList, iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null; const formattedDescription = numberOfRequests === 1 ? getDescription(lastTransaction) : null; @@ -250,7 +250,7 @@ function ReportPreview({ const isArchived = isArchivedReportWithID(iouReport?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const filteredTransactions = transactions?.filter((transaction) => transaction) ?? []; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, transactionViolations); + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions); const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e6f9810ab336..b959479d8e63 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1614,9 +1614,9 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry { - if (!reportID) { - return; +function getIOUActionForReportID(reportID: string | undefined, transactionID: string | undefined): OnyxEntry { + if (!reportID || !transactionID) { + return undefined; } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const reportActions = getAllReportActions(report?.reportID); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index f47a0d56001d..51f8e9428bdb 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -340,7 +340,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr } // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead - if (canSubmitReport(report, policy, allReportTransactions, allViolations) && isAllowedToApproveExpenseReport) { + if (canSubmitReport(report, policy, allReportTransactions) && isAllowedToApproveExpenseReport) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index a8ffc240aa6f..474329bd6951 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -766,8 +766,11 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry): /** * Get all transaction violations of the transaction with given tranactionID. */ -function getTransactionViolations(transactionID: string | undefined, transactionViolations: OnyxCollection | null): TransactionViolations | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; +function getTransactionViolations(transactionID: string | undefined): TransactionViolations | null { + if (!transactionID) { + return null; + } + return allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((violation) => !isViolationDismissed(transactionID, violation)) ?? null; } /** @@ -786,8 +789,8 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if there is broken connection violation. */ -function hasBrokenConnectionViolation(transactionID?: string, allViolations?: OnyxCollection): boolean { - const violations = getTransactionViolations(transactionID, allViolations ?? allTransactionViolations); +function hasBrokenConnectionViolation(transactionID?: string): boolean { + const violations = getTransactionViolations(transactionID); return !!violations?.find( (violation) => violation.name === CONST.VIOLATIONS.RTER && @@ -798,13 +801,8 @@ function hasBrokenConnectionViolation(transactionID?: string, allViolations?: On /** * Check if user should see broken connection violation warning. */ -function shouldShowBrokenConnectionViolation( - transactionIDList: string[] | undefined, - report: OnyxEntry | SearchReport, - policy: OnyxEntry | SearchPolicy, - allViolations?: OnyxCollection, -): boolean { - const transactionsWithBrokenConnectionViolation = transactionIDList?.map((transactionID) => hasBrokenConnectionViolation(transactionID, allViolations)) ?? []; +function shouldShowBrokenConnectionViolation(transactionIDList: string[] | undefined, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean { + const transactionsWithBrokenConnectionViolation = transactionIDList?.map((transactionID) => hasBrokenConnectionViolation(transactionID)) ?? []; return ( transactionsWithBrokenConnectionViolation.length > 0 && transactionsWithBrokenConnectionViolation?.some((value) => value === true) && @@ -815,9 +813,9 @@ function shouldShowBrokenConnectionViolation( /** * Check if there is pending rter violation in all transactionViolations with given transactionIDs. */ -function allHavePendingRTERViolation(transactionIds: string[], allViolations?: OnyxCollection): boolean { +function allHavePendingRTERViolation(transactionIds: string[]): boolean { const transactionsWithRTERViolations = transactionIds.map((transactionId) => { - const transactionViolations = getTransactionViolations(transactionId, allViolations ?? allTransactionViolations); + const transactionViolations = getTransactionViolations(transactionId); return hasPendingRTERViolation(transactionViolations); }); return transactionsWithRTERViolations.length > 0 && transactionsWithRTERViolations.every((value) => value === true); @@ -908,14 +906,20 @@ function getRecentTransactions(transactions: Record, size = 2): * @param checkDismissed - whether to check if the violation has already been dismissed as well */ function isDuplicate(transactionID: string | undefined, checkDismissed = false): boolean { - const hasDuplicatedViolation = !!allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]?.some( + if (!transactionID) { + return false; + } + const duplicateViolation = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]?.find( (violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, ); + + const hasDuplicatedViolation = !!duplicateViolation; if (!checkDismissed) { - return hasDuplicatedViolation; + return !!duplicateViolation; } - const didDismissedViolation = - allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]?.comment?.dismissedViolations?.duplicatedTransaction?.[currentUserEmail] === `${currentUserAccountID}`; + + const didDismissedViolation = isViolationDismissed(transactionID, duplicateViolation); + return hasDuplicatedViolation && !didDismissedViolation; } @@ -941,16 +945,32 @@ function isOnHoldByTransactionID(transactionID: string | undefined | null): bool return isOnHold(allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]); } +/** + * Checks if a violation is dismissed for the given transaction + */ +function isViolationDismissed(transactionID: string | undefined, violation: TransactionViolation | undefined): boolean { + if (!transactionID || !violation) { + return false; + } + return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]?.comment?.dismissedViolations?.[violation.name]?.[currentUserEmail] === `${currentUserAccountID}`; +} + /** * Checks if any violations for the provided transaction are of type 'violation' */ function hasViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { + if (!transactionID) { + return false; + } const transaction = getTransaction(transactionID); if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { return false; } return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( - (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && (showInReview === undefined || showInReview === (violation.showInReview ?? false)), + (violation: TransactionViolation) => + violation.type === CONST.VIOLATION_TYPES.VIOLATION && + (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && + !isViolationDismissed(transactionID, violation), ); } @@ -958,12 +978,18 @@ function hasViolation(transactionID: string | undefined, transactionViolations: * Checks if any violations for the provided transaction are of type 'notice' */ function hasNoticeTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { + if (!transactionID) { + return false; + } const transaction = getTransaction(transactionID); if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { return false; } return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( - (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && (showInReview === undefined || showInReview === (violation.showInReview ?? false)), + (violation: TransactionViolation) => + violation.type === CONST.VIOLATION_TYPES.NOTICE && + (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && + !isViolationDismissed(transactionID, violation), ); } @@ -971,6 +997,9 @@ function hasNoticeTypeViolation(transactionID: string | undefined, transactionVi * Checks if any violations for the provided transaction are of type 'warning' */ function hasWarningTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { + if (!transactionID) { + return false; + } const transaction = getTransaction(transactionID); if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { return false; @@ -978,7 +1007,10 @@ function hasWarningTypeViolation(transactionID: string | undefined, transactionV const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]; const warningTypeViolations = violations?.filter( - (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.WARNING && (showInReview === undefined || showInReview === (violation.showInReview ?? false)), + (violation: TransactionViolation) => + violation.type === CONST.VIOLATION_TYPES.WARNING && + (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && + !isViolationDismissed(transactionID, violation), ) ?? []; const hasOnlyDupeDetectionViolation = warningTypeViolations?.every((violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); @@ -1451,6 +1483,7 @@ export { getFormattedPostedDate, getCategoryTaxCodeAndAmount, isPerDiemRequest, + isViolationDismissed, }; export type {TransactionChanges}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4723009822df..6425e0eb46b1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3854,7 +3854,7 @@ function updateMoneyRequestAttendees( policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, - violations: OnyxEntry, + violations?: OnyxEntry, ) { const transactionChanges: TransactionChanges = { attendees, @@ -8011,15 +8011,14 @@ function canSubmitReport( report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, transactions: OnyxTypes.Transaction[] | SearchTransaction[], - allViolations?: OnyxCollection, ) { const currentUserAccountID = getCurrentUserAccountID(); const isOpenExpenseReport = isOpenExpenseReportReportUtils(report); const isArchived = isArchivedReportWithID(report?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const transactionIDList = transactions.map((transaction) => transaction.transactionID); - const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList, allViolations); - const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolation(transactionIDList, report, policy, allViolations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList); + const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolation(transactionIDList, report, policy); const hasOnlyPendingCardOrScanFailTransactions = transactions.length > 0 && diff --git a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx index e13fd01fdcd7..5190d6c76e50 100644 --- a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx +++ b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import ScrollView from '@components/ScrollView'; @@ -7,7 +6,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import ROUTES from '@src/ROUTES'; import type {TransactionViolation} from '@src/types/onyx'; @@ -17,7 +16,8 @@ type DebugTransactionViolationsProps = { }; function DebugTransactionViolations({transactionID}: DebugTransactionViolationsProps) { - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = getTransactionViolations(transactionID); + const styles = useThemeStyles(); const {translate} = useLocalize(); diff --git a/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx b/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx index b5f3d0d603d5..452925da8b86 100644 --- a/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx +++ b/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -10,10 +9,11 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DebugUtils from '@libs/DebugUtils'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import Debug from '@userActions/Debug'; import CONST from '@src/CONST'; @@ -62,7 +62,7 @@ function DebugTransactionViolationCreatePage({ }: DebugTransactionViolationCreatePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = getTransactionViolations(transactionID); const [draftTransactionViolation, setDraftTransactionViolation] = useState(() => getInitialTransactionViolation()); const [error, setError] = useState(); @@ -95,7 +95,7 @@ function DebugTransactionViolationCreatePage({ {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx b/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx index 9db84c341d59..1b26b0c5f72a 100644 --- a/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx +++ b/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx @@ -1,18 +1,18 @@ import React, {useCallback, useMemo} from 'react'; import {InteractionManager, View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Debug from '@libs/actions/Debug'; import DebugUtils from '@libs/DebugUtils'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import type {DebugTabNavigatorRoutes} from '@libs/Navigation/DebugTabNavigator'; import DebugTabNavigator from '@libs/Navigation/DebugTabNavigator'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -29,7 +29,7 @@ function DebugTransactionViolationPage({ }, }: DebugTransactionViolationPageProps) { const {translate} = useLocalize(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = getTransactionViolations(transactionID); const transactionViolation = useMemo(() => transactionViolations?.[Number(index)], [index, transactionViolations]); const styles = useThemeStyles(); @@ -84,7 +84,7 @@ function DebugTransactionViolationPage({ {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index cb27ecfcbb3c..883b7e2d8031 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -10,13 +10,13 @@ import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {dismissDuplicateTransactionViolation} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as Transaction from '@userActions/Transaction'; +import {getLinkedTransactionID, getReportAction} from '@libs/ReportActionsUtils'; +import {isReportApproved, isSettled} from '@libs/ReportUtils'; +import {getTransaction, getTransactionViolations} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -28,23 +28,24 @@ function TransactionDuplicateReview() { const route = useRoute>(); const currentPersonalDetails = useCurrentUserPersonalDetails(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`); - const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); - const transactionID = ReportActionsUtils.getLinkedTransactionID(reportAction, report?.reportID ?? '-1') ?? '-1'; - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const reportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + const transactionID = getLinkedTransactionID(reportAction, report?.reportID) ?? undefined; + const transactionViolations = getTransactionViolations(transactionID); + const duplicateTransactionIDs = useMemo( () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], [transactionViolations], ); - const transactionIDs = [transactionID, ...duplicateTransactionIDs]; + const transactionIDs = transactionID ? [transactionID, ...duplicateTransactionIDs] : [...duplicateTransactionIDs]; - const transactions = transactionIDs.map((item) => TransactionUtils.getTransaction(item)).sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); + const transactions = transactionIDs.map((item) => getTransaction(item)).sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); const keepAll = () => { - Transaction.dismissDuplicateTransactionViolation(transactionIDs, currentPersonalDetails); + dismissDuplicateTransactionViolation(transactionIDs, currentPersonalDetails); Navigation.goBack(); }; - const hasSettledOrApprovedTransaction = transactions.some((transaction) => ReportUtils.isSettled(transaction?.reportID) || ReportUtils.isReportApproved(transaction?.reportID)); + const hasSettledOrApprovedTransaction = transactions.some((transaction) => isSettled(transaction?.reportID) || isReportApproved(transaction?.reportID)); return ( diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx index 409ef1cfe02d..2e64e55fb8a1 100644 --- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx +++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx @@ -4,10 +4,10 @@ import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import {setMoneyRequestAttendees, updateMoneyRequestAttendees} from '@libs/actions/IOU'; import Navigation from '@libs/Navigation/Navigation'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getAttendees, getTransactionViolations} from '@libs/TransactionUtils'; import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttendeeSelector'; -import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -39,20 +39,20 @@ function IOURequestStepAttendees({ policyCategories, }: IOURequestStepAttendeesProps) { const isEditing = action === CONST.IOU.ACTION.EDIT; - const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || -1}`); - const [attendees, setAttendees] = useState(() => TransactionUtils.getAttendees(transaction)); + const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || CONST.DEFAULT_NUMBER_ID}`); + const [attendees, setAttendees] = useState(() => getAttendees(transaction)); const previousAttendees = usePrevious(attendees); const {translate} = useLocalize(); - const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const violations = getTransactionViolations(transactionID); const saveAttendees = useCallback(() => { if (attendees.length <= 0) { return; } if (!lodashIsEqual(previousAttendees, attendees)) { - IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing); + setMoneyRequestAttendees(transactionID, attendees, !isEditing); if (isEditing) { - IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations); + updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations ?? undefined); } } diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 968b9a4dea4b..a573bce74e27 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -82,7 +82,7 @@ type Comment = { splits?: Split[]; /** Violations that were dismissed */ - dismissedViolations?: Record>; + dismissedViolations?: Partial>>; }; /** Model of transaction custom unit */ diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 659240cc0c30..b161ea8fe40c 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -1,9 +1,12 @@ import {beforeEach} from '@jest/globals'; import Onyx from 'react-native-onyx'; +import {getTransactionViolations, hasWarningTypeViolation, isViolationDismissed} from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; +import type {TransactionViolationsCollectionDataSet} from '@src/types/onyx/TransactionViolation'; const categoryOutOfPolicyViolation = { name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, @@ -30,6 +33,16 @@ const tagOutOfPolicyViolation = { type: CONST.VIOLATION_TYPES.VIOLATION, }; +const smartScanFailedViolation = { + name: CONST.VIOLATIONS.SMARTSCAN_FAILED, + type: CONST.VIOLATION_TYPES.WARNING, +}; + +const duplicatedTransactionViolation = { + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + type: CONST.VIOLATION_TYPES.WARNING, +}; + describe('getViolationsOnyxData', () => { let transaction: Transaction; let transactionViolations: TransactionViolation[]; @@ -349,3 +362,91 @@ describe('getViolationsOnyxData', () => { }); }); }); + +const getFakeTransaction = (transactionID: string, comment?: Transaction['comment']) => ({ + transactionID, + attendees: [{email: 'text@expensify.com'}], + reportID: '1234', + amount: 100, + comment: comment ?? {}, + created: '2023-07-24 13:46:20', + merchant: 'United Airlines', + currency: 'USD', +}); + +const CARLOS_EMAIL = 'cmartins@expensifail.com'; +const CARLOS_ACCOUNT_ID = 1; + +describe('getViolations', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: { + email: CARLOS_EMAIL, + accountID: CARLOS_ACCOUNT_ID, + }, + }, + }); + }); + + afterEach(() => Onyx.clear()); + + it('should check if violation is dismissed or not', async () => { + 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.transactionID, smartScanFailedViolation); + const isDuplicateViolationDismissed = isViolationDismissed(transaction.transactionID, duplicatedTransactionViolation); + + expect(isSmartScanDismissed).toBeTruthy(); + expect(isDuplicateViolationDismissed).toBeFalsy(); + }); + + it('should return filtered out dismissed violations', async () => { + const transaction = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, + }; + + const transactionViolationsCollection: TransactionViolationsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + }; + + await Onyx.multiSet({...transactionCollectionDataSet, ...transactionViolationsCollection}); + + // Should filter out the smartScanFailedViolation + const filteredViolations = getTransactionViolations(transaction.transactionID); + expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); + }); + + it('checks if transaction has warning type violation after filtering dismissed violations', async () => { + 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 and return true, duplicatedTransactionViolation is a warning type violation but it's not considered in hasWarningTypeViolation + const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction.transactionID, transactionViolationsCollection); + expect(hasWarningTypeViolationRes).toBeFalsy(); + }); +});