Skip to content

Commit 4e710ae

Browse files
authored
Merge pull request Expensify#87717 from Uzaifm127/fix/85981
fix: Stale "Review fraudulent charge" task appears on home screen but the fraud flag was already cleared
2 parents b1b6e6c + c742de6 commit 4e710ae

5 files changed

Lines changed: 100 additions & 10 deletions

File tree

src/libs/ReportUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4166,9 +4166,9 @@ type ReasonAndReportActionThatRequiresAttention = {
41664166
/**
41674167
* Returns the unresolved card fraud alert action for a given report.
41684168
*/
4169-
function getUnresolvedCardFraudAlertAction(reportID: string): OnyxEntry<ReportAction> {
4170-
const reportActions = getAllReportActions(reportID);
4171-
return Object.values(reportActions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution);
4169+
function getUnresolvedCardFraudAlertAction(reportID: string, reportActions?: OnyxEntry<ReportActions>): OnyxEntry<ReportAction> {
4170+
const actions = reportActions ?? getAllReportActions(reportID);
4171+
return Object.values(actions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution);
41724172
}
41734173

41744174
/**
@@ -13647,6 +13647,7 @@ export {
1364713647
hasMissingInvoiceBankAccount,
1364813648
reasonForReportToBeInOptionList,
1364913649
getReasonAndReportActionThatRequiresAttention,
13650+
getUnresolvedCardFraudAlertAction,
1365013651
buildOptimisticChangeFieldAction,
1365113652
isPolicyRelatedReport,
1365213653
hasReportErrorsOtherThanFailedReceipt,

src/libs/actions/Card.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import CONST from '@src/CONST';
3838
import ONYXKEYS from '@src/ONYXKEYS';
3939
import type {SpendRuleForm} from '@src/types/form';
4040
import type {Card, CompanyCardFeedWithDomainID, PersonalDetailsList, Report, Transaction} from '@src/types/onyx';
41-
import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card';
41+
import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep, PossibleFraudData} from '@src/types/onyx/Card';
4242
import type {ExpensifyCardRule} from '@src/types/onyx/ExpensifyCardSettings';
4343
import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
4444
import type {ConnectionName} from '@src/types/onyx/Policy';
@@ -1749,15 +1749,21 @@ function deleteExpensifyCardRule(domainAccountID: number, cardRuleID: string, ex
17491749
* Resolves a fraud alert for a given card.
17501750
* When the user clicks on the whisper it sets the optimistic data to the resolution and calls the API
17511751
*/
1752-
function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportID: string | undefined, reportActionID: string | undefined) {
1752+
function resolveFraudAlert(
1753+
cardID: number | undefined,
1754+
isFraud: boolean,
1755+
reportID: string | undefined,
1756+
reportActionID: string | undefined,
1757+
previousPossibleFraud: PossibleFraudData | null = null,
1758+
) {
17531759
if (!reportID || !reportActionID || !cardID) {
17541760
Log.hmmm('[resolveFraudAlert] Missing required parameters');
17551761
return;
17561762
}
17571763

17581764
const resolution = isFraud ? CONST.CARD_FRAUD_ALERT_RESOLUTION.FRAUD : CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED;
17591765

1760-
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
1766+
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.CARD_LIST>> = [
17611767
{
17621768
onyxMethod: Onyx.METHOD.MERGE,
17631769
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
@@ -1770,6 +1776,17 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI
17701776
},
17711777
},
17721778
},
1779+
{
1780+
onyxMethod: Onyx.METHOD.MERGE,
1781+
key: ONYXKEYS.CARD_LIST,
1782+
value: {
1783+
[cardID]: {
1784+
nameValuePairs: {
1785+
possibleFraud: null,
1786+
},
1787+
},
1788+
},
1789+
},
17731790
];
17741791

17751792
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
@@ -1784,7 +1801,7 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI
17841801
},
17851802
];
17861803

