diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 940cb72e29ae..805db75461ef 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4166,9 +4166,9 @@ type ReasonAndReportActionThatRequiresAttention = { /** * Returns the unresolved card fraud alert action for a given report. */ -function getUnresolvedCardFraudAlertAction(reportID: string): OnyxEntry { - const reportActions = getAllReportActions(reportID); - return Object.values(reportActions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution); +function getUnresolvedCardFraudAlertAction(reportID: string, reportActions?: OnyxEntry): OnyxEntry { + const actions = reportActions ?? getAllReportActions(reportID); + return Object.values(actions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution); } /** @@ -13647,6 +13647,7 @@ export { hasMissingInvoiceBankAccount, reasonForReportToBeInOptionList, getReasonAndReportActionThatRequiresAttention, + getUnresolvedCardFraudAlertAction, buildOptimisticChangeFieldAction, isPolicyRelatedReport, hasReportErrorsOtherThanFailedReceipt, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index aa3384467e67..b4fbe2f20b12 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -38,7 +38,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SpendRuleForm} from '@src/types/form'; import type {Card, CompanyCardFeedWithDomainID, PersonalDetailsList, Report, Transaction} from '@src/types/onyx'; -import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card'; +import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep, PossibleFraudData} from '@src/types/onyx/Card'; import type {ExpensifyCardRule} from '@src/types/onyx/ExpensifyCardSettings'; import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -1749,7 +1749,13 @@ function deleteExpensifyCardRule(domainAccountID: number, cardRuleID: string, ex * Resolves a fraud alert for a given card. * When the user clicks on the whisper it sets the optimistic data to the resolution and calls the API */ -function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportID: string | undefined, reportActionID: string | undefined) { +function resolveFraudAlert( + cardID: number | undefined, + isFraud: boolean, + reportID: string | undefined, + reportActionID: string | undefined, + previousPossibleFraud: PossibleFraudData | null = null, +) { if (!reportID || !reportActionID || !cardID) { Log.hmmm('[resolveFraudAlert] Missing required parameters'); return; @@ -1757,7 +1763,7 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI const resolution = isFraud ? CONST.CARD_FRAUD_ALERT_RESOLUTION.FRAUD : CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED; - const optimisticData: Array> = [ + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -1770,6 +1776,17 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + nameValuePairs: { + possibleFraud: null, + }, + }, + }, + }, ]; const successData: Array> = [ @@ -1784,7 +1801,7 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI }, ]; - const failureData: Array> = [ + const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -1798,6 +1815,17 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + nameValuePairs: { + possibleFraud: previousPossibleFraud, + }, + }, + }, + }, ]; const parameters: ResolveFraudAlertParams = { diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts index 3ebad36d4772..2ccafba79bba 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -1,10 +1,12 @@ import useOnyx from '@hooks/useOnyx'; import {isCard, isCardPendingActivate, isCardPendingIssue, isCardWithCustomZeroLimit, isCardWithPotentialFraud, isExpensifyCard} from '@libs/CardUtils'; +import {getUnresolvedCardFraudAlertAction} from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card} from '@src/types/onyx'; function useTimeSensitiveCards() { const [cards] = useOnyx(ONYXKEYS.CARD_LIST); + const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const cardsNeedingShippingAddress: Card[] = []; const cardsNeedingActivation: Card[] = []; @@ -19,7 +21,11 @@ function useTimeSensitiveCards() { continue; } - if (isCardWithPotentialFraud(card) && card.nameValuePairs?.possibleFraud?.fraudAlertReportID) { + const fraudAlertReportID = card.nameValuePairs?.possibleFraud?.fraudAlertReportID; + const reportActions = fraudAlertReportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fraudAlertReportID}`] : undefined; + const hasUnresolvedFraudAction = !!fraudAlertReportID && !!getUnresolvedCardFraudAlertAction(String(fraudAlertReportID), reportActions); + + if (isCardWithPotentialFraud(card) && !!fraudAlertReportID && hasUnresolvedFraudAction) { cardsWithFraud.push(card); } diff --git a/src/pages/inbox/report/actionContents/FraudAlertContent.tsx b/src/pages/inbox/report/actionContents/FraudAlertContent.tsx index 9ef90456c3ef..b8cd6005b2f2 100644 --- a/src/pages/inbox/report/actionContents/FraudAlertContent.tsx +++ b/src/pages/inbox/report/actionContents/FraudAlertContent.tsx @@ -1,12 +1,15 @@ +import {cardByIdSelector} from '@selectors/Card'; import React from 'react'; import {View} from 'react-native'; import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import {getActionableCardFraudAlertMessage, getOriginalMessage} from '@libs/ReportActionsUtils'; import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage'; import {resolveFraudAlert} from '@userActions/Card'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; type FraudAlertContentProps = { @@ -20,6 +23,8 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) { const reportActionID = action?.reportActionID; const originalMessage = getOriginalMessage(action); const cardID = originalMessage?.cardID; + const [card] = useOnyx(ONYXKEYS.CARD_LIST, {selector: cardByIdSelector(String(cardID))}); + const possibleFraud = card?.nameValuePairs?.possibleFraud ?? null; const buttons: ActionableItem[] = originalMessage?.resolution ? [] @@ -28,7 +33,7 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) { text: 'cardPage.cardFraudAlert.confirmButtonText', key: `${action.reportActionID}-cardFraudAlert-confirm`, onPress: () => { - resolveFraudAlert(cardID, false, reportID, reportActionID); + resolveFraudAlert(cardID, false, reportID, reportActionID, possibleFraud); }, isPrimary: true, }, @@ -36,7 +41,7 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) { text: 'cardPage.cardFraudAlert.reportFraudButtonText', key: `${action.reportActionID}-cardFraudAlert-reportFraud`, onPress: () => { - resolveFraudAlert(cardID, true, reportID, reportActionID); + resolveFraudAlert(cardID, true, reportID, reportActionID, possibleFraud); }, }, ]; diff --git a/tests/unit/hooks/useTimeSensitiveCards.test.ts b/tests/unit/hooks/useTimeSensitiveCards.test.ts index b6302bc3eb77..475bc3765061 100644 --- a/tests/unit/hooks/useTimeSensitiveCards.test.ts +++ b/tests/unit/hooks/useTimeSensitiveCards.test.ts @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card, CardList} from '@src/types/onyx'; import {createRandomExpensifyCard} from '../../utils/collections/card'; +import createRandomReportAction from '../../utils/collections/reportActions'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; describe('useTimeSensitiveCards', () => { @@ -178,8 +179,15 @@ describe('useTimeSensitiveCards', () => { possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 123456}, }); const cardList: CardList = {'1': cardWithFraud}; + const unresolvedFraudAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT, + }; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithFraud.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, { + [unresolvedFraudAction.reportActionID]: unresolvedFraudAction, + }); await waitForBatchedUpdates(); const {result} = renderHook(() => useTimeSensitiveCards()); @@ -190,6 +198,34 @@ describe('useTimeSensitiveCards', () => { expect(result.current.shouldShowReviewCardFraud).toBe(true); }); + it('should not show fraud review when card has possibleFraud data but the fraud action is already resolved as recognized', async () => { + const cardWithResolvedFraudAlert = createRandomExpensifyCard(1, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, + possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 123457}, + }); + const cardList: CardList = {'1': cardWithResolvedFraudAlert}; + const baseFraudAction = createRandomReportAction(4); + const resolvedFraudAction = { + ...baseFraudAction, + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT, + originalMessage: { + resolution: CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED, + }, + }; + + await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithResolvedFraudAlert.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, { + [resolvedFraudAction.reportActionID]: resolvedFraudAction, + }); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveCards()); + + expect(result.current.cardsWithFraud).toHaveLength(0); + expect(result.current.shouldShowReviewCardFraud).toBe(false); + }); + it('should not show fraud review for cards with fraud type NONE and no possibleFraud data', async () => { const cardWithNoFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE}); const cardList: CardList = {'1': cardWithNoFraud}; @@ -210,8 +246,15 @@ describe('useTimeSensitiveCards', () => { possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 5230242215684213}, }); const cardList: CardList = {'1': cardWithPendingFraudAlert}; + const unresolvedFraudAction = { + ...createRandomReportAction(2), + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT, + }; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithPendingFraudAlert.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, { + [unresolvedFraudAction.reportActionID]: unresolvedFraudAction, + }); await waitForBatchedUpdates(); const {result} = renderHook(() => useTimeSensitiveCards()); @@ -283,8 +326,15 @@ describe('useTimeSensitiveCards', () => { } as Card['nameValuePairs'], }; const cardList: CardList = {'1': zeroLimitFraudCard}; + const unresolvedFraudAction = { + ...createRandomReportAction(3), + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT, + }; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${zeroLimitFraudCard.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, { + [unresolvedFraudAction.reportActionID]: unresolvedFraudAction, + }); await waitForBatchedUpdates(); const {result} = renderHook(() => useTimeSensitiveCards());