Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[InternalQA] fix: always send feedType with the order number for custom feeds #55223

Merged
merged 9 commits into from
Jan 16, 2025
Merged
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
25 changes: 24 additions & 1 deletion src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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, DirectCardFeedData} from '@src/types/onyx/CardFeeds';
import type {CompanyCardFeedWithNumber, 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 @@ -455,6 +455,28 @@ const getDescriptionForPolicyDomainCard = (domainName: string): string => {
return domainName;
};

const CUSTOM_FEEDS = [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX];

function getFeedType(feedKey: CompanyCardFeed, cardFeeds: OnyxEntry<CardFeeds>): CompanyCardFeedWithNumber {
if (CUSTOM_FEEDS.some((feed) => feed === feedKey)) {
const filteredFeeds = Object.keys(cardFeeds?.settings?.companyCards ?? {}).filter((str) => str.includes(feedKey));

const feedNumbers = filteredFeeds.map((str) => parseInt(str.replace(feedKey, ''), 10)).filter(Boolean);
feedNumbers.sort((a, b) => a - b);

let firstAvailableNumber = 1;
for (const num of feedNumbers) {
if (num && num !== firstAvailableNumber) {
return `${feedKey}${firstAvailableNumber}`;
}
firstAvailableNumber++;
}

return `${feedKey}${firstAvailableNumber}`;
}
return feedKey;
}

export {
isExpensifyCard,
isCorporateCard,
Expand Down Expand Up @@ -490,4 +512,5 @@ export {
getAllCardsForWorkspace,
isCardIssued,
isCardHiddenFromSearch,
getFeedType,
};
35 changes: 32 additions & 3 deletions src/libs/actions/CompanyCards.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
import type {
AssignCompanyCardParams,
Expand All @@ -18,7 +18,7 @@ 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} 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';
Expand Down Expand Up @@ -53,19 +53,36 @@ function clearAddNewCardFlow() {
});
}

function addNewCompanyCardsFeed(policyID: string, feedType: CompanyCardFeed, feedDetails: string, lastSelectedFeed?: CompanyCardFeed) {
function addNewCompanyCardsFeed(policyID: string, cardFeed: CompanyCardFeed, feedDetails: string, cardFeeds: OnyxEntry<CardFeeds>, lastSelectedFeed?: CompanyCardFeed) {
const authToken = NetworkStore.getAuthToken();
const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID);

if (!authToken) {
return;
}

const feedType = CardUtils.getFeedType(cardFeed, cardFeeds);

const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`,
value: feedType,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
isLoading: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only adding isLoading: true in optimisticData / failureData without changing it back to false led to the following issue:

which we fixed by adding isLoading: false in the finallyData.

settings: {
companyCards: {
[feedType]: {
errors: null,
},
},
},
},
},
];

const failureData: OnyxUpdate[] = [
Expand All @@ -74,6 +91,18 @@ function addNewCompanyCardsFeed(policyID: string, feedType: CompanyCardFeed, fee
key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`,
value: lastSelectedFeed ?? null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
isLoading: true,
settings: {
companyCards: {
[feedType]: null,
},
},
},
},
];

const successData: OnyxUpdate[] = [
Expand Down
18 changes: 12 additions & 6 deletions src/pages/workspace/companyCards/addNew/DetailsStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ValidationUtils from '@libs/ValidationUtils';
import {getWorkspaceAccountID} from '@libs/PolicyUtils';
import {getFieldRequiredErrors} from '@libs/ValidationUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import * as CompanyCards from '@userActions/CompanyCards';
import {addNewCompanyCardsFeed, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand All @@ -34,8 +35,13 @@ function DetailsStep({policyID}: DetailsStepProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {inputCallbackRef} = useAutoFocusInput();

const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);

const workspaceAccountID = getWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);

const feedProvider = addNewCard?.data?.feedType;
const isStripeFeedProvider = feedProvider === CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE;
const bank = addNewCard?.data?.selectedBank;
Expand All @@ -53,21 +59,21 @@ function DetailsStep({policyID}: DetailsStepProps) {
.map(([key, value]) => `${key}: ${value}`)
.join(', ');

CompanyCards.addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, feedDetails, lastSelectedFeed);
addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, feedDetails, cardFeeds, lastSelectedFeed);
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID));
};

