diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index aa3300981fd2..76c5bc4cd540 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -101,6 +101,7 @@ import { isTimeRequest as isTimeRequestTransactionUtils, shouldShowAttendees as shouldShowAttendeesTransactionUtils, } from '@libs/TransactionUtils'; +import {isInvalidMerchantValue} from '@libs/ValidationUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/inbox/report/AnimatedEmptyStateBackground'; @@ -278,8 +279,7 @@ function MoneyRequestView({ convertedAmount: transactionConvertedAmount, } = getTransactionDetails(transaction, undefined, undefined, allowNegativeAmount, false, currentUserPersonalDetails) ?? {}; const isZeroTransactionAmount = transactionAmount === 0; - const isEmptyMerchant = - transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transactionMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + const isEmptyMerchant = isInvalidMerchantValue(transactionMerchant); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction, !!mergeTransactionID); const isGPSDistanceRequest = isGPSDistanceRequestTransactionUtils(transaction); @@ -436,10 +436,7 @@ function MoneyRequestView({ const shouldNavigateToUpgradePath = !policyForMovingExpenses && !shouldSelectPolicy; const updatedTransactionDescription = getDescription(updatedTransaction) || undefined; - const isEmptyUpdatedMerchant = - updatedTransaction?.modifiedMerchant === '' || - updatedTransaction?.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || - updatedTransaction?.modifiedMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + const isEmptyUpdatedMerchant = isInvalidMerchantValue(updatedTransaction?.modifiedMerchant); const updatedMerchantTitle = isEmptyUpdatedMerchant ? '' : (updatedTransaction?.modifiedMerchant ?? merchantTitle); const shouldShowConvertedAmount = diff --git a/src/components/Rule/TextBase.tsx b/src/components/Rule/TextBase.tsx index b72564bb1648..607e456d75de 100644 --- a/src/components/Rule/TextBase.tsx +++ b/src/components/Rule/TextBase.tsx @@ -8,7 +8,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isRequiredFulfilled, isValidInputLength} from '@libs/ValidationUtils'; +import {isInvalidMerchantValue, isRequiredFulfilled, isValidInputLength} from '@libs/ValidationUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {OnyxFormKey} from '@src/ONYXKEYS'; @@ -62,6 +62,10 @@ function TextBase({ if (!isValid) { (errors as Record)[fieldID] = translate('common.error.characterLimitExceedCounter', byteLength, characterLimit); + } else if (fieldID === CONST.EXPENSE_RULES.FIELDS.RENAME_MERCHANT || fieldID === CONST.MERCHANT_RULES.FIELDS.MERCHANT) { + if (isInvalidMerchantValue(trimmedValue)) { + (errors as Record)[fieldID] = translate('iou.error.invalidMerchant'); + } } } diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 87026e8681d6..b79a3bf75a9d 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -158,6 +158,7 @@ import { isScanning, isViolationDismissed, } from './TransactionUtils'; +import {isInvalidMerchantValue} from './ValidationUtils'; import ViolationsUtils from './Violations/ViolationsUtils'; type ColumnSortMapping = Partial>; @@ -889,7 +890,7 @@ function getTransactionItemCommonFormattedProperties( const formattedTotal = getTransactionAmount(transactionItem, isExpenseReport); const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created; const merchant = getTransactionMerchant(transactionItem, policy); - const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant; + const formattedMerchant = isInvalidMerchantValue(merchant) ? '' : merchant; const submitted = report?.submitted; const approved = report?.approved; @@ -955,7 +956,7 @@ function getShouldShowMerchant(data: OnyxTypes.SearchResults['data']): boolean { if (isTransactionEntry(key)) { const item = data[key]; const merchant = item.modifiedMerchant ? item.modifiedMerchant : (item.merchant ?? ''); - return merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; + return !isInvalidMerchantValue(merchant); } return false; }); @@ -2361,11 +2362,7 @@ function getMerchantSections(data: OnyxTypes.SearchResults['data'], queryJSON: S // - UNKNOWN_MERCHANT ('Unknown Merchant') - used when merchant cannot be determined const rawMerchant = merchantGroup.merchant; const isEmptyMerchant = - !rawMerchant || - rawMerchant === CONST.SEARCH.MERCHANT_EMPTY_VALUE || - rawMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT || - rawMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || - rawMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT; + !rawMerchant || rawMerchant === CONST.SEARCH.MERCHANT_EMPTY_VALUE || rawMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || isInvalidMerchantValue(rawMerchant); const formattedMerchant = isEmptyMerchant ? translate('search.noMerchant') : rawMerchant; merchantSections[key] = { @@ -4258,7 +4255,7 @@ function getColumnsToShow( const {moneyRequestReportActionsByTransactionID} = Array.isArray(data) ? {} : createReportActionsLookupMaps(data); const updateColumns = (transaction: OnyxTypes.Transaction) => { const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : (transaction.merchant ?? ''); - if ((merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT) || isScanning(transaction)) { + if (!isInvalidMerchantValue(merchant) || isScanning(transaction)) { columns[CONST.SEARCH.TABLE_COLUMNS.MERCHANT] = true; } diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index 62ee51ca985f..d7db2dd81673 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -45,6 +45,7 @@ import { isScanning, isUnreportedAndHasInvalidDistanceRateTransaction, } from './TransactionUtils'; +import {isInvalidMerchantValue} from './ValidationUtils'; import {filterReceiptViolations} from './Violations/ViolationsUtils'; const emptyPersonalDetails: OnyxTypes.PersonalDetails = { @@ -432,11 +433,7 @@ function createTransactionPreviewConditionals({ - the expense is not a distance expense with a pending route and amount = 0 - in this case, the merchant says: "Route pending...", which is already shown in the amount field; */ - const shouldShowMerchant = - !!requestMerchant && - requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && - requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && - !(isFetchingWaypoints && !requestAmount); + const shouldShowMerchant = !isInvalidMerchantValue(requestMerchant) && !(isFetchingWaypoints && !requestAmount); const shouldShowDescription = !!description && !shouldShowMerchant && !isScanning(transaction); return { diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 4c036cb93ecd..834b87e65a10 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -46,6 +46,7 @@ import { isSettled, isThread, } from '@libs/ReportUtils'; +import {isInvalidMerchantValue} from '@libs/ValidationUtils'; import type {IOURequestType, UpdateMoneyRequestDataKeys} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; @@ -592,12 +593,9 @@ function isDemoTransaction(transaction: OnyxInputOrEntry): boolean function isMerchantMissing(transaction: OnyxEntry) { if (transaction?.modifiedMerchant && transaction.modifiedMerchant !== '') { - return transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.modifiedMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + return isInvalidMerchantValue(transaction.modifiedMerchant); } - const isMerchantEmpty = - transaction?.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction?.merchant === CONST.TRANSACTION.DEFAULT_MERCHANT || transaction?.merchant === ''; - - return isMerchantEmpty; + return isInvalidMerchantValue(transaction?.merchant); } /** diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 9604b9d3b6cc..5290262691cc 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -763,6 +763,13 @@ function isValidTaxIDEINNumber(number: string, country: Country | '') { } } +/** + * Checks if a merchant string value is considered invalid/empty + */ +function isInvalidMerchantValue(merchant?: string): boolean { + return !merchant || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT; +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -816,4 +823,5 @@ export { isValidRegistrationNumber, isValidInputLength, isValidTaxIDEINNumber, + isInvalidMerchantValue, }; diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 949fa2633c25..9e085ef276ee 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -16,7 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; import {getTransactionDetails, isExpenseRequest, isPolicyExpenseChat} from '@libs/ReportUtils'; -import {isValidInputLength} from '@libs/ValidationUtils'; +import {isInvalidMerchantValue, isValidInputLength} from '@libs/ValidationUtils'; import {setDraftSplitTransaction, setMoneyRequestMerchant, updateMoneyRequestMerchant} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,7 +57,7 @@ function IOURequestStepMerchant({ // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value const isEditingSplitBill = iouType === CONST.IOU.TYPE.SPLIT && isEditing; const merchant = getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant; - const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + const isEmptyMerchant = isInvalidMerchantValue(merchant); const initialMerchant = isEmptyMerchant ? '' : merchant; const [currentMerchant, setCurrentMerchant] = useState(initialMerchant); const [isSaved, setIsSaved] = useState(false); @@ -89,10 +89,7 @@ function IOURequestStepMerchant({ if (isMerchantRequired && !value.moneyRequestMerchant) { errors.moneyRequestMerchant = translate('common.error.fieldRequired'); - } else if ( - isMerchantRequired && - (value.moneyRequestMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || value.moneyRequestMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT) - ) { + } else if (isMerchantRequired && isInvalidMerchantValue(value.moneyRequestMerchant)) { errors.moneyRequestMerchant = translate('iou.error.invalidMerchant'); } else if (!isValid) { errors.moneyRequestMerchant = translate('common.error.characterLimitExceedCounter', byteLength, CONST.MERCHANT_NAME_MAX_BYTES); diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts index 50871c752b12..56faa67c29ec 100644 --- a/tests/unit/ValidationUtilsTest.ts +++ b/tests/unit/ValidationUtilsTest.ts @@ -4,6 +4,7 @@ import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; import { getAgeRequirementError, + isInvalidMerchantValue, isRequiredFulfilled, isValidAccountRoute, isValidDate, @@ -550,4 +551,19 @@ describe('ValidationUtils', () => { }); }); }); + + describe('isInvalidMerchantValue', () => { + test('Valid merchnt name', () => { + expect(isInvalidMerchantValue('test name')).toBe(false); + expect(isInvalidMerchantValue('none')).toBe(false); + expect(isInvalidMerchantValue('Unknown Merchant')).toBe(false); + expect(isInvalidMerchantValue('X Æ A test')).toBe(false); + }); + + test('Invalid merchant name', () => { + expect(isInvalidMerchantValue('')).toBe(true); + expect(isInvalidMerchantValue('Expense')).toBe(true); + expect(isInvalidMerchantValue('(none)')).toBe(true); + }); + }); });