Skip to content
Merged
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
11 changes: 5 additions & 6 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';

Check warning on line 6 in src/components/ReportActionItem/MoneyRequestView.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Expensicons' import is restricted from being used by a pattern. Direct imports from Icon/Expensicons are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyExpensifyIcons` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details

Check warning on line 6 in src/components/ReportActionItem/MoneyRequestView.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Expensicons' import is restricted from being used. Direct imports from @components/Icon/Expensicons are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyExpensifyIcons` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
Expand Down Expand Up @@ -33,7 +33,7 @@
import {convertToDisplayString} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getRateFromMerchant, getReportIDForExpense} from '@libs/MergeTransactionUtils';
import {getRateFromMerchant} from '@libs/MergeTransactionUtils';
import {hasEnabledOptions} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getLengthOfTag, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isTaxTrackingEnabled} from '@libs/PolicyUtils';
Expand All @@ -45,7 +45,6 @@
canEditMoneyRequest,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
getReportName,
getReportOrDraftReport,
getTransactionDetails,
getTripIDFromTransactionParentReportID,
isInvoiceReport,
Expand Down Expand Up @@ -626,9 +625,9 @@
);
});

const actualParentReport = isFromMergeTransaction ? getReportOrDraftReport(getReportIDForExpense(updatedTransaction)) : parentReport;
const shouldShowReport = !!parentReportID || !!actualParentReport;
const reportCopyValue = !canEditReport ? getReportName(actualParentReport) || actualParentReport?.reportName : undefined;
const reportNameToDisplay = isFromMergeTransaction ? updatedTransaction?.reportName : getReportName(parentReport) || parentReport?.reportName;
const shouldShowReport = !!parentReportID || (isFromMergeTransaction && !!reportNameToDisplay);
const reportCopyValue = !canEditReport ? reportNameToDisplay : undefined;

