Skip to content

Commit 9a130e5

Browse files
committed
address reviewers comments
1 parent e14bcbd commit 9a130e5

File tree

4 files changed

+156
-8
lines changed

4 files changed

+156
-8
lines changed

src/hooks/useIsBlockedToAddFeed.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {useMemo} from 'react';
2-
import {getCompanyFeeds} from '@libs/CardUtils';
2+
import {getCompanyFeeds, isCSVFeed} from '@libs/CardUtils';
33
import {isCollectPolicy} from '@libs/PolicyUtils';
4-
import CONST from '@src/CONST';
54
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
65
import useCardFeeds from './useCardFeeds';
76
import usePolicy from './usePolicy';
@@ -35,12 +34,8 @@ function useIsBlockedToAddFeed(policyID?: string) {
3534
if (isLoading) {
3635
return 0;
3736
}
38-
const nonCSVFeeds = Object.entries(companyFeeds ?? {}).filter(([feedKey]) => {
39-
const lowerFeedKey = feedKey.toLowerCase();
40-
// Exclude CSV feeds (feed types starting with "csv" or "ccupload", or containing "ccupload")
41-
// Also exclude Expensify Cards which don't count toward the limit
42-
return !lowerFeedKey.startsWith('csv') && !lowerFeedKey.startsWith('ccupload') && !feedKey.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV) && feedKey !== 'Expensify Card';
43-
});
37+
const feeds = companyFeeds ?? {};
38+
const nonCSVFeeds = Object.keys(feeds).filter((feedKey) => !isCSVFeed(feedKey));
4439
return nonCSVFeeds.length;
4540
}, [isLoading, companyFeeds]);
4641

src/libs/CardUtils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,21 @@ function isCustomFeed(feed: CompanyCardFeedWithNumber | undefined): boolean {
397397
return [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX].some((value) => feed.startsWith(value));
398398
}
399399

