Skip to content
Open
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
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3914,6 +3914,7 @@ const CONST = {
AMEX_DIRECT: 'oauth.americanexpressfdx.com',
AMEX_FILE_DOWNLOAD: 'americanexpressfd.us',
CSV: 'ccupload',
CSV_CLASSIC: 'csv',
MOCK_BANK: 'oauth.mockbank.com',
UPLOAD: 'upload',
},
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useCardFeeds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ const useCardFeeds = (policyID: string | undefined): [CombinedCardFeeds | undefi
let workspaceFeeds: CombinedCardFeeds | undefined;
if (policyID && allFeeds) {
const shouldIncludeFeedPredicate = (combinedCardFeed: CombinedCardFeed) => {
if (combinedCardFeed?.linkedPolicyIDs) {
return combinedCardFeed.linkedPolicyIDs.includes(policyID);
const validLinkedPolicyIDs = combinedCardFeed?.linkedPolicyIDs?.filter(Boolean);
if (validLinkedPolicyIDs?.length) {
return validLinkedPolicyIDs.includes(policyID);
}
return combinedCardFeed.preferredPolicy ? combinedCardFeed.preferredPolicy === policyID : combinedCardFeed.domainID === effectiveWorkspaceAccountID;
};
Expand Down
9 changes: 6 additions & 3 deletions src/libs/CardFeedUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isCard,
isCardClosed,
isCardHiddenFromSearch,
isCSVUploadFeed,
isCustomFeed,
isDirectFeed,
isPersonalCard,
Expand Down Expand Up @@ -628,20 +629,22 @@ function getCombinedCardFeedsFromAllFeeds(

// When we have card data, filter out stale feeds:
// - Direct feeds without oAuthAccountDetails AND no assigned cards
// - "Gray zone" feeds (not commercial, not direct) without assigned cards
// - "Gray zone" feeds (not commercial, not direct, not CSV upload) without assigned cards
Comment thread
Gonals marked this conversation as resolved.
Comment thread
Gonals marked this conversation as resolved.
// CSV upload feeds are always shown when they exist in settings, since their
// unassigned cards are loaded on-demand when the feed is selected.
if (feedKeysWithCards) {
if (isDirectFeed(feedName) && !oAuthAccountDetails && !feedHasCards(feedName, domainID, feedKeysWithCards)) {
continue;
}
if (!isCustomFeed(feedName) && !isDirectFeed(feedName) && !feedHasCards(feedName, domainID, feedKeysWithCards)) {
if (!isCustomFeed(feedName) && !isDirectFeed(feedName) && !isCSVUploadFeed(feedName) && !feedHasCards(feedName, domainID, feedKeysWithCards)) {
continue;
}
}

const combinedCardFeed: CombinedCardFeed = {
...feedSettings,
...oAuthAccountDetails,
customFeedName,
customFeedName: customFeedName ?? feedSettings?.uploadLayoutSettings?.layoutName,
domainID,
feed: feedName,
status,
Expand Down
16 changes: 14 additions & 2 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,13 +542,24 @@ function isCustomFeed(feed: string | undefined): boolean {
return CUSTOM_FEED_PREFIXES.some((value) => feed.startsWith(value));
}

/**
* Checks if a feed is a CSV upload feed (ccupload or csv prefix).
* Covers both NewDot-created feeds (ccupload*) and Classic-created feeds (csv*).
*/
function isCSVUploadFeed(feed: string | undefined): boolean {
if (!feed) {
return false;
}
const lowerFeed = feed.toLowerCase();
return lowerFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV) || lowerFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV_CLASSIC);
}

/**
* Checks if a feed key represents a CSV feed or Expensify Card.
* CSV feeds from Classic and Expensify Cards should not count toward the feed limit for Collect plan workspaces.
*/
function isCSVFeedOrExpensifyCard(feedKey: string): boolean {
const lowerFeedKey = feedKey.toLowerCase();
return lowerFeedKey.startsWith('csv') || lowerFeedKey.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV) || feedKey === CONST.EXPENSIFY_CARD.BANK;
return isCSVUploadFeed(feedKey) || feedKey === CONST.EXPENSIFY_CARD.BANK;
}

/**
Expand Down Expand Up @@ -1725,6 +1736,7 @@ export {
hasCompanyCardFeeds,
isPersonalCardBrokenConnection,
isCustomFeed,
isCSVUploadFeed,
isCSVFeedOrExpensifyCard,
getBankCardDetailsImage,
getSelectedFeed,
Expand Down
7 changes: 7 additions & 0 deletions src/types/onyx/CardFeeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ type CustomCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Plaid access token */
plaidAccessToken?: string;

/** CSV upload layout settings (present on ccupload feeds) */
uploadLayoutSettings?: {
/** User-defined name for the CSV upload layout */
layoutName?: string;
[key: string]: unknown;
};

/** Field-specific error messages */
errorFields?: OnyxCommon.ErrorFields<'statementPeriodEndDay'>;

Expand Down
46 changes: 46 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
isCardAlreadyAssigned,
isCardFrozen,
isCSVFeedOrExpensifyCard,
isCSVUploadFeed,
isCustomFeed as isCustomFeedCardUtils,
isDirectFeed as isDirectFeedCardUtils,
isExpensifyCard,
Expand Down Expand Up @@ -1319,6 +1320,51 @@ describe('CardUtils', () => {
});
});

describe('isCSVUploadFeed', () => {
it('Should return true for ccupload feed', () => {
expect(isCSVUploadFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV)).toBe(true);
});

it('Should return true for ccupload feed with number suffix', () => {
expect(isCSVUploadFeed('ccupload1')).toBe(true);
expect(isCSVUploadFeed('ccupload4')).toBe(true);
});

it('Should return true for Classic csv feed', () => {
expect(isCSVUploadFeed('csv')).toBe(true);
expect(isCSVUploadFeed('csv1')).toBe(true);
});

it('Should return true for csv feed key with domain ID', () => {
expect(isCSVUploadFeed('csv#123456')).toBe(true);
expect(isCSVUploadFeed('ccupload1#158')).toBe(true);
});

it('Should return true regardless of case', () => {
expect(isCSVUploadFeed('CCUpload1')).toBe(true);
expect(isCSVUploadFeed('CSV1')).toBe(true);
});

it('Should return false for direct feeds', () => {
expect(isCSVUploadFeed('oauth.chase.com')).toBe(false);
expect(isCSVUploadFeed('plaid.ins_19')).toBe(false);
});

it('Should return false for custom feeds', () => {
expect(isCSVUploadFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.VISA)).toBe(false);
expect(isCSVUploadFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)).toBe(false);
});

it('Should return false for Expensify Card', () => {
expect(isCSVUploadFeed(CONST.EXPENSIFY_CARD.BANK)).toBe(false);
});

it('Should return false for undefined or empty', () => {
expect(isCSVUploadFeed(undefined)).toBe(false);
expect(isCSVUploadFeed('')).toBe(false);
});
});

describe('isCSVFeedOrExpensifyCard', () => {
it('Should return true for CSV feed keys', () => {
expect(isCSVFeedOrExpensifyCard('csv#123456')).toBe(true);
Expand Down
Loading