const handleBackButtonPress = () => {
if (isOtherBankSelected) {
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_NAME});
setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_NAME});
return;
}
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS});
setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS});
};

const validate = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.ADD_NEW_CARD_FEED_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.ADD_NEW_CARD_FEED_FORM> => {
const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.BANK_ID]);
const errors = getFieldRequiredErrors(values, [INPUT_IDS.BANK_ID]);

switch (feedProvider) {
case CONST.COMPANY_CARD.FEED_BANK_NAME.VISA:
Expand Down
4 changes: 4 additions & 0 deletions src/types/onyx/CardFeeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type * as OnyxCommon from './OnyxCommon';
/** Card feed */
type CompanyCardFeed = ValueOf<typeof CONST.COMPANY_CARD.FEED_BANK_NAME>;

/** Custom card feed with a number */
type CompanyCardFeedWithNumber = CompanyCardFeed | `${CompanyCardFeed}${number}`;

/** Card feed provider */
type CardFeedProvider =
| typeof CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD
Expand Down Expand Up @@ -135,4 +138,5 @@ export type {
CardFeedProvider,
CompanyFeeds,
CompanyCardNicknames,
CompanyCardFeedWithNumber,
};
63 changes: 59 additions & 4 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ const companyCardsCustomFeedSettings = {
liabilityType: 'personal',
},
};
const companyCardsCustomFeedSettingsWithNumbers = {
[`${CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD}1`]: {
pending: true,
},
[`${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}1`]: {
liabilityType: 'personal',
},
};
const companyCardsCustomVisaFeedSettingsWithNumbers = {
[`${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}1`]: {
pending: false,
},
[`${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}3`]: {
pending: false,
},
};
const companyCardsCustomFeedSettingsWithoutExpensifyBank = {
[CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: {
pending: true,
Expand Down Expand Up @@ -158,6 +174,18 @@ const cardFeedsCollection: OnyxCollection<OnyxTypes.CardFeeds> = {
companyCards: companyCardsCustomFeedSettings,
},
},
// Policy with custom feeds only, feed names with numbers
FAKE_ID_4: {
settings: {
companyCards: companyCardsCustomFeedSettingsWithNumbers,
},
},
// Policy with several Visa feeds
FAKE_ID_5: {
settings: {
companyCards: companyCardsCustomVisaFeedSettingsWithNumbers,
},
},
};

describe('CardUtils', () => {
Expand Down Expand Up @@ -355,8 +383,12 @@ describe('CardUtils', () => {
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'});
expect(cardsList).toStrictEqual({
// eslint-disable-next-line @typescript-eslint/naming-convention
'480801XXXXXX2111': 'ENCRYPTED_CARD_NUMBER',
// eslint-disable-next-line @typescript-eslint/naming-convention
'480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER',
});
});

it('Should return filtered direct feed cards list with a single card', () => {
Expand All @@ -367,13 +399,36 @@ describe('CardUtils', () => {

it('Should return filtered direct feed cards list with multiple cards', () => {
const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, oAuthAccountDetails[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'});
expect(cardsList).toStrictEqual({
// eslint-disable-next-line @typescript-eslint/naming-convention
'CREDIT CARD...1233': 'CREDIT CARD...1233',
// eslint-disable-next-line @typescript-eslint/naming-convention
'CREDIT CARD...3333': 'CREDIT CARD...3333',
// eslint-disable-next-line @typescript-eslint/naming-convention
'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({});
});
});

describe('getFeedType', () => {
it('should return the feed name with a consecutive number, if there is already a feed with a number', () => {
const feedType = CardUtils.getFeedType('vcf', cardFeedsCollection.FAKE_ID_4);
expect(feedType).toBe('vcf2');
});

it('should return the feed name with 1, if there is already a feed without a number', () => {
const feedType = CardUtils.getFeedType('vcf', cardFeedsCollection.FAKE_ID_3);
expect(feedType).toBe('vcf1');
});

it('should return the feed name with with the first smallest available number', () => {
const feedType = CardUtils.getFeedType('vcf', cardFeedsCollection.FAKE_ID_5);
expect(feedType).toBe('vcf2');
});
});
});
Loading