diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 1550fb646f0f5..bdd6a8cef0223 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -942,7 +942,7 @@ function isCardPendingActivate(card?: Card) { * Returns true if the card has fraud type 'domain' or 'individual'. */ function isCardWithPotentialFraud(card: Card): boolean { - return card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL; + return card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL || !!card.nameValuePairs?.possibleFraud; } function isCardPendingReplace(card?: Card) { diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index b37c7f4eddc27..e39a4842d7ed2 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -125,13 +125,13 @@ function TimeSensitiveSection() { {/* Priority 1: Card fraud alerts */} {shouldShowReviewCardFraud && cardsWithFraud.map((card) => { - if (!card.message?.possibleFraud) { + if (!card.nameValuePairs?.possibleFraud) { return null; } return ( ); })} diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 189feb3fc25fa..073e2fb09880e 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -40,12 +40,6 @@ type PossibleFraudData = { fraudAlertReportActionID?: number; }; -/** Model of card message data */ -type CardMessage = { - /** Possible fraud information */ - possibleFraud?: PossibleFraudData; -}; - /** Model of Expensify card */ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Card ID number */ @@ -93,9 +87,6 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Current fraud state of the card */ fraud: ValueOf; - /** Card message data containing possible fraud info and other metadata */ - message?: CardMessage; - /** Card name */ cardName?: string; @@ -201,6 +192,9 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ * null/undefined if card is not frozen */ frozen?: FrozenCardData | null; + + /** Possible fraud information */ + possibleFraud?: PossibleFraudData; }> & OnyxCommon.OnyxValueWithOfflineFeedback< /** Type of export card */ @@ -423,7 +417,6 @@ export type { AssignableCardsList, CardAssignmentData, UnassignedCard, - CardMessage, PossibleFraudData, FrozenCardData, }; diff --git a/tests/unit/hooks/useTimeSensitiveCards.test.ts b/tests/unit/hooks/useTimeSensitiveCards.test.ts index b1ae37e5665e1..96aa6f405bbd0 100644 --- a/tests/unit/hooks/useTimeSensitiveCards.test.ts +++ b/tests/unit/hooks/useTimeSensitiveCards.test.ts @@ -172,7 +172,11 @@ describe('useTimeSensitiveCards', () => { }); it('should identify cards with fraud and set shouldShowReviewCardFraud to true', async () => { - const cardWithFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN}); + const cardWithFraud = 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: 123456}, + }); const cardList: CardList = {'1': cardWithFraud}; await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); @@ -182,10 +186,11 @@ describe('useTimeSensitiveCards', () => { expect(result.current.cardsWithFraud).toHaveLength(1); expect(result.current.cardsWithFraud.at(0)?.cardID).toBe(1); + expect(result.current.cardsWithFraud.at(0)?.nameValuePairs?.possibleFraud?.triggerAmount).toBe(5663); expect(result.current.shouldShowReviewCardFraud).toBe(true); }); - it('should not show fraud review for cards with fraud type NONE', async () => { + 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}; @@ -197,4 +202,22 @@ describe('useTimeSensitiveCards', () => { expect(result.current.cardsWithFraud).toHaveLength(0); expect(result.current.shouldShowReviewCardFraud).toBe(false); }); + + it('should show fraud review when fraud is NONE but possibleFraud data exists in nameValuePairs', async () => { + const cardWithPendingFraudAlert = createRandomExpensifyCard(1, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE, + possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 5230242215684213}, + }); + const cardList: CardList = {'1': cardWithPendingFraudAlert}; + + await Onyx.merge(ONYXKEYS.CARD_LIST, cardList); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useTimeSensitiveCards()); + + expect(result.current.cardsWithFraud).toHaveLength(1); + expect(result.current.cardsWithFraud.at(0)?.cardID).toBe(1); + expect(result.current.shouldShowReviewCardFraud).toBe(true); + }); }); diff --git a/tests/unit/selectors/CardTest.ts b/tests/unit/selectors/CardTest.ts index 3bff645af2b93..b69aca649979e 100644 --- a/tests/unit/selectors/CardTest.ts +++ b/tests/unit/selectors/CardTest.ts @@ -376,7 +376,11 @@ describe('timeSensitiveCardsSelector', () => { }); it('identifies cards with domain fraud', () => { - const cardWithDomainFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN}); + const cardWithDomainFraud = createRandomExpensifyCard(1, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, + possibleFraud: {triggerAmount: 5000, triggerMerchant: 'Test Merchant', currency: 'USD', fraudAlertReportID: 123}, + }); const normalCard = createRandomExpensifyCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE}); const cardList: CardList = { @@ -389,10 +393,15 @@ describe('timeSensitiveCardsSelector', () => { expect(result.cardsWithFraud).toHaveLength(1); expect(result.cardsWithFraud.at(0)?.cardID).toBe(1); expect(result.cardsWithFraud.at(0)?.fraud).toBe(CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); + expect(result.cardsWithFraud.at(0)?.nameValuePairs?.possibleFraud?.triggerAmount).toBe(5000); }); it('identifies cards with individual fraud', () => { - const cardWithIndividualFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL}); + const cardWithIndividualFraud = createRandomExpensifyCard(1, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL, + possibleFraud: {triggerAmount: 3000, triggerMerchant: 'Suspicious Shop', currency: 'USD', fraudAlertReportID: 456}, + }); const normalCard = createRandomExpensifyCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE}); const cardList: CardList = { @@ -405,13 +414,25 @@ describe('timeSensitiveCardsSelector', () => { expect(result.cardsWithFraud).toHaveLength(1); expect(result.cardsWithFraud.at(0)?.cardID).toBe(1); expect(result.cardsWithFraud.at(0)?.fraud).toBe(CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); + expect(result.cardsWithFraud.at(0)?.nameValuePairs?.possibleFraud?.triggerMerchant).toBe('Suspicious Shop'); }); it('detects fraud on both physical and virtual Expensify cards', () => { - const physicalCardWithFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN}); + const physicalCardWithFraud = createRandomExpensifyCard(1, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, + possibleFraud: {triggerAmount: 5000, triggerMerchant: 'Store A', currency: 'USD', fraudAlertReportID: 111}, + }); const virtualCardWithFraud: Card = { - ...createRandomExpensifyCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL}), - nameValuePairs: {isVirtual: true} as Card['nameValuePairs'], + ...createRandomExpensifyCard(2, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL, + possibleFraud: {triggerAmount: 2000, triggerMerchant: 'Store B', currency: 'USD', fraudAlertReportID: 222}, + }), + nameValuePairs: { + isVirtual: true, + possibleFraud: {triggerAmount: 2000, triggerMerchant: 'Store B', currency: 'USD', fraudAlertReportID: 222}, + } as Card['nameValuePairs'], }; const cardList: CardList = { @@ -430,7 +451,11 @@ describe('timeSensitiveCardsSelector', () => { ...createRandomCompanyCard(1, {bank: 'vcf'}), fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, }; - const expensifyCardWithFraud = createRandomExpensifyCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN}); + const expensifyCardWithFraud = createRandomExpensifyCard(2, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, + possibleFraud: {triggerAmount: 7500, triggerMerchant: 'Online Store', currency: 'USD', fraudAlertReportID: 789}, + }); const cardList: CardList = { '1': companyCardWithFraud, @@ -444,7 +469,7 @@ describe('timeSensitiveCardsSelector', () => { expect(result.cardsWithFraud.at(0)?.cardID).toBe(2); }); - it('does not include cards with fraud type NONE', () => { + it('does not include cards with fraud type NONE and no possibleFraud data', () => { const cardWithNoFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE}); const cardList: CardList = { @@ -455,6 +480,26 @@ describe('timeSensitiveCardsSelector', () => { expect(result.cardsWithFraud).toHaveLength(0); }); + + it('includes cards with fraud type NONE when possibleFraud data exists in nameValuePairs', () => { + const cardWithPendingFraudAlert = createRandomExpensifyCard(1, { + state: CONST.EXPENSIFY_CARD.STATE.OPEN, + fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE, + possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 5230242215684213}, + }); + const normalCard = createRandomExpensifyCard(2, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE}); + + const cardList: CardList = { + '1': cardWithPendingFraudAlert, + '2': normalCard, + }; + + const result = timeSensitiveCardsSelector(cardList); + + expect(result.cardsWithFraud).toHaveLength(1); + expect(result.cardsWithFraud.at(0)?.cardID).toBe(1); + expect(result.cardsWithFraud.at(0)?.nameValuePairs?.possibleFraud?.triggerAmount).toBe(5663); + }); }); describe('areAllExpensifyCardsShipped', () => { diff --git a/tests/utils/collections/card.ts b/tests/utils/collections/card.ts index c1aeab2d79881..65106859c14ff 100644 --- a/tests/utils/collections/card.ts +++ b/tests/utils/collections/card.ts @@ -3,6 +3,7 @@ import {format} from 'date-fns'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {Card} from '@src/types/onyx'; +import type {PossibleFraudData} from '@src/types/onyx/Card'; import type {CardFeedWithNumber} from '@src/types/onyx/CardFeeds'; export default function createRandomCard( @@ -14,6 +15,7 @@ export default function createRandomCard( fraud?: ValueOf; accountID?: number; domainName?: string; + possibleFraud?: PossibleFraudData; }, ): Card { const cardID = index > 0 ? index : randNumber(); @@ -62,6 +64,7 @@ export default function createRandomCard( scrapeMinDate: format(randPastDate(), CONST.DATE.FNS_DB_FORMAT_STRING), errors: {}, errorFields: {}, + ...(options?.possibleFraud ? {nameValuePairs: {possibleFraud: options.possibleFraud} as Card['nameValuePairs']} : {}), }; } @@ -76,6 +79,7 @@ function createRandomExpensifyCard( fraud?: ValueOf; accountID?: number; domainName?: string; + possibleFraud?: PossibleFraudData; }, ): Card { return createRandomCard(index, {