diff --git a/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx b/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx index 11fe26d8a404..e336386a8c1d 100644 --- a/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx @@ -53,8 +53,7 @@ function MerchantField({ const merchantState = useTransactionSelector(transactionID, merchantStateSelector, isEditingSplitBill); const merchantValue = merchantState?.merchant ?? ''; - const displayMerchantValue = isInvalidMerchantValue(merchantValue) ? '' : merchantValue; - const isMerchantEmpty = !displayMerchantValue; + const displayMerchantValue = !merchantState?.isMerchantSet && isInvalidMerchantValue(merchantValue) ? '' : merchantValue; const transactionHasReceipt = merchantState?.hasReceipt ?? false; // Determine if the merchant error should be displayed @@ -69,7 +68,7 @@ function MerchantField({ return translate('iou.error.invalidMerchant'); } - if (shouldDisplayFieldError && isMerchantRequired && isMerchantEmpty) { + if (shouldDisplayFieldError && isMerchantRequired && !displayMerchantValue) { return translate('common.error.fieldRequired'); } @@ -120,7 +119,7 @@ function MerchantField({ return ( ): CategoryState | undef // --- MerchantField --- -type MerchantState = {merchant: string; isMissing: boolean; hasReceipt: boolean}; +type MerchantState = {merchant: string; isMerchantSet: boolean; isMissing: boolean; hasReceipt: boolean}; const merchantStateSelector = (t: OnyxEntry): MerchantState | undefined => { if (!t) { @@ -114,6 +114,7 @@ const merchantStateSelector = (t: OnyxEntry): MerchantState | undef } return { merchant: getMerchant(t), + isMerchantSet: t.isMerchantSet ?? false, isMissing: isMerchantMissing(t), hasReceipt: hasReceipt(t), }; diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 5e1787752074..c5129df322bb 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -21,7 +21,7 @@ import {skipNextFocusRestore} from '@libs/NavigationFocusReturn'; import {getTransactionDetails, isExpenseRequest, isPolicyExpenseChat} from '@libs/ReportUtils'; import {hasReceipt} from '@libs/TransactionUtils'; import {isInvalidMerchantValue, isValidInputLength} from '@libs/ValidationUtils'; -import {setMoneyRequestMerchant} from '@userActions/IOU/MoneyRequest'; +import {clearMoneyRequestMerchant, setMoneyRequestMerchant} from '@userActions/IOU/MoneyRequest'; import {setDraftSplitTransaction} from '@userActions/IOU/Split'; import {updateMoneyRequestMerchant} from '@userActions/IOU/UpdateMoneyRequest'; import CONST from '@src/CONST'; @@ -123,7 +123,13 @@ function IOURequestStepMerchant({ return; } - if (newMerchant === merchant || (newMerchant === '' && merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) { + if (newMerchant === '' && isInvalidMerchantValue(merchant)) { + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; + clearMoneyRequestMerchant(transactionID); + return; + } + if (newMerchant === merchant || (newMerchant === '' && isInvalidMerchantValue(merchant))) { setIsSaved(true); shouldNavigateAfterSaveRef.current = true; return; @@ -146,8 +152,10 @@ function IOURequestStepMerchant({ parentReportNextStep, delegateAccountID, }); + } else if (!newMerchant) { + clearMoneyRequestMerchant(transactionID); } else { - setMoneyRequestMerchant(transactionID, newMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, true, hasReceipt(transaction)); + setMoneyRequestMerchant(transactionID, newMerchant, true, hasReceipt(transaction)); } setIsSaved(true); shouldNavigateAfterSaveRef.current = true; diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index e9ada561b413..8411edbaaf7c 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -728,6 +728,37 @@ describe('TransactionUtils', () => { }); }); + describe('isMerchantMissing', () => { + it('returns true for empty, default, and partial merchant values', () => { + expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: ''}))).toBe(true); + expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: CONST.TRANSACTION.DEFAULT_MERCHANT}))).toBe(true); + expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}))).toBe(true); + }); + + it('returns false for a valid merchant', () => { + expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: 'Starbucks'}))).toBe(false); + }); + + it('uses modifiedMerchant when present', () => { + expect( + TransactionUtils.isMerchantMissing( + generateTransaction({ + merchant: 'Starbucks', + modifiedMerchant: CONST.TRANSACTION.DEFAULT_MERCHANT, + }), + ), + ).toBe(true); + expect( + TransactionUtils.isMerchantMissing( + generateTransaction({ + merchant: CONST.TRANSACTION.DEFAULT_MERCHANT, + modifiedMerchant: 'Starbucks', + }), + ), + ).toBe(false); + }); + }); + describe('getMerchant', () => { it('should return merchant if transaction has merchant', () => { const transaction = generateTransaction({ diff --git a/tests/unit/components/MoneyRequestConfirmationList/selectorsTest.ts b/tests/unit/components/MoneyRequestConfirmationList/selectorsTest.ts new file mode 100644 index 000000000000..3281612d80ad --- /dev/null +++ b/tests/unit/components/MoneyRequestConfirmationList/selectorsTest.ts @@ -0,0 +1,61 @@ +import {merchantStateSelector} from '@components/MoneyRequestConfirmationList/sections/selectors'; +import CONST from '@src/CONST'; +import type {Transaction} from '@src/types/onyx'; + +function createTransaction(overrides: Partial = {}): Transaction { + return { + transactionID: 'txn1', + amount: 100, + currency: 'USD', + merchant: 'Coffee Shop', + ...overrides, + } as Transaction; +} + +describe('MoneyRequestConfirmationList selectors', () => { + describe('merchantStateSelector', () => { + it('returns undefined when transaction is undefined', () => { + expect(merchantStateSelector(undefined)).toBeUndefined(); + }); + + it('returns merchant state for a valid transaction', () => { + const transaction = createTransaction({merchant: 'Starbucks', isMerchantSet: true}); + + expect(merchantStateSelector(transaction)).toEqual({ + merchant: 'Starbucks', + isMerchantSet: true, + isMissing: false, + hasReceipt: false, + }); + }); + + it('prefers modifiedMerchant over merchant', () => { + const transaction = createTransaction({ + merchant: 'Original Merchant', + modifiedMerchant: 'Updated Merchant', + }); + + expect(merchantStateSelector(transaction)?.merchant).toBe('Updated Merchant'); + }); + + it('defaults isMerchantSet to false when not set', () => { + const transaction = createTransaction({merchant: 'Starbucks'}); + + expect(merchantStateSelector(transaction)?.isMerchantSet).toBe(false); + }); + + it('marks default merchant values as missing', () => { + expect(merchantStateSelector(createTransaction({merchant: CONST.TRANSACTION.DEFAULT_MERCHANT}))?.isMissing).toBe(true); + expect(merchantStateSelector(createTransaction({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}))?.isMissing).toBe(true); + expect(merchantStateSelector(createTransaction({merchant: ''}))?.isMissing).toBe(true); + }); + + it('returns hasReceipt true when transaction has a receipt state', () => { + const transaction = createTransaction({ + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_COMPLETE}, + }); + + expect(merchantStateSelector(transaction)?.hasReceipt).toBe(true); + }); + }); +}); diff --git a/tests/unit/hooks/useConfirmationValidation.test.ts b/tests/unit/hooks/useConfirmationValidation.test.ts index 66bcb590ce82..bc6cda11bc42 100644 --- a/tests/unit/hooks/useConfirmationValidation.test.ts +++ b/tests/unit/hooks/useConfirmationValidation.test.ts @@ -69,6 +69,30 @@ describe('useConfirmationValidation', () => { expect(result.current.validate()).toEqual({errorKey: 'iou.error.invalidMerchant'}); }); + it('returns invalidMerchant when merchant is not required but was explicitly set to an invalid value', () => { + const {result} = renderHook(() => + useConfirmationValidation({ + ...baseParams, + isMerchantRequired: false, + isMerchantFieldValid: false, + transaction: {transactionID: 'txn1', comment: {}, amount: 100, isMerchantSet: true} as unknown as OnyxTypes.Transaction, + }), + ); + expect(result.current.validate()).toEqual({errorKey: 'iou.error.invalidMerchant'}); + }); + + it('returns null when merchant is not required and was not explicitly set', () => { + const {result} = renderHook(() => + useConfirmationValidation({ + ...baseParams, + isMerchantRequired: false, + isMerchantFieldValid: false, + transaction: {transactionID: 'txn1', comment: {}, amount: 100, isMerchantSet: false} as unknown as OnyxTypes.Transaction, + }), + ); + expect(result.current.validate()).toEqual({errorKey: null}); + }); + it('returns invalidCategoryLength when category exceeds max', () => { const longCategory = 'C'.repeat(CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH + 1); const {result} = renderHook(() => useConfirmationValidation({...baseParams, iouCategory: longCategory}));