Skip to content

Commit

Permalink
Merge pull request #53200 from callstack-internal/VickyStash/bugfix/5…
Browse files Browse the repository at this point in the history
…2820-direct-feed-card-assignment
  • Loading branch information
mountiny authored Nov 27, 2024
2 parents 951a308 + 1785105 commit 1a68638
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 38 deletions.
17 changes: 12 additions & 5 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -352,10 +352,17 @@ function getSelectedFeed(lastSelectedFeed: OnyxEntry<CompanyCardFeed>, 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
14 changes: 12 additions & 2 deletions src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {

switch (currentStep) {
case CONST.COMPANY_CARD.STEP.ASSIGNEE:
return <AssigneeStep policy={policy} />;
return (
<AssigneeStep
policy={policy}
feed={feed}
/>
);
case CONST.COMPANY_CARD.STEP.CARD:
return (
<CardSelectionStep
Expand All @@ -52,7 +57,12 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
/>
);
default:
return <AssigneeStep policy={policy} />;
return (
<AssigneeStep
policy={policy}
feed={feed}
/>
);
}
}

Expand Down
14 changes: 10 additions & 4 deletions src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<OnyxTypes.Policy>;

/** 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;

Expand Down Expand Up @@ -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,
});
Expand Down
30 changes: 6 additions & 24 deletions src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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: (
<Icon
src={CardUtils.getCardFeedIcon(feed)}
height={variables.cardIconHeight}
width={variables.cardIconWidth}
additionalStyles={[styles.mr3, styles.cardIcon]}
/>
),
}));

const submit = () => {
if (!cardSelected) {
setShouldShowError(true);
Expand All @@ -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,
});
};
Expand All @@ -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 (
<InteractiveStepWrapper
Expand All @@ -127,7 +109,7 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) {
headerTitle={translate('workspace.companyCards.assignCard')}
headerSubtitle={assigneeDisplayName}
>
{!listOptions.length ? (
{!cardListOptions.length ? (
<View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsCenter, styles.ph5, styles.mb9]}>
<Icon
src={Illustrations.BrokenMagnifyingGlass}
Expand All @@ -150,7 +132,7 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) {
<>
<SelectionList
sections={[{data: searchedListOptions}]}
shouldShowTextInput={listOptions.length > CONST.COMPANY_CARDS.CARD_LIST_THRESHOLD}
shouldShowTextInput={cardListOptions.length > CONST.COMPANY_CARDS.CARD_LIST_THRESHOLD}
textInputLabel={translate('common.search')}
textInputValue={searchText}
onChangeText={setSearchText}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
103 changes: 102 additions & 1 deletion tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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({});
});
});
});

0 comments on commit 1a68638

Please sign in to comment.