1787-
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
1804+
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.CARD_LIST>> = [
17881805
{
17891806
onyxMethod: Onyx.METHOD.MERGE,
17901807
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
@@ -1798,6 +1815,17 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI
17981815
},
17991816
},
18001817
},
1818+
{
1819+
onyxMethod: Onyx.METHOD.MERGE,
1820+
key: ONYXKEYS.CARD_LIST,
1821+
value: {
1822+
[cardID]: {
1823+
nameValuePairs: {
1824+
possibleFraud: previousPossibleFraud,
1825+
},
1826+
},
1827+
},
1828+
},
18011829
];
18021830

18031831
const parameters: ResolveFraudAlertParams = {

src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import useOnyx from '@hooks/useOnyx';
22
import {isCard, isCardPendingActivate, isCardPendingIssue, isCardWithCustomZeroLimit, isCardWithPotentialFraud, isExpensifyCard} from '@libs/CardUtils';
3+
import {getUnresolvedCardFraudAlertAction} from '@libs/ReportUtils';
34
import ONYXKEYS from '@src/ONYXKEYS';
45
import type {Card} from '@src/types/onyx';
56

67
function useTimeSensitiveCards() {
78
const [cards] = useOnyx(ONYXKEYS.CARD_LIST);
9+
const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
810

911
const cardsNeedingShippingAddress: Card[] = [];
1012
const cardsNeedingActivation: Card[] = [];
@@ -19,7 +21,11 @@ function useTimeSensitiveCards() {
1921
continue;
2022
}
2123

22-
if (isCardWithPotentialFraud(card) && card.nameValuePairs?.possibleFraud?.fraudAlertReportID) {
24+
const fraudAlertReportID = card.nameValuePairs?.possibleFraud?.fraudAlertReportID;
25+
const reportActions = fraudAlertReportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fraudAlertReportID}`] : undefined;
26+
const hasUnresolvedFraudAction = !!fraudAlertReportID && !!getUnresolvedCardFraudAlertAction(String(fraudAlertReportID), reportActions);
27+
28+
if (isCardWithPotentialFraud(card) && !!fraudAlertReportID && hasUnresolvedFraudAction) {
2329
cardsWithFraud.push(card);
2430
}
2531

src/pages/inbox/report/actionContents/FraudAlertContent.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import {cardByIdSelector} from '@selectors/Card';
12
import React from 'react';
23
import {View} from 'react-native';
34
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
45
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
56
import useLocalize from '@hooks/useLocalize';
7+
import useOnyx from '@hooks/useOnyx';
68
import {getActionableCardFraudAlertMessage, getOriginalMessage} from '@libs/ReportActionsUtils';
79
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
810
import {resolveFraudAlert} from '@userActions/Card';
911
import CONST from '@src/CONST';
12+
import ONYXKEYS from '@src/ONYXKEYS';
1013
import type {ReportAction} from '@src/types/onyx';
1114

1215
type FraudAlertContentProps = {
@@ -20,6 +23,8 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) {
2023
const reportActionID = action?.reportActionID;
2124
const originalMessage = getOriginalMessage(action);
2225
const cardID = originalMessage?.cardID;
26+
const [card] = useOnyx(ONYXKEYS.CARD_LIST, {selector: cardByIdSelector(String(cardID))});
27+
const possibleFraud = card?.nameValuePairs?.possibleFraud ?? null;
2328

2429
const buttons: ActionableItem[] = originalMessage?.resolution
2530
? []
@@ -28,15 +33,15 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) {
2833
text: 'cardPage.cardFraudAlert.confirmButtonText',
2934
key: `${action.reportActionID}-cardFraudAlert-confirm`,
3035
onPress: () => {
31-
resolveFraudAlert(cardID, false, reportID, reportActionID);
36+
resolveFraudAlert(cardID, false, reportID, reportActionID, possibleFraud);
3237
},
3338
isPrimary: true,
3439
},
3540
{
3641
text: 'cardPage.cardFraudAlert.reportFraudButtonText',
3742
key: `${action.reportActionID}-cardFraudAlert-reportFraud`,
3843
onPress: () => {
39-
resolveFraudAlert(cardID, true, reportID, reportActionID);
44+
resolveFraudAlert(cardID, true, reportID, reportActionID, possibleFraud);
4045
},
4146
},
4247
];

tests/unit/hooks/useTimeSensitiveCards.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import CONST from '@src/CONST';
66
import ONYXKEYS from '@src/ONYXKEYS';
77
import type {Card, CardList} from '@src/types/onyx';
88
import {createRandomExpensifyCard} from '../../utils/collections/card';
9+
import createRandomReportAction from '../../utils/collections/reportActions';
910
import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates';
1011

1112
describe('useTimeSensitiveCards', () => {
@@ -178,8 +179,15 @@ describe('useTimeSensitiveCards', () => {
178179
possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 123456},
179180
});
180181
const cardList: CardList = {'1': cardWithFraud};
182+
const unresolvedFraudAction = {
183+
...createRandomReportAction(1),
184+
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
185+
};
181186

182187
await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
188+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithFraud.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
189+
[unresolvedFraudAction.reportActionID]: unresolvedFraudAction,
190+
});
183191
await waitForBatchedUpdates();
184192

185193
const {result} = renderHook(() => useTimeSensitiveCards());
@@ -190,6 +198,34 @@ describe('useTimeSensitiveCards', () => {
190198
expect(result.current.shouldShowReviewCardFraud).toBe(true);
191199
});
192200

201+
it('should not show fraud review when card has possibleFraud data but the fraud action is already resolved as recognized', async () => {
202+
const cardWithResolvedFraudAlert = createRandomExpensifyCard(1, {
203+
state: CONST.EXPENSIFY_CARD.STATE.OPEN,
204+
fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN,
205+
possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 123457},
206+
});
207+
const cardList: CardList = {'1': cardWithResolvedFraudAlert};
208+
const baseFraudAction = createRandomReportAction(4);
209+
const resolvedFraudAction = {
210+
...baseFraudAction,
211+
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
212+
originalMessage: {
213+
resolution: CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED,
214+
},
215+
};
216+
217+
await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
218+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithResolvedFraudAlert.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
219+
[resolvedFraudAction.reportActionID]: resolvedFraudAction,
220+
});
221+
await waitForBatchedUpdates();
222+
223+
const {result} = renderHook(() => useTimeSensitiveCards());
224+
225+
expect(result.current.cardsWithFraud).toHaveLength(0);
226+
expect(result.current.shouldShowReviewCardFraud).toBe(false);
227+
});
228+
193229
it('should not show fraud review for cards with fraud type NONE and no possibleFraud data', async () => {
194230
const cardWithNoFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE});
195231
const cardList: CardList = {'1': cardWithNoFraud};
@@ -210,8 +246,15 @@ describe('useTimeSensitiveCards', () => {
210246
possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 5230242215684213},
211247
});
212248
const cardList: CardList = {'1': cardWithPendingFraudAlert};
249+
const unresolvedFraudAction = {
250+
...createRandomReportAction(2),
251+
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
252+
};
213253

214254
await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
255+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithPendingFraudAlert.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
256+
[unresolvedFraudAction.reportActionID]: unresolvedFraudAction,
257+
});
215258
await waitForBatchedUpdates();
216259

217260
const {result} = renderHook(() => useTimeSensitiveCards());
@@ -283,8 +326,15 @@ describe('useTimeSensitiveCards', () => {
283326
} as Card['nameValuePairs'],
284327
};
285328
const cardList: CardList = {'1': zeroLimitFraudCard};
329+
const unresolvedFraudAction = {
330+
...createRandomReportAction(3),
331+
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
332+
};
286333

287334
await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
335+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${zeroLimitFraudCard.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
336+
[unresolvedFraudAction.reportActionID]: unresolvedFraudAction,
337+
});
288338
await waitForBatchedUpdates();
289339

290340
const {result} = renderHook(() => useTimeSensitiveCards());

0 commit comments

Comments
 (0)