From fd9046b18c858c5d79df33d5efc57ffbac5b6adf Mon Sep 17 00:00:00 2001 From: Chavda Sachin Date: Thu, 21 May 2026 18:27:36 +0530 Subject: [PATCH 1/6] Refactor IOURequestStepMerchant to handle empty merchant input and clear merchant state. Updated logic to validate merchant input and added a call to clearMoneyRequestMerchant when the new merchant is empty. --- src/pages/iou/request/step/IOURequestStepMerchant.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 5e1787752074..8f3f8ae073fa 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,7 @@ function IOURequestStepMerchant({ return; } - if (newMerchant === merchant || (newMerchant === '' && merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) { + if (newMerchant === merchant || (newMerchant === '' && isInvalidMerchantValue(merchant))) { setIsSaved(true); shouldNavigateAfterSaveRef.current = true; return; @@ -146,8 +146,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; From 3c8eee20a70caef4ae9749d1a42c421e05d1dfa3 Mon Sep 17 00:00:00 2001 From: Chavda Sachin Date: Thu, 21 May 2026 18:44:59 +0530 Subject: [PATCH 2/6] Enhance IOURequestStepMerchant to handle empty merchant input more effectively. Added logic to set saved state and clear merchant data when the new merchant is empty and invalid. --- src/pages/iou/request/step/IOURequestStepMerchant.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 8f3f8ae073fa..9b0213c83d58 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -123,6 +123,12 @@ function IOURequestStepMerchant({ return; } + if ((newMerchant === '' && isInvalidMerchantValue(merchant))) { + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; + clearMoneyRequestMerchant(transactionID); + return; + } if (newMerchant === merchant || (newMerchant === '' && isInvalidMerchantValue(merchant))) { setIsSaved(true); shouldNavigateAfterSaveRef.current = true; From 20e0e89472100f49ddeef914fb2c28f791f99c3e Mon Sep 17 00:00:00 2001 From: Chavda Sachin Date: Thu, 21 May 2026 18:54:59 +0530 Subject: [PATCH 3/6] prettier --- src/pages/iou/request/step/IOURequestStepMerchant.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 9b0213c83d58..c5129df322bb 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -123,7 +123,7 @@ function IOURequestStepMerchant({ return; } - if ((newMerchant === '' && isInvalidMerchantValue(merchant))) { + if (newMerchant === '' && isInvalidMerchantValue(merchant)) { setIsSaved(true); shouldNavigateAfterSaveRef.current = true; clearMoneyRequestMerchant(transactionID); From 74c918fbb2c71c556a3c451ba38ff5a18d41e8ef Mon Sep 17 00:00:00 2001 From: Chavda Sachin Date: Thu, 21 May 2026 20:40:20 +0530 Subject: [PATCH 4/6] Update MerchantField to conditionally display merchant value based on transaction state, ensuring it only shows when the merchant is set. --- .../MoneyRequestConfirmationList/sections/MerchantField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx b/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx index 72c5be794961..bb6bf7fc66b4 100644 --- a/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx +++ b/src/components/MoneyRequestConfirmationList/sections/MerchantField.tsx @@ -54,7 +54,7 @@ function MerchantField({ const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); const merchantValue = getMerchant(transaction); - const displayMerchantValue = isInvalidMerchantValue(merchantValue) ? '' : merchantValue; + const displayMerchantValue = !transaction?.isMerchantSet && isInvalidMerchantValue(merchantValue) ? '' : merchantValue; const isMerchantEmpty = !displayMerchantValue; // Determine if the merchant error should be displayed From 72596c28573a488b1cf62993f49cdaa2de7dcc7a Mon Sep 17 00:00:00 2001 From: Chavda Sachin Date: Fri, 22 May 2026 16:37:09 +0530 Subject: [PATCH 5/6] Adjust the logic for the new selector --- .../MoneyRequestConfirmationList/sections/selectors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList/sections/selectors.ts b/src/components/MoneyRequestConfirmationList/sections/selectors.ts index ecd6cf509c63..b4bcf5683702 100644 --- a/src/components/MoneyRequestConfirmationList/sections/selectors.ts +++ b/src/components/MoneyRequestConfirmationList/sections/selectors.ts @@ -106,7 +106,7 @@ const categoryStateSelector = (t: OnyxEntry): 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), }; From 6236ddf05ab5cda7e459e0fbb4cc032ea93660d9 Mon Sep 17 00:00:00 2001 From: Chavda Sachin Date: Fri, 22 May 2026 16:47:03 +0530 Subject: [PATCH 6/6] Add merchant tests --- tests/unit/TransactionUtilsTest.ts | 31 ++++++++++ .../selectorsTest.ts | 61 +++++++++++++++++++ .../hooks/useConfirmationValidation.test.ts | 24 ++++++++ 3 files changed, 116 insertions(+) create mode 100644 tests/unit/components/MoneyRequestConfirmationList/selectorsTest.ts 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}));