Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/TimeSensitiveSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ReviewCardFraud
key={card.cardID}
possibleFraud={card.message.possibleFraud}
possibleFraud={card.nameValuePairs.possibleFraud}
/>
);
})}
Expand Down
13 changes: 3 additions & 10 deletions src/types/onyx/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -93,9 +87,6 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Current fraud state of the card */
fraud: ValueOf<typeof CONST.EXPENSIFY_CARD.FRAUD_TYPES>;

/** Card message data containing possible fraud info and other metadata */
message?: CardMessage;

/** Card name */
cardName?: string;

Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -423,7 +417,6 @@ export type {
AssignableCardsList,
CardAssignmentData,
UnassignedCard,
CardMessage,
PossibleFraudData,
FrozenCardData,
};
27 changes: 25 additions & 2 deletions tests/unit/hooks/useTimeSensitiveCards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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};

Expand All @@ -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);
});
});
59 changes: 52 additions & 7 deletions tests/unit/selectors/CardTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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,
Expand All @@ -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 = {
Expand All @@ -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', () => {
Expand Down
4 changes: 4 additions & 0 deletions tests/utils/collections/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -14,6 +15,7 @@ export default function createRandomCard(
fraud?: ValueOf<typeof CONST.EXPENSIFY_CARD.FRAUD_TYPES>;
accountID?: number;
domainName?: string;
possibleFraud?: PossibleFraudData;
},
): Card {
const cardID = index > 0 ? index : randNumber();
Expand Down Expand Up @@ -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']} : {}),
};
}

Expand All @@ -76,6 +79,7 @@ function createRandomExpensifyCard(
fraud?: ValueOf<typeof CONST.EXPENSIFY_CARD.FRAUD_TYPES>;
accountID?: number;
domainName?: string;
possibleFraud?: PossibleFraudData;
},
): Card {
return createRandomCard(index, {
Expand Down
Loading