Skip to content

Commit 42eb490

Browse files
authored
Merge pull request Expensify#80636 from Expensify/mario-createCsvCompanyCards
[Payment due @Krishna2323] [Sprint] [CSV Card Import] Add Company Cards creation flow
2 parents 1314980 + 6978246 commit 42eb490

43 files changed

Lines changed: 1230 additions & 55 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

src/CONST/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,7 @@ const CONST = {
11681168
'https://help.expensify.com/articles/new-expensify/connect-credit-cards/company-cards/Commercial-feeds#how-to-set-up-an-american-express-corporate-feed',
11691169
COMPANY_CARDS_STRIPE_HELP: 'https://dashboard.stripe.com/login?redirect=%2Fexpenses%2Fsettings',
11701170
COMPANY_CARDS_CONNECT_CREDIT_CARDS_HELP_URL: 'https://help.expensify.com/new-expensify/hubs/connect-credit-cards/',
1171+
COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL: 'https://help.expensify.com/articles/new-expensify/connect-credit-cards/Company-Card-Settings',
11711172
CUSTOM_REPORT_NAME_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports#formulas',
11721173
CONFIGURE_REIMBURSEMENT_SETTINGS_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/workspaces/Configure-Reimbursement-Settings',
11731174
CONFIGURE_EXPENSE_REPORT_RULES_HELP_URL: 'https://help.expensify.com/articles/new-expensify/workspaces/Set-up-rules#configure-expense-report-rules',
@@ -4019,6 +4020,7 @@ const CONST = {
40194020
PLAID_CONNECTION: 'PlaidConnection',
40204021
SELECT_STATEMENT_CLOSE_DATE: 'SelectStatementCloseDate',
40214022
SELECT_DIRECT_STATEMENT_CLOSE_DATE: 'SelectDirectStatementCloseDate',
4023+
IMPORT_FROM_FILE: 'ImportFromFile',
40224024
},
40234025
CARD_TYPE: {
40244026
AMEX: 'amex',
@@ -4050,6 +4052,7 @@ const CONST = {
40504052
WELLS_FARGO: 'Wells Fargo',
40514053
MOCK_BANK: 'Mock Bank',
40524054
OTHER: 'Other',
4055+
FILE_IMPORT: 'Import transactions from file',
40534056
},
40544057
NON_CONNECTABLE_BANKS: {
40554058
PEX: 'PEX',
@@ -8293,6 +8296,15 @@ const CONST = {
82938296
DATE: 'date',
82948297
MERCHANT: 'merchant',
82958298
TRANSACTION_FIELDS: ['date', 'merchant', 'amount', 'category'] as const,
8299+
CARD_NUMBER: 'cardNumber',
8300+
POSTED_DATE: 'postedDate',
8301+
TAG: 'tag',
8302+
COMMENT: 'comment',
8303+
ORIGINAL_TRANSACTION_DATE: 'originalTransactionDate',
8304+
ORIGINAL_AMOUNT: 'originalAmount',
8305+
ORIGINAL_CURRENCY: 'originalCurrency',
8306+
UNIQUE_ID: 'uniqueID',
8307+
EXTERNAL_ID: 'externalID',
82968308
},
82978309

82988310
IMPORT_SPREADSHEET: {

src/ONYXKEYS.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,8 @@ const ONYXKEYS = {
987987
ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft',
988988
ADD_NEW_CARD_FEED_FORM: 'addNewCardFeed',
989989
ADD_NEW_CARD_FEED_FORM_DRAFT: 'addNewCardFeedDraft',
990+
COMPANY_CARD_LAYOUT_NAME_FORM: 'companyCardLayoutNameForm',
991+
COMPANY_CARD_LAYOUT_NAME_FORM_DRAFT: 'companyCardLayoutNameFormDraft',
990992
ASSIGN_CARD_FORM: 'assignCard',
991993
ASSIGN_CARD_FORM_DRAFT: 'assignCardDraft',
992994
EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName',
@@ -1165,6 +1167,7 @@ type OnyxFormValuesMapping = {
11651167
[ONYXKEYS.FORMS.SUBSCRIPTION_EXPENSIFY_CODE_FORM]: FormTypes.SubscriptionExpensifyCodeForm;
11661168
[ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm;
11671169
[ONYXKEYS.FORMS.ADD_NEW_CARD_FEED_FORM]: FormTypes.AddNewCardFeedForm;
1170+
[ONYXKEYS.FORMS.COMPANY_CARD_LAYOUT_NAME_FORM]: FormTypes.CompanyCardLayoutNameForm;
11681171
[ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm;
11691172
[ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm;
11701173
[ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm;

src/ROUTES.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2528,6 +2528,18 @@ const ROUTES = {
25282528
// eslint-disable-next-line no-restricted-syntax -- Legacy route generation
25292529
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/add-card-feed`, backTo),
25302530
},
2531+
WORKSPACE_COMPANY_CARDS_IMPORT_SPREADSHEET: {
2532+
route: 'workspaces/:policyID/company-cards/add-card-feed/import',
2533+
getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/add-card-feed/import` as const,
2534+
},
2535+
WORKSPACE_COMPANY_CARDS_IMPORTED: {
2536+
route: 'workspaces/:policyID/company-cards/add-card-feed/import/mapping',
2537+
getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/add-card-feed/import/mapping` as const,
2538+
},
2539+
WORKSPACE_COMPANY_CARDS_LAYOUT_NAME: {
2540+
route: 'workspaces/:policyID/company-cards/add-card-feed/layout-name',
2541+
getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/add-card-feed/layout-name` as const,
2542+
},
25312543
WORKSPACE_COMPANY_CARDS_SELECT_FEED: {
25322544
route: 'workspaces/:policyID/company-cards/select-feed',
25332545
getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/select-feed` as const,

src/SCREENS.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,10 +670,13 @@ const SCREENS = {
670670
COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed',
671671
COMPANY_CARDS_BANK_CONNECTION: 'Workspace_CompanyCards_BankConnection',
672672
COMPANY_CARDS_ADD_NEW: 'Workspace_CompanyCards_New',
673+
COMPANY_CARDS_IMPORT_SPREADSHEET: 'Workspace_CompanyCards_Import_Spreadsheet',
674+
COMPANY_CARDS_IMPORTED: 'Workspace_CompanyCards_Imported',
673675
COMPANY_CARDS_TYPE: 'Workspace_CompanyCards_Type',
674676
COMPANY_CARDS_INSTRUCTIONS: 'Workspace_CompanyCards_Instructions',
675677
COMPANY_CARDS_NAME: 'Workspace_CompanyCards_Name',
676678
COMPANY_CARDS_DETAILS: 'Workspace_CompanyCards_Details',
679+
COMPANY_CARDS_LAYOUT_NAME: 'Workspace_CompanyCards_Layout_Name',
677680
COMPANY_CARDS_SETTINGS: 'Workspace_CompanyCards_Settings',
678681
COMPANY_CARDS_SETTINGS_FEED_NAME: 'Workspace_CompanyCards_Settings_Feed_Name',
679682
COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE: 'Workspace_CompanyCards_Settings_Statement_Close_Date',

src/components/ImportColumn.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {DropdownOption} from './ButtonWithDropdownMenu/types';
1212
import Text from './Text';
1313

1414
// cspell:disable
15-
function findColumnName(header: string): string {
15+
function findColumnName(header: string, columnRoles?: ColumnRole[]): string {
1616
let attribute = '';
1717
const formattedHeader = Str.removeSpaces(String(header).toLowerCase().trim());
1818
switch (formattedHeader) {
@@ -96,13 +96,30 @@ function findColumnName(header: string): string {
9696
break;
9797

9898
case 'amount':
99+
case 'postedamount':
100+
case 'posted_amount':
99101
attribute = CONST.CSV_IMPORT_COLUMNS.AMOUNT;
100102
break;
101103

104+
case 'cardnumber':
105+
case 'card':
106+
case 'number':
107+
attribute = CONST.CSV_IMPORT_COLUMNS.CARD_NUMBER;
108+
break;
109+
102110
case 'currency':
111+
case 'postedcurrency':
112+
case 'posted_currency':
103113
attribute = CONST.CSV_IMPORT_COLUMNS.CURRENCY;
104114
break;
105115

116+
case 'posteddate':
117+
case 'posted_date':
118+
case 'postingdate':
119+
case 'posting_date':
120+
attribute = CONST.CSV_IMPORT_COLUMNS.POSTED_DATE;
121+
break;
122+
106123
case 'date':
107124
case 'transactiondate':
108125
case 'transaction_date':
@@ -129,6 +146,19 @@ function findColumnName(header: string): string {
129146
break;
130147
}
131148

149+
// If the detected attribute isn't available in the current context but a semantic equivalent is,
150+
// remap to it. This handles e.g. "Date" headers in company card imports where DATE is not a
151+
// valid column role but POSTED_DATE is.
152+
if (columnRoles && attribute) {
153+
const isAvailable = columnRoles.some((role) => role.value === attribute);
154+
if (!isAvailable) {
155+
if (attribute === CONST.CSV_IMPORT_COLUMNS.DATE && columnRoles.some((role) => role.value === CONST.CSV_IMPORT_COLUMNS.POSTED_DATE)) {
156+
return CONST.CSV_IMPORT_COLUMNS.POSTED_DATE;
157+
}
158+
return '';
159+
}
160+
}
161+
132162
return attribute;
133163
}
134164
// cspell:enable
@@ -183,7 +213,7 @@ function ImportColumn({column, columnName, columnRoles, columnIndex, shouldShowD
183213
const currentColumnValue = spreadsheet?.columns?.[columnIndex];
184214
// Treat 'ignore' as unmapped so auto-detection can still run
185215
const isMapped = currentColumnValue && currentColumnValue !== CONST.CSV_IMPORT_COLUMNS.IGNORE;
186-
const autoDetectedColName = isMapped ? '' : findColumnName(column.at(0) ?? '');
216+
const autoDetectedColName = isMapped ? '' : findColumnName(column.at(0) ?? '', columnRoles);
187217

188218
const foundIndex = columnRoles?.findIndex((item) => item.value === (currentColumnValue ?? autoDetectedColName)) ?? -1;
189219
const selectedIndex = foundIndex !== -1 ? foundIndex : 0;

src/hooks/useFeedKeysWithAssignedCards.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import {buildFeedKeysWithAssignedCards} from '@selectors/Card';
2+
import {useCallback} from 'react';
3+
import type {OnyxCollection} from 'react-native-onyx';
24
import ONYXKEYS from '@src/ONYXKEYS';
5+
import type {WorkspaceCardsList} from '@src/types/onyx';
36
import useOnyx from './useOnyx';
47

58
type FeedKeysWithAssignedCards = Record<string, true>;
69

710
function useFeedKeysWithAssignedCards(): FeedKeysWithAssignedCards | undefined {
8-
const [feedKeysWithCards] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {selector: buildFeedKeysWithAssignedCards});
11+
const [betas] = useOnyx(ONYXKEYS.BETAS);
12+
const feedKeysWithCardsSelector = useCallback((allWorkspaceCards: OnyxCollection<WorkspaceCardsList>) => buildFeedKeysWithAssignedCards(allWorkspaceCards, betas), [betas]);
13+
const [feedKeysWithCards] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {
14+
selector: feedKeysWithCardsSelector,
15+
});
916

1017
return feedKeysWithCards;
1118
}

src/languages/de.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,8 @@ const translations: TranslationDeepObject<typeof en> = {
10891089
emptyMappedField: (fieldName: string) => `Ups! Das Feld („${fieldName}“) enthält einen oder mehrere leere Werte. Bitte überprüfe es und versuche es erneut.`,
10901090
importSuccessfulTitle: 'Import erfolgreich',
10911091
importCategoriesSuccessfulDescription: ({categories}: {categories: number}) => (categories > 1 ? `${categories} Kategorien wurden hinzugefügt.` : '1 Kategorie wurde hinzugefügt.'),
1092+
importCompanyCardTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
1093+
transactions > 1 ? `${transactions} Transaktionen wurden hinzugefügt.` : '1 Transaktion wurde hinzugefügt.',
10921094
importMembersSuccessfulDescription: ({added, updated}: {added: number; updated: number}) => {
10931095
if (!added && !updated) {
10941096
return 'Es wurden keine Mitglieder hinzugefügt oder aktualisiert.';
@@ -5041,6 +5043,11 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
50415043
},
50425044
addNewCard: {
50435045
other: 'Sonstiges',
5046+
fileImport: 'Transaktionen aus Datei importieren',
5047+
createFileFeedHelpText: `<muted-text>Bitte folge dieser <a href="${CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL}">Hilfsanleitung</a>, um die Ausgaben deiner Firmenkarte zu importieren!</muted-text>`,
5048+
companyCardLayoutName: 'Name des Firmenkarten-Layouts',
5049+
cardLayoutNameRequired: 'Der Name des Firmenkarten-Layouts ist erforderlich',
5050+
useAdvancedFields: 'Erweiterte Felder verwenden (nicht empfohlen)',
50445051
cardProviders: {
50455052
gl1025: 'American Express Corporate Cards',
50465053
cdf: 'Mastercard Firmenkarten',
@@ -5121,6 +5128,25 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
51215128
confirmText: 'Problem melden',
51225129
cancelText: 'Überspringen',
51235130
},
5131+
csvColumns: {
5132+
cardNumber: 'Kartennummer',
5133+
postedDate: 'Datum',
5134+
merchant: 'Händler',
5135+
amount: 'Betrag',
5136+
currency: 'Währung',
5137+
ignore: 'Ignorieren',
5138+
originalTransactionDate: 'Ursprüngliches Transaktionsdatum',
5139+
originalAmount: 'Ursprünglicher Betrag',
5140+
originalCurrency: 'Ursprüngliche Währung',
5141+
comment: 'Kommentar',
5142+
category: 'Kategorie',
5143+
tag: 'Tag',
5144+
},
5145+
csvErrors: {
5146+
requiredColumns: (missingColumns: string) => `Bitte weisen Sie jeder der folgenden Eigenschaften eine Spalte zu: ${missingColumns}.`,
5147+
duplicateColumns: (duplicateColumn: string) =>
5148+
`Ups! Du hast ein einzelnes Feld („${duplicateColumn}“) mehreren Spalten zugeordnet. Bitte überprüfe die Zuordnung und versuche es erneut.`,
5149+
},
51245150
},
51255151
statementCloseDate: {
51265152
[CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Letzter Tag des Monats',

src/languages/en.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,8 @@ const translations = {
11391139
emptyMappedField: (fieldName: string) => `Oops! The field ("${fieldName}") contains one or more empty values. Please review and try again.`,
11401140
importSuccessfulTitle: 'Import successful',
11411141
importCategoriesSuccessfulDescription: ({categories}: {categories: number}) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'),
1142+
importCompanyCardTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
1143+
transactions > 1 ? `${transactions} transactions have been added.` : '1 transaction has been added.',
11421144
importMembersSuccessfulDescription: ({added, updated}: {added: number; updated: number}) => {
11431145
if (!added && !updated) {
11441146
return 'No members have been added or updated.';
@@ -5062,6 +5064,11 @@ const translations = {
50625064
},
50635065
addNewCard: {
50645066
other: 'Other',
5067+
fileImport: 'Import transactions from file',
5068+
createFileFeedHelpText: `<muted-text>Please follow this <a href="${CONST.COMPANY_CARDS_CREATE_FILE_FEED_HELP_URL}">help guide</a> to get your company card expenses imported!</muted-text>`,
5069+
companyCardLayoutName: 'Company card layout name',
5070+
cardLayoutNameRequired: 'The Company card layout name is required',
5071+
useAdvancedFields: 'Use advanced fields (not recommended)',
50655072
cardProviders: {
50665073
gl1025: 'American Express Corporate Cards',
50675074
cdf: 'Mastercard Commercial Cards',
@@ -5123,6 +5130,24 @@ const translations = {
51235130
confirmText: 'Report issue',
51245131
cancelText: 'Skip',
51255132
},
5133+
csvColumns: {
5134+
cardNumber: 'Card number',
5135+
postedDate: 'Date',
5136+
merchant: 'Merchant',
5137+
amount: 'Amount',
5138+
currency: 'Currency',
5139+
ignore: 'Ignore',
5140+
originalTransactionDate: 'Original transaction date',
5141+
originalAmount: 'Original amount',
5142+
originalCurrency: 'Original currency',
5143+
comment: 'Comment',
5144+
category: 'Category',
5145+
tag: 'Tag',
5146+
},
5147+
csvErrors: {
5148+
requiredColumns: (missingColumns: string) => `Please assign a column to each of the attributes: ${missingColumns}.`,
5149+
duplicateColumns: (duplicateColumn: string) => `Oops! You've mapped a single field ("${duplicateColumn}") to multiple columns. Please review and try again.`,
5150+
},
51265151
},
51275152
statementCloseDate: {
51285153
[CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH]: 'Last day of the month',

0 commit comments

Comments
 (0)