diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 31d9dbe58337..4455d2ab6087 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -870,6 +870,24 @@ function createPolicyCategory({ API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData); } +/** + * Creates multiple enabled workspace categories at once. Used by flows (such as the merchant rule + * spreadsheet import) that reference categories which may not yet exist on the policy, so a + * referenced category is added to the workspace instead of being silently dropped. + */ +function createPolicyCategories(policyID: string, categoryNames: string[]) { + if (categoryNames.length === 0) { + return; + } + const onyxData = buildOptimisticPolicyCategories(policyID, categoryNames); + const parameters = { + policyID, + categories: JSON.stringify(categoryNames.map((name) => ({name}))), + }; + + API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData); +} + async function importPolicyCategories(policyID: string, categories: PolicyCategory[], existingCategories?: OnyxEntry): Promise { const policyCategories = existingCategories ?? {}; const seenNames = new Set(); @@ -1904,6 +1922,7 @@ export { isDefaultMccGroupID, clearCategoryErrors, createPolicyCategory, + createPolicyCategories, deleteWorkspaceCategories, downloadCategoriesCSV, enablePolicyCategories, diff --git a/src/libs/actions/Policy/Rules.ts b/src/libs/actions/Policy/Rules.ts index 7dde67ddfb7f..3e9d5056e3fc 100644 --- a/src/libs/actions/Policy/Rules.ts +++ b/src/libs/actions/Policy/Rules.ts @@ -15,12 +15,15 @@ import type {MerchantRuleForm} from '@src/types/form'; import type {ImportFinalModal} from '@src/types/onyx/ImportedSpreadsheet'; import type Policy from '@src/types/onyx/Policy'; import type {AgentRule, CodingRule, CodingRuleFilter, CodingRuleTax} from '@src/types/onyx/Policy'; +import type {PolicyCategories} from '@src/types/onyx/PolicyCategory'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import {createPolicyCategories} from './Category'; + /** A coding rule parsed from an imported spreadsheet row, keyed by a client-generated ruleID */ type ImportedMerchantRule = Omit; @@ -240,13 +243,25 @@ function setPolicyCodingRule(policyID: string, form: MerchantRuleForm, policy: P * Imports coding rules parsed from a spreadsheet into the given policy in bulk * @param policyID - The ID of the policy to import the rules into * @param rules - Coding rule values keyed by client-generated ruleID + * @param policyCategories - The policy's existing categories, used to auto-create any imported category that doesn't exist yet */ -async function importMerchantRulesSpreadsheet(policyID: string, rules: Record): Promise { +async function importMerchantRulesSpreadsheet(policyID: string, rules: Record, policyCategories: OnyxEntry): Promise { // The API rejects an empty rules object, so fail fast when the spreadsheet produced no importable rules if (Object.keys(rules).length === 0) { return getImportFailedFinalModal(); } + // A rule only applies its category to matching expenses if that category exists on the policy, so auto-create any + // imported category that isn't already in the workspace instead of silently dropping it. + const missingCategories = [ + ...new Set( + Object.values(rules) + .map((rule) => rule.category?.trim()) + .filter((category): category is string => !!category && !policyCategories?.[category]), + ), + ]; + createPolicyCategories(policyID, missingCategories); + const importFinalModal: ImportFinalModal = { titleKey: 'spreadsheet.importSuccessfulTitle', promptKey: 'spreadsheet.importMerchantRulesSuccessfulDescription', diff --git a/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx b/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx index ef39b9e92432..b23dcb25a926 100644 --- a/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx +++ b/src/pages/workspace/rules/MerchantRules/ImportedMerchantRulesPage.tsx @@ -77,6 +77,7 @@ type ImportedMerchantRulesPageProps = PlatformStackScreenProps 0 ? {titleKey: 'spreadsheet.importSuccessfulTitle', promptKey: 'spreadsheet.importMerchantRulesSuccessfulDescription', promptKeyParams: {rules: 0}} - : await importMerchantRulesSpreadsheet(policyID, rules); + : await importMerchantRulesSpreadsheet(policyID, rules, policyCategories); const didShowImportFinalModal = await showImportSpreadsheetConfirmModal(importFinalModal, {shouldHandleNavigationBack: false}); if (!didShowImportFinalModal) { setIsImportingRules(false);