Skip to content

Commit ee4863b

Browse files
authored
Merge pull request #96477 from Expensify/alberto-importFix
[Payment due @bernhardoj] Allow optional domain parameter when uploading cards via CSV
2 parents 0a0e504 + 7a72b32 commit ee4863b

6 files changed

Lines changed: 126 additions & 7 deletions

File tree

‎src/libs/API/parameters/ImportCSVCompanyCardsParams.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ type ImportCSVCompanyCardsParams = {
77

88
/** Stringified JSON array of imported row data */
99
csvData: string;
10+
11+
/** Account that owns the feed */
12+
domainAccountID?: number;
1013
};
1114

1215
export default ImportCSVCompanyCardsParams;

‎src/libs/actions/CompanyCards.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ type AddNewCompanyCardFlowData = {
6767

6868
type ImportCSVCompanyCardsData = {
6969
policyID: string;
70-
workspaceAccountID: number;
70+
domainAccountID: number;
7171
layoutName: string;
7272
layoutType: string;
7373
columnMappings: string[];
@@ -1236,7 +1236,7 @@ function setFeedStatementPeriodEndDay(
12361236

12371237
function importCSVCompanyCards({
12381238
policyID,
1239-
workspaceAccountID,
1239+
domainAccountID,
12401240
layoutName,
12411241
layoutType,
12421242
columnMappings,
@@ -1260,9 +1260,10 @@ function importCSVCompanyCards({
12601260
layoutType,
12611261
}),
12621262
csvData: JSON.stringify(csvDataWithGeneratedIDs),
1263+
domainAccountID,
12631264
};
12641265

1265-
const feedNameWithDomainID = getCardFeedWithDomainID(feedName, workspaceAccountID);
1266+
const feedNameWithDomainID = getCardFeedWithDomainID(feedName, domainAccountID);
12661267
const existingCompanyCards = workspaceCardFeeds?.settings?.companyCards ?? {};
12671268
const existingNicknames = workspaceCardFeeds?.settings?.companyCardNicknames ?? {};
12681269
const shouldCreateFeed = !existingCompanyCards?.[feedName];
@@ -1303,7 +1304,7 @@ function importCSVCompanyCards({
13031304
if (shouldCreateFeed || shouldSetNickname) {
13041305
optimisticData.push({
13051306
onyxMethod: Onyx.METHOD.MERGE,
1306-
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
1307+
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
13071308
value: {
13081309
settings: {
13091310
...(shouldCreateFeed
@@ -1329,7 +1330,7 @@ function importCSVCompanyCards({
13291330

13301331
failureData.push({
13311332
onyxMethod: Onyx.METHOD.MERGE,
1332-
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
1333+
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
13331334
value: {
13341335
settings: {
13351336
...(shouldCreateFeed ? {companyCards: {[feedName]: null}} : {}),

‎src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ function WorkspaceCompanyCardsSettingsPage({
206206
companyCardLayoutName: selectedFeedData?.customFeedName ?? feedName ?? '',
207207
useAdvancedFields: hadAdvancedFields,
208208
existingInstanceID: selectedFeedData?.uploadLayoutSettings?.instanceID ?? null,
209+
domainAccountID: domainOrWorkspaceAccountID,
209210
},
210211
});
211212
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_IMPORT_SPREADSHEET.getRoute(policyID));

‎src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ function CompanyCardsImportedPage({route}: CompanyCardsImportedPageProps) {
4242
const policyID = route.params.policyID;
4343
const policy = usePolicy(policyID);
4444
const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID;
45+
const feedDomainAccountID = addNewCard?.data?.domainAccountID ?? workspaceAccountID;
4546
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
46-
const [workspaceCardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
47+
const [workspaceCardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${feedDomainAccountID}`);
4748
const [isImportingTransactions, setIsImportingTransactions] = useState(false);
4849
const {setIsClosing} = useCloseImportPage();
4950
const showImportSpreadsheetConfirmModal = useImportSpreadsheetConfirmModal();
@@ -205,7 +206,7 @@ function CompanyCardsImportedPage({route}: CompanyCardsImportedPageProps) {
205206
setIsImportingTransactions(true);
206207
const importFinalModal = await importCSVCompanyCards({
207208
policyID,
208-
workspaceAccountID,
209+
domainAccountID: feedDomainAccountID,
209210
layoutName,
210211
layoutType,
211212
columnMappings,

‎src/types/onyx/CardFeeds.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ type AddNewCardFeedData = {
324324
/** Existing instance ID when editing a CSV feed */
325325
existingInstanceID?: string;
326326

327+
/** Account that owns the CSV feed being edited */
328+
domainAccountID?: number;
329+
327330
/** Plaid accounts */
328331
plaidAccounts?: LinkAccount[] | PlaidAccount[];
329332
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {importCSVCompanyCards} from '@libs/actions/CompanyCards';
2+
import {WRITE_COMMANDS} from '@libs/API/types';
3+
4+
import CONST from '@src/CONST';
5+
import IntlStore from '@src/languages/IntlStore';
6+
import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
7+
import ONYXKEYS from '@src/ONYXKEYS';
8+
import type {CardFeeds} from '@src/types/onyx';
9+
10+
import Onyx from 'react-native-onyx';
11+
12+
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
13+
14+
const POLICY_ID = 'POLICY_1';
15+
const DOMAIN_ACCOUNT_ID = 777;
16+
const CSV_FEED = CONST.COMPANY_CARD.FEED_BANK_NAME.CSV;
17+
18+
OnyxUpdateManager();
19+
describe('actions/CompanyCards importCSVCompanyCards', () => {
20+
beforeAll(() => {
21+
Onyx.init({keys: ONYXKEYS});
22+
});
23+
24+
beforeEach(() => {
25+
IntlStore.load(CONST.LOCALES.EN);
26+
return Onyx.clear().then(waitForBatchedUpdates);
27+
});
28+
29+
it('targets the feed-owning domain account when re-importing a domain feed surfaced via a preferred workspace', () => {
30+
// Given a domain feed (its NVPs live on the +@domain account, not the workspace account) that is re-imported
31+
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
32+
33+
// When importing with an explicit domainAccountID and no cached feeds for that account
34+
importCSVCompanyCards({
35+
policyID: POLICY_ID,
36+
domainAccountID: DOMAIN_ACCOUNT_ID,
37+
layoutName: 'My Layout',
38+
layoutType: CSV_FEED,
39+
columnMappings: ['merchant', 'amount'],
40+
csvData: [
41+
['merchant', 'amount'],
42+
['Coffee Shop', '-5.00'],
43+
],
44+
existingInstanceID: 'domain-instance',
45+
workspaceCardFeeds: undefined,
46+
});
47+
48+
// Then the domainAccountID is forwarded to the backend so the existing feed is updated in place, and the
49+
// optimistic Onyx writes target the domain account rather than the workspace account.
50+
expect(apiWriteSpy).toHaveBeenCalledWith(
51+
WRITE_COMMANDS.IMPORT_CSV_COMPANY_CARDS,
52+
expect.objectContaining({
53+
policyID: POLICY_ID,
54+
domainAccountID: DOMAIN_ACCOUNT_ID,
55+
}),
56+
expect.objectContaining({
57+
optimisticData: expect.arrayContaining([
58+
expect.objectContaining({
59+
key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${POLICY_ID}`,
60+
value: `${CSV_FEED}${CONST.COMPANY_CARD.FEED_KEY_SEPARATOR}${DOMAIN_ACCOUNT_ID}`,
61+
}),
62+
expect.objectContaining({
63+
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${DOMAIN_ACCOUNT_ID}`,
64+
}),
65+
]),
66+
}),
67+
);
68+
69+
apiWriteSpy.mockRestore();
70+
});
71+
72+
it('does not optimistically create the feed when it already exists on the target account', () => {
73+
// Given the target account already has the feed and a nickname for it
74+
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
75+
76+
const existingFeeds: CardFeeds = {
77+
settings: {
78+
companyCards: {[CSV_FEED]: {pending: false}},
79+
companyCardNicknames: {[CSV_FEED]: 'Existing Layout'},
80+
},
81+
};
82+
83+
// When re-importing into that existing feed
84+
importCSVCompanyCards({
85+
policyID: POLICY_ID,
86+
domainAccountID: DOMAIN_ACCOUNT_ID,
87+
layoutName: 'Existing Layout',
88+
layoutType: CSV_FEED,
89+
columnMappings: ['merchant', 'amount'],
90+
csvData: [['merchant', 'amount']],
91+
existingInstanceID: 'domain-instance',
92+
workspaceCardFeeds: existingFeeds,
93+
});
94+
95+
// Then only the last-selected-feed is updated optimistically; no domain_member feed stub is created
96+
expect(apiWriteSpy).toHaveBeenCalledWith(
97+
WRITE_COMMANDS.IMPORT_CSV_COMPANY_CARDS,
98+
expect.objectContaining({policyID: POLICY_ID, domainAccountID: DOMAIN_ACCOUNT_ID}),
99+
expect.objectContaining({
100+
optimisticData: [
101+
expect.objectContaining({
102+
key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${POLICY_ID}`,
103+
}),
104+
],
105+
}),
106+
);
107+
108+
apiWriteSpy.mockRestore();
109+
});
110+
});

0 commit comments

Comments
 (0)