400+
/**
401+
* Checks if a feed key represents a CSV feed or Expensify Card.
402+
* CSV feeds include feeds starting with "csv" or "ccupload", or containing "ccupload".
403+
* Expensify Cards also don't count toward feed limits.
404+
*
405+
* @param feedKey - The feed key to check
406+
* @returns true if the feed is a CSV feed or Expensify Card, false otherwise
407+
*/
408+
function isCSVFeed(feedKey: string): boolean {
409+
const lowerFeedKey = feedKey.toLowerCase();
410+
// Exclude CSV feeds (feed types starting with "csv" or "ccupload", or containing "ccupload")
411+
// Also exclude Expensify Cards which don't count toward the limit
412+
return lowerFeedKey.startsWith('csv') || lowerFeedKey.startsWith('ccupload') || feedKey.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV) || feedKey === 'Expensify Card';
413+
}
414+
400415
function getOriginalCompanyFeeds(cardFeeds: OnyxEntry<CardFeeds>): CompanyFeeds {
401416
return Object.fromEntries(
402417
Object.entries(cardFeeds?.settings?.companyCards ?? {}).filter(([key, value]) => {
@@ -922,6 +937,7 @@ export {
922937
isSelectedFeedExpired,
923938
getCompanyFeeds,
924939
isCustomFeed,
940+
isCSVFeed,
925941
getBankCardDetailsImage,
926942
getSelectedFeed,
927943
getPlaidCountry,

tests/unit/CardUtilsTest.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getSelectedFeed,
3333
getYearFromExpirationDateString,
3434
hasIssuedExpensifyCard,
35+
isCSVFeed,
3536
isCustomFeed as isCustomFeedCardUtils,
3637
isExpensifyCard,
3738
isExpensifyCardFullySetUp,
@@ -499,6 +500,63 @@ describe('CardUtils', () => {
499500
});
500501
});
501502

503+
describe('isCSVFeed', () => {
504+
it('Should return true for feed key starting with "csv" (lowercase)', () => {
505+
expect(isCSVFeed('csv123')).toBe(true);
506+
});
507+
508+
it('Should return true for feed key starting with "CSV" (uppercase)', () => {
509+
expect(isCSVFeed('CSV123')).toBe(true);
510+
});
511+
512+
it('Should return true for feed key starting with "Csv" (mixed case)', () => {
513+
expect(isCSVFeed('Csv123')).toBe(true);
514+
});
515+
516+
it('Should return true for feed key starting with "ccupload" (lowercase)', () => {
517+
expect(isCSVFeed('ccupload123')).toBe(true);
518+
});
519+
520+
it('Should return true for feed key starting with "CCUPLOAD" (uppercase)', () => {
521+
expect(isCSVFeed('CCUPLOAD123')).toBe(true);
522+
});
523+
524+
it('Should return true for feed key starting with "Ccupload" (mixed case)', () => {
525+
expect(isCSVFeed('Ccupload123')).toBe(true);
526+
});
527+
528+
it('Should return true for feed key containing "ccupload"', () => {
529+
expect(isCSVFeed('prefix-ccupload-suffix')).toBe(true);
530+
});
531+
532+
it('Should return true for feed key containing CONST.COMPANY_CARD.FEED_BANK_NAME.CSV', () => {
533+
expect(isCSVFeed(`prefix-${CONST.COMPANY_CARD.FEED_BANK_NAME.CSV}-suffix`)).toBe(true);
534+
});
535+
536+
it('Should return true for "Expensify Card" feed key', () => {
537+
expect(isCSVFeed('Expensify Card')).toBe(true);
538+
});
539+
540+
it('Should return false for regular feed keys', () => {
541+
expect(isCSVFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.VISA)).toBe(false);
542+
expect(isCSVFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)).toBe(false);
543+
expect(isCSVFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX)).toBe(false);
544+
expect(isCSVFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE)).toBe(false);
545+
});
546+
547+
it('Should return false for feed keys that contain "csv" but do not start with it', () => {
548+
expect(isCSVFeed('vcf-csv-feed')).toBe(false);
549+
});
550+
551+
it('Should return true for feed keys that contain "ccupload" anywhere (not just at start)', () => {
552+
expect(isCSVFeed('prefix-ccupload-suffix')).toBe(true);
553+
});
554+
555+
it('Should return false for empty string', () => {
556+
expect(isCSVFeed('')).toBe(false);
557+
});
558+
});
559+
502560
describe('getOriginalCompanyFeeds', () => {
503561
it('Should return both custom and direct feeds with filtered out "Expensify Card" bank', () => {
504562
const companyFeeds = getOriginalCompanyFeeds(cardFeedsCollection.FAKE_ID_1);

tests/unit/hooks/useIsBlockedToAddFeed.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,83 @@ describe('useIsBlockedToAddFeed', () => {
123123
rerender(mockPolicyID);
124124
expect(result.current.isBlockedToAddNewFeeds).toBe(true);
125125
});
126+
127+
it('should return true if collect policy and pending feed exists (pending feeds count toward limit)', () => {
128+
(useCardFeeds as jest.Mock).mockReturnValue([
129+
{
130+
// eslint-disable-next-line @typescript-eslint/naming-convention
131+
'plaid.ins_19#123456': {
132+
feed: 'plaid.ins_19',
133+
domainID: 123456,
134+
pending: true,
135+
liabilityType: 'corporate',
136+
},
137+
},
138+
{status: 'loaded'},
139+
]);
140+
const {result} = renderHook(() => useIsBlockedToAddFeed(mockPolicyID));
141+
// Pending feeds count toward the limit, so user should be blocked from adding another feed
142+
expect(result.current.isBlockedToAddNewFeeds).toBe(true);
143+
});
144+
145+
it('should return false if collect policy and pending CSV feed exists (pending CSV feeds do not count toward limit)', () => {
146+
(useCardFeeds as jest.Mock).mockReturnValue([
147+
{
148+
// eslint-disable-next-line @typescript-eslint/naming-convention
149+
'csv#123456': {
150+
feed: 'csv#123456',
151+
customFeedName: 'CSV Upload',
152+
accountList: ['Card 0000'],
153+
pending: true,
154+
},
155+
},
156+
{status: 'loaded'},
157+
]);
158+
const {result} = renderHook(() => useIsBlockedToAddFeed(mockPolicyID));
159+
// Pending CSV feeds don't count toward the limit, so user can add another feed
160+
expect(result.current.isBlockedToAddNewFeeds).toBe(false);
161+
});
162+
163+
it('should return true if collect policy has both regular feed and pending feed', () => {
164+
(useCardFeeds as jest.Mock).mockReturnValue([
165+
{
166+
// eslint-disable-next-line @typescript-eslint/naming-convention
167+
'plaid.ins_19#123456': {
168+
feed: 'plaid.ins_19',
169+
domainID: 123456,
170+
pending: false,
171+
liabilityType: 'corporate',
172+
},
173+
// eslint-disable-next-line @typescript-eslint/naming-convention
174+
'oauth.chase.com#123456': {
175+
feed: 'oauth.chase.com',
176+
domainID: 123456,
177+
pending: true,
178+
liabilityType: 'corporate',
179+
},
180+
},
181+
{status: 'loaded'},
182+
]);
183+
const {result} = renderHook(() => useIsBlockedToAddFeed(mockPolicyID));
184+
// Both regular and pending feeds count, so user should be blocked
185+
expect(result.current.isBlockedToAddNewFeeds).toBe(true);
186+
});
187+
188+
it('should return true if collect policy has only pending feed (no regular feeds)', () => {
189+
(useCardFeeds as jest.Mock).mockReturnValue([
190+
{
191+
// eslint-disable-next-line @typescript-eslint/naming-convention
192+
'oauth.chase.com#123456': {
193+
feed: 'oauth.chase.com',
194+
domainID: 123456,
195+
pending: true,
196+
liabilityType: 'corporate',
197+
},
198+
},
199+
{status: 'loaded'},
200+
]);
201+
const {result} = renderHook(() => useIsBlockedToAddFeed(mockPolicyID));
202+
// Pending feeds count toward the limit, so even with only a pending feed, user should be blocked
203+
expect(result.current.isBlockedToAddNewFeeds).toBe(true);
204+
});
126205
});

0 commit comments

Comments
 (0)