Skip to content

Commit 21b1304

Browse files
MariaHCDOSBotify
authored andcommitted
Merge pull request #78615 from Expensify/vit-fixDirectfeedIssue
[CP Staging] Make sure to treat normal direct feeds same as plaid (cherry picked from commit 409b0d6) (cherry-picked to staging by mountiny)
1 parent 48b98ce commit 21b1304

File tree

2 files changed

+238
-1
lines changed

2 files changed

+238
-1
lines changed

src/hooks/useCompanyCards.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ function useCompanyCards({policyID, feedName: feedNameProp}: UseCompanyCardsProp
4949
const selectedFeed = feedName && companyCardFeeds[feedName];
5050
const isPlaidCardFeed = !!getPlaidInstitutionId(feedName);
5151

52+
// Direct feeds include Plaid feeds and OAuth feeds (like oauth.chase.com) that have accountList
53+
const isDirectFeed = isPlaidCardFeed || !!selectedFeed?.accountList;
5254
let cardFeedType: CardFeedType = 'customFeed';
53-
if (isPlaidCardFeed) {
55+
if (isDirectFeed) {
5456
cardFeedType = 'directFeed';
5557
}
5658

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import {renderHook} from '@testing-library/react-native';
2+
import Onyx from 'react-native-onyx';
3+
import useCardFeeds from '@hooks/useCardFeeds';
4+
import useCardsList from '@hooks/useCardsList';
5+
import useCompanyCards from '@hooks/useCompanyCards';
6+
import CONST from '@src/CONST';
7+
import ONYXKEYS from '@src/ONYXKEYS';
8+
import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx/CardFeeds';
9+
import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates';
10+
11+
const mockPolicyID = '123456';
12+
const workspaceAccountID = 11111111;
13+
const domainID = 22222222;
14+
15+
// Custom feed (VCF) without accountList
16+
const mockCustomFeed: CompanyCardFeedWithDomainID = `${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}#${workspaceAccountID}` as CompanyCardFeedWithDomainID;
17+
18+
// OAuth feed (Chase) with accountList
19+
const mockOAuthFeed: CompanyCardFeedWithDomainID = `${CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE}#${domainID}` as CompanyCardFeedWithDomainID;
20+
21+
// Plaid feed
22+
const mockPlaidFeed: CompanyCardFeedWithDomainID = `plaid.ins_123#${workspaceAccountID}` as CompanyCardFeedWithDomainID;
23+
24+
const mockCustomFeedData = {
25+
[mockCustomFeed]: {
26+
liabilityType: 'personal',
27+
pending: false,
28+
domainID: workspaceAccountID,
29+
customFeedName: 'Custom VCF feed',
30+
feed: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA as CompanyCardFeed,
31+
},
32+
};
33+
34+
const mockOAuthFeedData = {
35+
[mockOAuthFeed]: {
36+
liabilityType: 'corporate',
37+
pending: false,
38+
domainID,
39+
customFeedName: 'Chase cards',
40+
feed: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE as CompanyCardFeed,
41+
42+
// OAuth feeds have accountList from oAuthAccountDetails
43+
accountList: ['CREDIT CARD...6607', 'CREDIT CARD...5501'],
44+
credentials: 'xxxxx',
45+
expiration: 1730998958,
46+
},
47+
};
48+
49+
const mockPlaidFeedData = {
50+
[mockPlaidFeed]: {
51+
liabilityType: 'corporate',
52+
pending: false,
53+
domainID: workspaceAccountID,
54+
customFeedName: 'Plaid Bank cards',
55+
feed: 'plaid.ins_123' as CompanyCardFeed,
56+
accountList: ['Plaid Checking 0000', 'Plaid Credit Card 3333'],
57+
credentials: 'xxxxx',
58+
expiration: 1730998958,
59+
},
60+
};
61+
62+
const mockCardsList = {
63+
cardList: {
64+
card1: 'card1',
65+
card2: 'card2',
66+
},
67+
// eslint-disable-next-line @typescript-eslint/naming-convention
68+
'21570652': {
69+
accountID: 18439984,
70+
bank: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA,
71+
cardName: 'card1',
72+
domainName: 'expensify-policy://123456',
73+
lastFourPAN: '1234',
74+
lastScrape: '',
75+
lastScrapeResult: 200,
76+
scrapeMinDate: '2024-09-01',
77+
state: 3,
78+
},
79+
};
80+
81+
jest.mock('@hooks/useCardFeeds', () => ({
82+
// eslint-disable-next-line @typescript-eslint/naming-convention
83+
__esModule: true,
84+
default: jest.fn(),
85+
}));
86+
87+
jest.mock('@hooks/useCardsList', () => ({
88+
// eslint-disable-next-line @typescript-eslint/naming-convention
89+
__esModule: true,
90+
default: jest.fn(),
91+
}));
92+
93+
describe('useCompanyCards', () => {
94+
beforeAll(() => {
95+
Onyx.init({keys: ONYXKEYS});
96+
});
97+
98+
beforeEach(async () => {
99+
await Onyx.clear();
100+
await waitForBatchedUpdates();
101+
});
102+
103+
afterEach(async () => {
104+
jest.restoreAllMocks();
105+
await Onyx.clear();
106+
});
107+
108+
describe('cardFeedType determination', () => {
109+
it('should return cardFeedType as customFeed for VCF feeds without accountList', async () => {
110+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed);
111+
(useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]);
112+
(useCardsList as jest.Mock).mockReturnValue([mockCardsList, {status: 'loaded'}]);
113+
114+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
115+
116+
expect(result.current.cardFeedType).toBe('customFeed');
117+
expect(result.current.feedName).toBe(mockCustomFeed);
118+
});
119+
120+
it('should return cardFeedType as directFeed for OAuth feeds with accountList', async () => {
121+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockOAuthFeed);
122+
(useCardFeeds as jest.Mock).mockReturnValue([mockOAuthFeedData, {status: 'loaded'}, undefined]);
123+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
124+
125+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
126+
127+
expect(result.current.cardFeedType).toBe('directFeed');
128+
expect(result.current.feedName).toBe(mockOAuthFeed);
129+
expect(result.current.selectedFeed?.accountList).toEqual(['CREDIT CARD...6607', 'CREDIT CARD...5501']);
130+
});
131+
132+
it('should return cardFeedType as directFeed for Plaid feeds', async () => {
133+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockPlaidFeed);
134+
(useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]);
135+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
136+
137+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
138+
139+
expect(result.current.cardFeedType).toBe('directFeed');
140+
expect(result.current.feedName).toBe(mockPlaidFeed);
141+
});
142+
});
143+
144+
describe('cardNames derivation', () => {
145+
it('should derive cardNames from cardList for custom feeds', async () => {
146+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed);
147+
(useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]);
148+
(useCardsList as jest.Mock).mockReturnValue([mockCardsList, {status: 'loaded'}]);
149+
150+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
151+
152+
expect(result.current.cardFeedType).toBe('customFeed');
153+
expect(result.current.cardNames).toEqual(['card1', 'card2']);
154+
});
155+
156+
it('should derive cardNames from accountList for OAuth feeds (direct feeds)', async () => {
157+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockOAuthFeed);
158+
(useCardFeeds as jest.Mock).mockReturnValue([mockOAuthFeedData, {status: 'loaded'}, undefined]);
159+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
160+
161+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
162+
163+
expect(result.current.cardFeedType).toBe('directFeed');
164+
expect(result.current.cardNames).toEqual(['CREDIT CARD...6607', 'CREDIT CARD...5501']);
165+
});
166+
167+
it('should derive cardNames from accountList for Plaid feeds (direct feeds)', async () => {
168+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockPlaidFeed);
169+
(useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]);
170+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
171+
172+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
173+
174+
expect(result.current.cardFeedType).toBe('directFeed');
175+
expect(result.current.cardNames).toEqual(['Plaid Checking 0000', 'Plaid Credit Card 3333']);
176+
});
177+
178+
it('should return empty cardNames when cardList is undefined for custom feeds', async () => {
179+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed);
180+
(useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]);
181+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
182+
183+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
184+
185+
expect(result.current.cardFeedType).toBe('customFeed');
186+
expect(result.current.cardNames).toEqual([]);
187+
});
188+
});
189+
190+
describe('policyID handling', () => {
191+
it('should return only onyxMetadata when policyID is undefined', () => {
192+
(useCardFeeds as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}, undefined]);
193+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
194+
195+
const {result} = renderHook(() => useCompanyCards({policyID: undefined}));
196+
197+
expect(result.current.feedName).toBeUndefined();
198+
expect(result.current.cardFeedType).toBeUndefined();
199+
expect(result.current.onyxMetadata).toBeDefined();
200+
});
201+
});
202+
203+
describe('feed selection', () => {
204+
it('should use feedNameProp when provided instead of lastSelectedFeed', async () => {
205+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed);
206+
207+
const combinedFeeds = {...mockCustomFeedData, ...mockOAuthFeedData};
208+
(useCardFeeds as jest.Mock).mockReturnValue([combinedFeeds, {status: 'loaded'}, undefined]);
209+
(useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]);
210+
211+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID, feedName: mockOAuthFeed}));
212+
213+
// Should use provided feedName, not lastSelectedFeed
214+
expect(result.current.feedName).toBe(mockOAuthFeed);
215+
expect(result.current.cardFeedType).toBe('directFeed');
216+
});
217+
});
218+
219+
describe('onyxMetadata', () => {
220+
it('should return all metadata from dependent hooks', async () => {
221+
await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed);
222+
223+
const cardListMetadata = {status: 'loaded'};
224+
const allCardFeedsMetadata = {status: 'loaded'};
225+
(useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, allCardFeedsMetadata, undefined]);
226+
(useCardsList as jest.Mock).mockReturnValue([mockCardsList, cardListMetadata]);
227+
228+
const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID}));
229+
230+
expect(result.current.onyxMetadata.cardListMetadata).toBe(cardListMetadata);
231+
expect(result.current.onyxMetadata.allCardFeedsMetadata).toBe(allCardFeedsMetadata);
232+
expect(result.current.onyxMetadata.lastSelectedFeedMetadata).toBeDefined();
233+
});
234+
});
235+
});

0 commit comments

Comments
 (0)