diff --git a/src/CONST/index.ts b/src/CONST/index.ts index aa271e2d3d3c..ed8d3f64f533 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8487,6 +8487,8 @@ const CONST = { ORIGINAL_CURRENCY: 'originalCurrency', UNIQUE_ID: 'uniqueID', EXTERNAL_ID: 'externalID', + MAX_AMOUNT_NO_RECEIPT: 'maxAmountNoReceipt', + MAX_AMOUNT_NO_ITEMIZED_RECEIPT: 'maxAmountNoItemizedReceipt', }, IMPORT_SPREADSHEET: { diff --git a/src/components/ImportColumn.tsx b/src/components/ImportColumn.tsx index 0c78acbc27a9..80aa440371c1 100644 --- a/src/components/ImportColumn.tsx +++ b/src/components/ImportColumn.tsx @@ -1,4 +1,3 @@ -import {Str} from 'expensify-common'; import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -14,7 +13,7 @@ import Text from './Text'; // cspell:disable function findColumnName(header: string, columnRoles?: ColumnRole[]): string { let attribute = ''; - const formattedHeader = Str.removeSpaces(String(header).toLowerCase().trim()); + const formattedHeader = String(header).toLowerCase().trim().replaceAll(' ', ''); switch (formattedHeader) { case 'email': case 'emailaddress': @@ -142,6 +141,19 @@ function findColumnName(header: string, columnRoles?: ColumnRole[]): string { attribute = CONST.CSV_IMPORT_COLUMNS.ENABLED; break; + case 'receiptsrequired': + case 'requirereceiptsover': + case 'maxamountnoreceipt': + attribute = CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT; + break; + + case 'itemisedreceiptrequirement': + case 'itemizedreceiptrequirement': + case 'requireitemizedreceiptsover': + case 'maxamountnoitemizedreceipt': + attribute = CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT; + break; + default: break; } diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 755d1c5d58ab..ee1ce02d9803 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -933,7 +933,12 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[], const existing = policyCategories[name]; if (!existing) { acc.added++; - } else if (existing.enabled !== category.enabled || (existing['GL Code'] ?? '') !== (category['GL Code'] ?? '')) { + } else if ( + existing.enabled !== category.enabled || + (existing['GL Code'] ?? '') !== (category['GL Code'] ?? '') || + ('maxAmountNoReceipt' in category && existing.maxAmountNoReceipt !== category.maxAmountNoReceipt) || + ('maxAmountNoItemizedReceipt' in category && existing.maxAmountNoItemizedReceipt !== category.maxAmountNoItemizedReceipt) + ) { acc.updated++; } @@ -952,6 +957,8 @@ function importPolicyCategories(policyID: string, categories: PolicyCategory[], enabled: category.enabled, // eslint-disable-next-line @typescript-eslint/naming-convention 'GL Code': String(category['GL Code']), + ...('maxAmountNoReceipt' in category && {maxAmountNoReceipt: category.maxAmountNoReceipt}), + ...('maxAmountNoItemizedReceipt' in category && {maxAmountNoItemizedReceipt: category.maxAmountNoItemizedReceipt}), })), ), }; diff --git a/src/pages/workspace/categories/ImportedCategoriesPage.tsx b/src/pages/workspace/categories/ImportedCategoriesPage.tsx index 8f3aa560efdb..3630b7b80328 100644 --- a/src/pages/workspace/categories/ImportedCategoriesPage.tsx +++ b/src/pages/workspace/categories/ImportedCategoriesPage.tsx @@ -23,6 +23,33 @@ import SCREENS from '@src/SCREENS'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +/** + * Parses a CSV cell value for receipt requirement columns. + * Mirrors the OD import logic: "required"/"always_required" → 0, + * "not_required" → DISABLED_MAX_EXPENSE_VALUE, numeric string → number. + * Returns undefined for unmapped columns, empty/default values, or invalid input. + */ +function parseCsvReceiptValue(raw: string | undefined): number | undefined { + if (raw === undefined) { + return undefined; + } + const trimmed = raw.trim().toLowerCase(); + if (!trimmed || trimmed === 'default') { + return undefined; + } + if (trimmed === 'required' || trimmed === 'always_required') { + return 0; + } + if (trimmed === 'not_required') { + return CONST.DISABLED_MAX_EXPENSE_VALUE; + } + const num = Number(trimmed); + if (Number.isFinite(num) && num >= 0) { + return num; + } + return undefined; +} + type ImportedCategoriesPageProps = { route: RouteProp; }; @@ -52,7 +79,11 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) { ); if (isControlPolicy(policy)) { - roles.push({text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE}); + roles.push( + {text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE}, + {text: translate('workspace.rules.categoryRules.requireReceiptsOver'), value: CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT}, + {text: translate('workspace.rules.categoryRules.requireItemizedReceiptsOver'), value: CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT}, + ); } return roles; @@ -99,17 +130,29 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) { const categoriesNamesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.NAME); const categoriesGLCodeColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.GL_CODE); const categoriesEnabledColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ENABLED); + const categoriesMaxAmountNoReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_RECEIPT); + const categoriesMaxAmountNoItemizedReceiptColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.MAX_AMOUNT_NO_ITEMIZED_RECEIPT); const categoriesNames = spreadsheet?.data[categoriesNamesColumn].map((name) => name); const categoriesEnabled = categoriesEnabledColumn !== -1 ? spreadsheet?.data[categoriesEnabledColumn].map((enabled) => enabled) : []; const categoriesGLCode = categoriesGLCodeColumn !== -1 ? spreadsheet?.data[categoriesGLCodeColumn].map((glCode) => glCode) : []; + const categoriesMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoReceiptColumn] : []; + const categoriesMaxAmountNoItemizedReceipt = categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? spreadsheet?.data[categoriesMaxAmountNoItemizedReceiptColumn] : []; const categories = categoriesNames?.slice(containsHeader ? 1 : 0).map((name, index) => { const categoryAlreadyExists = policyCategories?.[name]; const existingGLCodeOrDefault = categoryAlreadyExists?.['GL Code'] ?? ''; + const dataIndex = containsHeader ? index + 1 : index; + + const parsedMaxAmountNoReceipt = categoriesMaxAmountNoReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoReceipt?.[dataIndex]?.toString()) : undefined; + const parsedMaxAmountNoItemizedReceipt = + categoriesMaxAmountNoItemizedReceiptColumn !== -1 ? parseCsvReceiptValue(categoriesMaxAmountNoItemizedReceipt?.[dataIndex]?.toString()) : undefined; + return { name, - enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[containsHeader ? index + 1 : index]?.toString().trim().toLowerCase() ?? '') : true, + enabled: categoriesEnabledColumn !== -1 ? ['true', 'yes'].includes(categoriesEnabled?.[dataIndex]?.toString().trim().toLowerCase() ?? '') : true, // eslint-disable-next-line @typescript-eslint/naming-convention - 'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[containsHeader ? index + 1 : index] ?? '') : existingGLCodeOrDefault, + 'GL Code': categoriesGLCodeColumn !== -1 ? (categoriesGLCode?.[dataIndex] ?? '') : existingGLCodeOrDefault, + ...(parsedMaxAmountNoReceipt !== undefined && {maxAmountNoReceipt: parsedMaxAmountNoReceipt}), + ...(parsedMaxAmountNoItemizedReceipt !== undefined && {maxAmountNoItemizedReceipt: parsedMaxAmountNoItemizedReceipt}), }; });