// In this case we want to use this value. The shouldUseNarrowLayout will always be true as this case is handled when we display ReportScreen in RHP.
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
Expand Down Expand Up @@ -926,7 +925,7 @@
<OfflineWithFeedback pendingAction={getPendingFieldAction('reportID')}>
<MenuItemWithTopDescription
shouldShowRightIcon={canEditReport}
title={getReportName(actualParentReport) || actualParentReport?.reportName}
title={reportNameToDisplay}
description={translate('common.report')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
Expand Down
2 changes: 2 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
}
switch (key) {
case 'reportID':
case 'reportName':
case 'currency':
case 'tag':
case 'category':
Expand Down Expand Up @@ -1064,6 +1065,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
participants: CONST.RED_BRICK_ROAD_PENDING_ACTION,
receipt: CONST.RED_BRICK_ROAD_PENDING_ACTION,
reportID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION,
routes: CONST.RED_BRICK_ROAD_PENDING_ACTION,
transactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
tag: CONST.RED_BRICK_ROAD_PENDING_ACTION,
Expand Down
14 changes: 12 additions & 2 deletions src/libs/MergeTransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ function getMergeableDataAndConflictFields(targetTransaction: OnyxEntry<Transact
// We allow user to select unreported report
if (field === 'reportID') {
if (targetValue === sourceValue) {
mergeableData[field] = targetValue;
const updatedValues = getMergeFieldUpdatedValues(targetTransaction, field, SafeString(targetValue));
Object.assign(mergeableData, updatedValues);
} else {
conflictFields.push(field);
}
Expand Down Expand Up @@ -339,6 +340,7 @@ function buildMergedTransactionData(targetTransaction: OnyxEntry<Transaction>, m
created: mergeTransaction.created,
modifiedCreated: mergeTransaction.created,
reportID: mergeTransaction.reportID,
reportName: mergeTransaction.reportName,
routes: mergeTransaction.routes,
};
}
Expand Down Expand Up @@ -390,7 +392,11 @@ function getDisplayValue(field: MergeFieldKey, transaction: Transaction, transla
return getCommaSeparatedTagNameWithSanitizedColons(SafeString(fieldValue));
}
if (field === 'reportID') {
return fieldValue === CONST.REPORT.UNREPORTED_REPORT_ID ? translate('common.none') : getReportName(getReportOrDraftReport(SafeString(fieldValue)));
if (fieldValue === CONST.REPORT.UNREPORTED_REPORT_ID) {
return translate('common.none');
}

return transaction?.reportName ?? getReportName(getReportOrDraftReport(SafeString(fieldValue)));
}
if (field === 'attendees') {
return Array.isArray(fieldValue) ? getAttendeesListDisplayString(fieldValue) : '';
Expand Down Expand Up @@ -457,6 +463,10 @@ function getMergeFieldUpdatedValues<K extends MergeFieldKey>(transaction: OnyxEn
updatedValues.currency = getCurrency(transaction);
}

if (field === 'reportID') {
updatedValues.reportName = transaction?.reportName ?? getReportName(getReportOrDraftReport(getReportIDForExpense(transaction)));
}

if (field === 'merchant' && isDistanceRequest(transaction)) {
const transactionDetails = getTransactionDetails(transaction);
updatedValues.amount = getMergeFieldValue(transactionDetails, transaction, 'amount') as number;
Expand Down
10 changes: 1 addition & 9 deletions src/pages/TransactionMerge/MergeTransactionsListContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import EmptyStateComponent from '@components/EmptyStateComponent';
import {EmptyShelves} from '@components/Icon/Illustrations';

Check warning on line 5 in src/pages/TransactionMerge/MergeTransactionsListContent.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Illustrations' import is restricted from being used by a pattern. Direct imports from Icon/Illustrations are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyIllustrations` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details

Check warning on line 5 in src/pages/TransactionMerge/MergeTransactionsListContent.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Illustrations' import is restricted from being used. Direct imports from @components/Icon/Illustrations are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyIllustrations` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details
import RenderHTML from '@components/RenderHTML';
import ScrollView from '@components/ScrollView';
import SelectionList from '@components/SelectionListWithSections';
Expand All @@ -22,9 +22,8 @@
shouldNavigateToReceiptReview,
} from '@libs/MergeTransactionUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getReportName, getReportOrDraftReport} from '@libs/ReportUtils';
import {getReportName} from '@libs/ReportUtils';
import {getCreated} from '@libs/TransactionUtils';
import {openReport} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -115,13 +114,6 @@
return;
}

// It's a temporary solution to ensure the source report is loaded, so we can display reportName in the merge transaction details page
// We plan to remove this in next phase of merge expenses project
const sourceReport = getReportOrDraftReport(sourceTransaction.reportID);
if (!sourceReport) {
openReport(sourceTransaction.reportID);
}

const {targetTransaction: newTargetTransaction, sourceTransaction: newSourceTransaction} = selectTargetAndSourceTransactionsForMerge(targetTransaction, sourceTransaction);
if (shouldNavigateToReceiptReview([newTargetTransaction, newSourceTransaction])) {
setMergeTransactionKey(transactionID, {
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/MergeTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ type MergeTransaction = {
/** The report ID of the transaction */
reportID: string;

/** The report name of the transaction */
reportName: string;

/** Custom unit data for distance requests */
customUnit?: TransactionCustomUnit;

Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The iouReportID associated with the transaction */
reportID: string | undefined;

/** The name of iouReport associated with the transaction */
reportName?: string;

/** Existing routes */
routes?: Routes;

Expand Down
106 changes: 106 additions & 0 deletions tests/unit/MergeTransactionUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Onyx from 'react-native-onyx';
import {
buildMergedTransactionData,
getDisplayValue,
Expand All @@ -15,14 +16,22 @@ import {
} from '@libs/MergeTransactionUtils';
import {getTransactionDetails} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import createRandomMergeTransaction from '../utils/collections/mergeTransaction';
import {createRandomReport} from '../utils/collections/reports';
import createRandomTransaction, {createRandomDistanceRequestTransaction} from '../utils/collections/transaction';
import {translateLocal} from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';

// Mock localeCompare function for tests
const mockLocaleCompare = (a: string, b: string) => a.localeCompare(b);

describe('MergeTransactionUtils', () => {
beforeAll(() => {
Onyx.init({keys: ONYXKEYS});
return waitForBatchedUpdates();
});

describe('getSourceTransactionFromMergeTransaction', () => {
it('should return undefined when mergeTransaction is undefined', () => {
// Given a null merge transaction
Expand Down Expand Up @@ -485,6 +494,27 @@ describe('MergeTransactionUtils', () => {
expect(result.conflictFields).toContain('attendees');
});
});

it('auto-merges reportID and populates reportName when reportIDs match', () => {
const sharedReportID = 'R123';
const targetTransaction = {
...createRandomTransaction(10),
reportID: sharedReportID,
reportName: 'Shared Report Name',
};
const sourceTransaction = {
...createRandomTransaction(11),
reportID: sharedReportID,
};

const result = getMergeableDataAndConflictFields(targetTransaction, sourceTransaction, mockLocaleCompare);

expect(result.conflictFields).not.toContain('reportID');
expect(result.mergeableData).toMatchObject({
reportID: sharedReportID,
reportName: 'Shared Report Name',
});
});
});

describe('buildMergedTransactionData', () => {
Expand Down Expand Up @@ -516,6 +546,7 @@ describe('MergeTransactionUtils', () => {
receipt: {receiptID: 1235, source: 'merged.jpg', filename: 'merged.jpg'},
created: '2025-01-02T00:00:00.000Z',
reportID: '1',
reportName: 'Test Report',
waypoints: {waypoint0: {name: 'Selected waypoint'}},
customUnit: {name: CONST.CUSTOM_UNITS.NAME_DISTANCE, customUnitID: 'distance1', quantity: 100},
};
Expand Down Expand Up @@ -545,6 +576,7 @@ describe('MergeTransactionUtils', () => {
created: '2025-01-02T00:00:00.000Z',
modifiedCreated: '2025-01-02T00:00:00.000Z',
reportID: '1',
reportName: 'Test Report',
});
});
});
Expand Down Expand Up @@ -751,6 +783,61 @@ describe('MergeTransactionUtils', () => {
expect(merchantResult).toBe('Starbucks Coffee');
expect(categoryResult).toBe('Food & Dining');
});

it('should return "None" for unreported reportID', () => {
// Given a transaction with unreported reportID
const transaction = {
...createRandomTransaction(0),
reportID: CONST.REPORT.UNREPORTED_REPORT_ID,
};

// When we get display value for reportID
const result = getDisplayValue('reportID', transaction, translateLocal);

// Then it should return translated "None"
expect(result).toBe('common.none');
});

it("should return transaction's reportName when available for reportID", () => {
// Given a transaction with reportID and reportName
const transaction = {
...createRandomTransaction(0),
reportID: '123',
reportName: 'Test Report Name',
};

// When we get display value for reportID
const result = getDisplayValue('reportID', transaction, translateLocal);

// Then it should return the reportName
expect(result).toBe('Test Report Name');
});

it("should return report's name when no reportName available on transaction", async () => {
// Given a random report
const reportID = 456;
const report = {
...createRandomReport(reportID, undefined),
reportName: 'Test Report Name',
};

// Store the report in Onyx
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report);
await waitForBatchedUpdates();

// Given a transaction with reportID but no reportName
const transaction = {
...createRandomTransaction(0),
reportID: report.reportID,
reportName: undefined,
};

// When we get display value for reportID
const result = getDisplayValue('reportID', transaction, translateLocal);

// Then it should return the report's name from Onyx
expect(result).toBe(report.reportName);
});
});

describe('getMergeFieldUpdatedValues', () => {
Expand Down Expand Up @@ -786,6 +873,25 @@ describe('MergeTransactionUtils', () => {
});
});

it('should include reportName when field is reportID', () => {
// Given a transaction with a reportID and reportName
const transaction = {
...createRandomTransaction(0),
reportID: '123',
reportName: 'Test Report',
};
const fieldValue = '456';

// When we get updated values for reportID field
const result = getMergeFieldUpdatedValues(transaction, 'reportID', fieldValue);

// Then it should include both reportID and reportName
expect(result).toEqual({
reportID: '456',
reportName: 'Test Report',
});
});

it('should include additional fields when merchant field is selected for distance request', () => {
// Given a distance request transaction
const transaction = {
Expand Down
1 change: 1 addition & 0 deletions tests/utils/collections/mergeTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export default function createRandomMergeTransaction(index: number): MergeTransa
receipt: {},
created: format(randPastDate(), CONST.DATE.FNS_DB_FORMAT_STRING),
reportID: index.toString(),
reportName: randWord(),
};
}
Loading