diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 76ea522624fa..d53ed4351d20 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -469,6 +469,41 @@ function getFeedType(feedKey: CompanyCardFeed, cardFeeds: OnyxEntry): return feedKey; } +/** + * Takes the list of cards divided by workspaces and feeds and returns the flattened non-Expensify cards related to the provided workspace + * + * @param allCardsList the list where cards split by workspaces and feeds and stored under `card_${workspaceAccountID}_${feedName}` keys + * @param workspaceAccountID the workspace account id we want to get cards for + */ +function flatAllCardsList(allCardsList: OnyxCollection, workspaceAccountID: number): Record | undefined { + if (!allCardsList) { + return; + } + + return Object.entries(allCardsList).reduce((acc, [key, cards]) => { + if (!key.includes(workspaceAccountID.toString()) || key.includes(CONST.EXPENSIFY_CARD.BANK)) { + return acc; + } + const {cardList, ...feedCards} = cards ?? {}; + Object.assign(acc, feedCards); + return acc; + }, {}); +} + +/** + * Check if any card from the provided feed(s) has a broken connection + * + * @param feedCards the list of the cards, related to one or several feeds + * @param [feedToExclude] the feed to ignore during the check, it's useful for checking broken connection error only in the feeds other than the selected one + */ +function checkIfFeedConnectionIsBroken(feedCards: Record | undefined, feedToExclude?: string): boolean { + if (!feedCards || isEmptyObject(feedCards)) { + return false; + } + + return Object.values(feedCards).some((card) => card.bank !== feedToExclude && card.lastScrapeResult !== 200); +} + export { isExpensifyCard, isCorporateCard, @@ -504,4 +539,6 @@ export { isCardIssued, isCardHiddenFromSearch, getFeedType, + flatAllCardsList, + checkIfFeedConnectionIsBroken, }; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index f6e6dbb3729d..0c72cd814cd1 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -1,4 +1,4 @@ -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type { @@ -18,11 +18,10 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, CardFeeds, WorkspaceCardsList} from '@src/types/onyx'; +import type {Card, CardFeeds} from '@src/types/onyx'; import type {AssignCard, AssignCardData} from '@src/types/onyx/AssignCard'; import type {AddNewCardFeedData, AddNewCardFeedStep, CompanyCardFeed} from '@src/types/onyx/CardFeeds'; import type {OnyxData} from '@src/types/onyx/Request'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AddNewCompanyCardFlowData = { /** Step to be set in Onyx */ @@ -725,41 +724,6 @@ function openPolicyCompanyCardsFeed(policyID: string, feed: CompanyCardFeed) { API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED, parameters); } -/** - * Takes the list of cards divided by workspaces and feeds and returns the flattened non-Expensify cards related to the provided workspace - * - * @param allCardsList the list where cards split by workspaces and feeds and stored under `card_${workspaceAccountID}_${feedName}` keys - * @param workspaceAccountID the workspace account id we want to get cards for - */ -function flatAllCardsList(allCardsList: OnyxCollection, workspaceAccountID: number): Record | undefined { - if (!allCardsList) { - return; - } - - return Object.entries(allCardsList).reduce((acc, [key, allCards]) => { - if (!key.includes(workspaceAccountID.toString()) || key.includes(CONST.EXPENSIFY_CARD.BANK)) { - return acc; - } - const {cardList, ...feedCards} = allCards ?? {}; - Object.assign(acc, feedCards); - return acc; - }, {}); -} - -/** - * Check if any feed card has a broken connection - * - * @param feedCards the list of the cards, related to one or several feeds - * @param [feedToExclude] the feed to ignore during the check, it's useful for checking broken connection error only in the feeds other than the selected one - */ -function checkIfFeedConnectionIsBroken(feedCards: Record | undefined, feedToExclude?: string): boolean { - if (!feedCards || isEmptyObject(feedCards)) { - return false; - } - - return Object.values(feedCards).some((card) => card.bank !== feedToExclude && card.lastScrapeResult !== 200); -} - export { setWorkspaceCompanyCardFeedName, deleteWorkspaceCompanyCardFeed, @@ -777,6 +741,4 @@ export { clearAddNewCardFlow, setAssignCardStepAndData, clearAssignCardStepAndData, - checkIfFeedConnectionIsBroken, - flatAllCardsList, }; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index af7d71b70d92..f57b6044d993 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -23,6 +23,7 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {isConnectionInProgress} from '@libs/actions/connections'; import {clearErrors, openPolicyInitialPage, removeWorkspace, updateGeneralSettings} from '@libs/actions/Policy/Policy'; import {navigateToBankAccountRoute} from '@libs/actions/ReimbursementAccount'; +import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; @@ -43,7 +44,6 @@ import { import {getDefaultWorkspaceAvatar, getIcons, getPolicyExpenseChat, getReportName, getReportOfflinePendingActionAndErrors} from '@libs/ReportUtils'; import type {FullScreenNavigatorParamList} from '@navigation/types'; import {confirmReadyToOpenApp} from '@userActions/App'; -import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx index 980d8e08fb77..ae677ca5dbaf 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -10,7 +10,7 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, getSelectedFeed} from '@libs/CardUtils'; +import {checkIfFeedConnectionIsBroken, getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, getSelectedFeed} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getWorkspaceAccountID} from '@libs/PolicyUtils'; @@ -18,7 +18,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import {updateSelectedFeed} from '@userActions/Card'; -import {checkIfFeedConnectionIsBroken, clearAddNewCardFlow} from '@userActions/CompanyCards'; +import {clearAddNewCardFlow} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 559029db3848..037aa2fdfe8b 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -13,11 +13,10 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getBankName, getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, isCustomFeed} from '@libs/CardUtils'; +import {checkIfFeedConnectionIsBroken, flatAllCardsList, getBankName, getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, isCustomFeed} from '@libs/CardUtils'; import {getWorkspaceAccountID} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; -import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@userActions/CompanyCards'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CompanyCardFeed} from '@src/types/onyx'; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 470031f2d3c8..5fef9d0dba38 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -8,7 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCompanyFeeds, getFilteredCardList, getSelectedFeed, hasOnlyOneCardToAssign, isSelectedFeedExpired} from '@libs/CardUtils'; +import {checkIfFeedConnectionIsBroken, getCompanyFeeds, getFilteredCardList, getSelectedFeed, hasOnlyOneCardToAssign, isSelectedFeedExpired} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; @@ -16,7 +16,7 @@ import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getWorkspaceAccountID, isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {checkIfFeedConnectionIsBroken, openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAssignCardStepAndData} from '@userActions/CompanyCards'; +import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 642494d6ed11..8cfa5d0c5e45 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -1,6 +1,7 @@ import type {OnyxCollection} from 'react-native-onyx'; import CONST from '@src/CONST'; import * as CardUtils from '@src/libs/CardUtils'; +import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@src/libs/CardUtils'; import type * as OnyxTypes from '@src/types/onyx'; import type {CompanyCardFeedWithNumber} from '@src/types/onyx/CardFeeds'; @@ -102,6 +103,7 @@ const directFeedCardsSingleList: OnyxTypes.WorkspaceCardsList = { lastFourPAN: '5501', lastScrape: '', lastUpdated: '', + lastScrapeResult: 200, scrapeMinDate: '2024-08-27', state: 3, }, @@ -118,6 +120,7 @@ const directFeedCardsMultipleList: OnyxTypes.WorkspaceCardsList = { lastFourPAN: '5678', lastScrape: '', lastUpdated: '', + lastScrapeResult: 200, scrapeMinDate: '2024-08-27', state: 3, }, @@ -132,6 +135,7 @@ const directFeedCardsMultipleList: OnyxTypes.WorkspaceCardsList = { lastFourPAN: '5678', lastScrape: '', lastUpdated: '', + lastScrapeResult: 403, scrapeMinDate: '2024-08-27', state: 3, }, @@ -200,6 +204,28 @@ const cardFeedsCollection: OnyxCollection = { }, }; +/* eslint-disable @typescript-eslint/naming-convention */ +const allCardsList = { + 'cards_11111111_oauth.capitalone.com': directFeedCardsMultipleList, + cards_11111111_vcf1: customFeedCardsList, + 'cards_22222222_oauth.chase.com': directFeedCardsSingleList, + 'cards_11111111_Expensify Card': { + '21570657': { + accountID: 18439984, + bank: CONST.EXPENSIFY_CARD.BANK, + cardID: 21570657, + cardName: 'CREDIT CARD...5644', + domainName: 'expensify-policya7f617b9fe23d2f1.exfy', + fraud: 'none', + lastFourPAN: '', + lastScrape: '', + lastUpdated: '', + state: 2, + }, + }, +} as OnyxCollection; +/* eslint-enable @typescript-eslint/naming-convention */ + describe('CardUtils', () => { describe('Expiration date formatting', () => { it('Should format expirationDate month and year to MM/YYYY', () => { @@ -472,4 +498,42 @@ describe('CardUtils', () => { expect(feedType).toBe('vcf2'); }); }); + + describe('flatAllCardsList', () => { + it('should return the flattened list of non-Expensify cards related to the provided workspaceAccountID', () => { + const workspaceAccountID = 11111111; + const flattenedCardsList = flatAllCardsList(allCardsList, workspaceAccountID); + const {cardList, ...customCards} = customFeedCardsList; + expect(flattenedCardsList).toStrictEqual({ + ...directFeedCardsMultipleList, + ...customCards, + }); + }); + + it('should return undefined if not defined cards list was provided', () => { + const workspaceAccountID = 11111111; + const flattenedCardsList = flatAllCardsList(undefined, workspaceAccountID); + expect(flattenedCardsList).toBeUndefined(); + }); + }); + + describe('checkIfFeedConnectionIsBroken', () => { + it('should return true if at least one of the feed(s) cards has the lastScrapeResult not equal to 200', () => { + expect(checkIfFeedConnectionIsBroken(directFeedCardsMultipleList)).toBeTruthy(); + }); + + it('should return false if all of the feed(s) cards has the lastScrapeResult equal to 200', () => { + expect(checkIfFeedConnectionIsBroken(directFeedCardsSingleList)).toBeFalsy(); + }); + + it('should return false if no feed(s) cards are provided', () => { + expect(checkIfFeedConnectionIsBroken({})).toBeFalsy(); + }); + + it('should not take into consideration cards related to feed which is provided as feedToExclude', () => { + const cards = {...directFeedCardsMultipleList, ...directFeedCardsSingleList}; + const feedToExclude = CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE; + expect(checkIfFeedConnectionIsBroken(cards, feedToExclude)).toBeFalsy(); + }); + }); });