Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -69,7 +68,7 @@ function MerchantField({
return translate('iou.error.invalidMerchant');
}

if (shouldDisplayFieldError && isMerchantRequired && isMerchantEmpty) {
if (shouldDisplayFieldError && isMerchantRequired && !displayMerchantValue) {
return translate('common.error.fieldRequired');
}

Expand Down Expand Up @@ -120,7 +119,7 @@ function MerchantField({
return (
<MenuItemWithTopDescription
shouldShowRightIcon={!isReadOnly}
title={isMerchantEmpty ? '' : displayMerchantValue}
title={displayMerchantValue}
description={translate('common.merchant')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,15 @@ const categoryStateSelector = (t: OnyxEntry<Transaction>): 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<Transaction>): MerchantState | undefined => {
if (!t) {
return undefined;
}
return {
merchant: getMerchant(t),
isMerchantSet: t.isMerchantSet ?? false,
isMissing: isMerchantMissing(t),
hasReceipt: hasReceipt(t),
};
Expand Down
14 changes: 11 additions & 3 deletions src/pages/iou/request/step/IOURequestStepMerchant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/TransactionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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> = {}): 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);
});
});
});
24 changes: 24 additions & 0 deletions tests/unit/hooks/useConfirmationValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}));
Expand Down
Loading