Skip to content
Closed
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
19 changes: 19 additions & 0 deletions src/libs/actions/Policy/Category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PolicyCategories>): Promise<ImportFinalModal> {
const policyCategories = existingCategories ?? {};
const seenNames = new Set<string>();
Expand Down Expand Up @@ -1904,6 +1922,7 @@ export {
isDefaultMccGroupID,
clearCategoryErrors,
createPolicyCategory,
createPolicyCategories,
deleteWorkspaceCategories,
downloadCategoriesCSV,
enablePolicyCategories,
Expand Down
19 changes: 17 additions & 2 deletions src/libs/actions/Policy/Rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodingRule, 'ruleID' | 'pendingAction' | 'errors'>;

Expand Down Expand Up @@ -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<string, ImportedMerchantRule>): Promise<ImportFinalModal> {
async function importMerchantRulesSpreadsheet(policyID: string, rules: Record<string, ImportedMerchantRule>, policyCategories: OnyxEntry<PolicyCategories>): Promise<ImportFinalModal> {
// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize category creation before importing rules

When the spreadsheet references a category that is not already on the policy, this enqueues CREATE_WORKSPACE_CATEGORIES and then immediately continues to API.makeRequestWithSideEffects for the merchant-rule import. API.write only puts the create request on the sequential write queue, while side-effect requests run right away, so the import can reach the backend before the category exists server-side; in that case the imported rule can still lose or reject the new category despite the optimistic Onyx update. Please make the import wait for the category write to finish, or have the server-side import create the missing categories atomically.

Useful? React with 👍 / 👎.


const importFinalModal: ImportFinalModal = {
titleKey: 'spreadsheet.importSuccessfulTitle',
promptKey: 'spreadsheet.importMerchantRulesSuccessfulDescription',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type ImportedMerchantRulesPageProps = PlatformStackScreenProps<SettingsNavigator
function ImportedMerchantRulesPage({route}: ImportedMerchantRulesPageProps) {
const {translate} = useLocalize();
const [spreadsheet, spreadsheetMetadata] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET);
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`);
const [isImportingRules, setIsImportingRules] = useState(false);
const {containsHeader = true} = spreadsheet ?? {};
const [isValidationEnabled, setIsValidationEnabled] = useState(false);
Expand Down Expand Up @@ -213,7 +214,7 @@ function ImportedMerchantRulesPage({route}: ImportedMerchantRulesPageProps) {
const importFinalModal: ImportFinalModal =
Object.keys(rules).length === 0 && skippedDuplicateCount > 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);
Expand Down
Loading