diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 211bd8e53c55..9a71480019a6 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -10,7 +10,7 @@ import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BankAccountList, Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import type {FilteredCardList} from '@src/types/onyx/Card'; -import type {CompanyCardNicknames, CompanyFeeds} from '@src/types/onyx/CardFeeds'; +import type {CompanyCardNicknames, CompanyFeeds, DirectCardFeedData} from '@src/types/onyx/CardFeeds'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import localeCompare from './LocaleCompare'; @@ -352,10 +352,17 @@ function getSelectedFeed(lastSelectedFeed: OnyxEntry, cardFeeds return lastSelectedFeed ?? defaultFeed; } -function getFilteredCardList(list?: WorkspaceCardsList) { - const {cardList, ...cards} = list ?? {}; - // We need to filter out cards which already has been assigned - return Object.fromEntries(Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN)))); +/** Returns list of cards which can be assigned */ +function getFilteredCardList(list: WorkspaceCardsList | undefined, directFeed: DirectCardFeedData | undefined) { + const {cardList: customFeedCardsToAssign, ...cards} = list ?? {}; + const assignedCards = Object.values(cards).map((card) => card.cardName); + + if (directFeed) { + const unassignedDirectFeedCards = directFeed.accountList.filter((cardNumber) => !assignedCards.includes(cardNumber)); + return Object.fromEntries(unassignedDirectFeedCards.map((cardNumber) => [cardNumber, cardNumber])); + } + + return Object.fromEntries(Object.entries(customFeedCardsToAssign ?? {}).filter(([cardNumber]) => !assignedCards.includes(cardNumber))); } function hasOnlyOneCardToAssign(list: FilteredCardList) { diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 7cdfb15ce68d..e956e618f9a4 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -42,7 +42,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { const policy = PolicyUtils.getPolicy(policyID); - const filteredCardList = CardUtils.getFilteredCardList(cardsList); + const filteredCardList = CardUtils.getFilteredCardList(cardsList, selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined); const companyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); const selectedFeedData = selectedFeed && companyCards[selectedFeed]; diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 2fe757c4e36f..bbdd1b299091 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -32,7 +32,12 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { switch (currentStep) { case CONST.COMPANY_CARD.STEP.ASSIGNEE: - return ; + return ( + + ); case CONST.COMPANY_CARD.STEP.CARD: return ( ); default: - return ; + return ( + + ); } } diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 9500300b063d..210e2b76929b 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -27,19 +27,23 @@ import type * as OnyxTypes from '@src/types/onyx'; const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8; type AssigneeStepProps = { - // The policy that the card will be issued under + /** The policy that the card will be issued under */ policy: OnyxEntry; + + /** Selected feed */ + feed: OnyxTypes.CompanyCardFeed; }; -function AssigneeStep({policy}: AssigneeStepProps) { +function AssigneeStep({policy, feed}: AssigneeStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1'); - const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${assignCard?.data?.bankName ?? ''}`); - const filteredCardList = CardUtils.getFilteredCardList(list); + const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${feed}`); + const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const filteredCardList = CardUtils.getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed]); const isEditing = assignCard?.isEditing; @@ -69,6 +73,8 @@ function AssigneeStep({policy}: AssigneeStepProps) { data: { email: selectedMember, cardName: CardUtils.getDefaultCardName(memberName), + cardNumber: Object.keys(filteredCardList).at(0), + encryptedCardNumber: Object.values(filteredCardList).at(0), }, isEditing: false, }); diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index dca02a1499b7..e537bbc3a625 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -41,11 +41,10 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) { const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${feed}`); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); - const accountCardList = cardFeeds?.settings?.oAuthAccountDetails?.[feed]?.accountList ?? []; const isEditing = assignCard?.isEditing; const assigneeDisplayName = PersonalDetailsUtils.getPersonalDetailByEmail(assignCard?.data?.email ?? '')?.displayName ?? ''; - const filteredCardList = CardUtils.getFilteredCardList(list); + const filteredCardList = CardUtils.getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed]); const [cardSelected, setCardSelected] = useState(assignCard?.data?.encryptedCardNumber ?? ''); const [shouldShowError, setShouldShowError] = useState(false); @@ -66,21 +65,6 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) { setShouldShowError(false); }; - const accountCardListOptions = accountCardList.map((encryptedCardNumber) => ({ - keyForList: encryptedCardNumber, - value: encryptedCardNumber, - text: encryptedCardNumber, - isSelected: cardSelected === encryptedCardNumber, - leftElement: ( - - ), - })); - const submit = () => { if (!cardSelected) { setShouldShowError(true); @@ -94,7 +78,7 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) { CompanyCards.setAssignCardStepAndData({ currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE, - data: {encryptedCardNumber: cardSelected, cardNumber: accountCardList?.length > 0 ? cardSelected : cardNumber}, + data: {encryptedCardNumber: cardSelected, cardNumber}, isEditing: false, }); }; @@ -114,11 +98,9 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) { ), })); - const listOptions = accountCardList?.length > 0 ? accountCardListOptions : cardListOptions; - const searchedListOptions = useMemo(() => { - return listOptions.filter((option) => option.text.toLowerCase().includes(searchText)); - }, [searchText, listOptions]); + return cardListOptions.filter((option) => option.text.toLowerCase().includes(searchText)); + }, [searchText, cardListOptions]); return ( - {!listOptions.length ? ( + {!cardListOptions.length ? ( CONST.COMPANY_CARDS.CARD_LIST_THRESHOLD} + shouldShowTextInput={cardListOptions.length > CONST.COMPANY_CARDS.CARD_LIST_THRESHOLD} textInputLabel={translate('common.search')} textInputValue={searchText} onChangeText={setSearchText} diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index d13e0182eca8..2ebc125b6dca 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -52,7 +52,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); - const filteredCardList = CardUtils.getFilteredCardList(list); + const filteredCardList = CardUtils.getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed as CompanyCardFeed]); const handleSubmit = () => { if (!selectedFeed) { diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 1377b89bc441..c294b068a62d 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -34,11 +34,87 @@ const customFeedsWithoutExpensifyBank = { }; const directFeeds = { [CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: { - accountList: ['CREDIT CARD...6607'], + accountList: ['CREDIT CARD...6607', 'CREDIT CARD...5501'], credentials: 'xxxxx', expiration: 1730998958, }, + [CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]: { + accountList: ['CREDIT CARD...1233', 'CREDIT CARD...5678', 'CREDIT CARD...4444', 'CREDIT CARD...3333', 'CREDIT CARD...7788'], + credentials: 'xxxxx', + expiration: 1730998959, + }, +}; + +const directFeedCardsSingleList: OnyxTypes.WorkspaceCardsList = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '21570652': { + accountID: 18439984, + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE, + cardID: 21570652, + cardName: 'CREDIT CARD...5501', + domainName: 'expensify-policya7f617b9fe23d2f1.exfy', + fraud: 'none', + lastFourPAN: '5501', + lastScrape: '', + lastUpdated: '', + scrapeMinDate: '2024-08-27', + state: 3, + }, }; +const directFeedCardsMultipleList: OnyxTypes.WorkspaceCardsList = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '21570655': { + accountID: 18439984, + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE, + cardID: 21570655, + cardName: 'CREDIT CARD...5678', + domainName: 'expensify-policya7f617b9fe23d2f1.exfy', + fraud: 'none', + lastFourPAN: '5678', + lastScrape: '', + lastUpdated: '', + scrapeMinDate: '2024-08-27', + state: 3, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '21570656': { + accountID: 18439984, + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE, + cardID: 21570656, + cardName: 'CREDIT CARD...4444', + domainName: 'expensify-policya7f617b9fe23d2f1.exfy', + fraud: 'none', + lastFourPAN: '5678', + lastScrape: '', + lastUpdated: '', + scrapeMinDate: '2024-08-27', + state: 3, + }, +}; +const customFeedCardsList = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '21310091': { + accountID: 18439984, + bank: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, + cardID: 21310091, + cardName: '480801XXXXXX2554', + domainName: 'expensify-policy41314f4dc5ce25af.exfy', + fraud: 'none', + lastFourPAN: '2554', + lastUpdated: '', + lastScrape: '2024-11-27 11:00:53', + scrapeMinDate: '2024-10-17', + state: 3, + }, + cardList: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '480801XXXXXX2111': 'ENCRYPTED_CARD_NUMBER', + // eslint-disable-next-line @typescript-eslint/naming-convention + '480801XXXXXX2554': 'ENCRYPTED_CARD_NUMBER', + // eslint-disable-next-line @typescript-eslint/naming-convention + '480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER', + }, +} as unknown as OnyxTypes.WorkspaceCardsList; const allFeeds: CompanyFeeds = {...customFeeds, ...directFeeds}; const customFeedName = 'Custom feed name'; @@ -272,4 +348,29 @@ describe('CardUtils', () => { expect(feedName).toBe(''); }); }); + + describe('getFilteredCardList', () => { + it('Should return filtered custom feed cards list', () => { + const cardsList = CardUtils.getFilteredCardList(customFeedCardsList, undefined); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(cardsList).toStrictEqual({'480801XXXXXX2111': 'ENCRYPTED_CARD_NUMBER', '480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER'}); + }); + + it('Should return filtered direct feed cards list with a single card', () => { + const cardsList = CardUtils.getFilteredCardList(directFeedCardsSingleList, directFeeds[CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(cardsList).toStrictEqual({'CREDIT CARD...6607': 'CREDIT CARD...6607'}); + }); + + it('Should return filtered direct feed cards list with multiple cards', () => { + const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, directFeeds[CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(cardsList).toStrictEqual({'CREDIT CARD...1233': 'CREDIT CARD...1233', 'CREDIT CARD...3333': 'CREDIT CARD...3333', 'CREDIT CARD...7788': 'CREDIT CARD...7788'}); + }); + + it('Should return empty object if no data was provided', () => { + const cardsList = CardUtils.getFilteredCardList(undefined, undefined); + expect(cardsList).toStrictEqual({}); + }); + }); });