diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c95c49c04d094..2bd1e3ba6c8bf 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -32,7 +32,6 @@ import {isCategoryMissing} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {getReportIDForExpense} from '@libs/MergeTransactionUtils'; import {hasEnabledOptions} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import {getLengthOfTag, getTagLists, hasDependentTags as hasDependentTagsPolicyUtils, isTaxTrackingEnabled} from '@libs/PolicyUtils'; @@ -44,7 +43,6 @@ import { canEditMoneyRequest, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, getReportName, - getReportOrDraftReport, getTransactionDetails, getTripIDFromTransactionParentReportID, isInvoiceReport, @@ -616,9 +614,9 @@ function MoneyRequestView({ ); }); - 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 @@ -915,7 +913,7 @@ function MoneyRequestView({ >; + const MERGE_FIELD_TRANSLATION_KEYS = { amount: 'iou.amount', currency: 'iou.currency', @@ -322,6 +325,7 @@ function buildMergedTransactionData(targetTransaction: OnyxEntry, m created: mergeTransaction.created, modifiedCreated: mergeTransaction.created, reportID: mergeTransaction.reportID, + reportName: mergeTransaction.reportName, }; } @@ -372,7 +376,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) : ''; @@ -426,6 +434,26 @@ function buildMergeFieldsData( }); } +/** + * Build updated values for merge transaction field selection + * Handles special cases like currency for amount field, reportID + */ +function getMergeFieldUpdatedValues(transaction: OnyxEntry, field: K, fieldValue: MergeTransaction[K]): MergeTransactionUpdateValues { + const updatedValues: MergeTransactionUpdateValues = { + [field]: fieldValue, + }; + + if (field === 'amount') { + updatedValues.currency = getCurrency(transaction); + } + + if (field === 'reportID') { + updatedValues.reportName = transaction?.reportName ?? getReportName(getReportOrDraftReport(getReportIDForExpense(transaction))); + } + + return updatedValues; +} + export { getSourceTransactionFromMergeTransaction, getTargetTransactionFromMergeTransaction, @@ -442,8 +470,9 @@ export { getDisplayValue, buildMergeFieldsData, getReportIDForExpense, + getMergeFieldUpdatedValues, getMergeFieldErrorText, MERGE_FIELDS, }; -export type {MergeFieldKey, MergeFieldData}; +export type {MergeFieldKey, MergeFieldData, MergeTransactionUpdateValues}; diff --git a/src/libs/actions/MergeTransaction.ts b/src/libs/actions/MergeTransaction.ts index 5ed6505d4a65e..b77db2fbe15f5 100644 --- a/src/libs/actions/MergeTransaction.ts +++ b/src/libs/actions/MergeTransaction.ts @@ -1,11 +1,11 @@ import {deepEqual} from 'fast-equals'; import Onyx from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxMergeInput, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type {GetTransactionsForMergingParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getMergeFieldValue, getTransactionThreadReportID, MERGE_FIELDS} from '@libs/MergeTransactionUtils'; -import type {MergeFieldKey} from '@libs/MergeTransactionUtils'; +import type {MergeFieldKey, MergeTransactionUpdateValues} from '@libs/MergeTransactionUtils'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import {getIOUActionForReportID} from '@libs/ReportActionsUtils'; import { @@ -34,8 +34,8 @@ function setupMergeTransactionData(transactionID: string, values: Partial) { - Onyx.merge(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, values); +function setMergeTransactionKey(transactionID: string, values: MergeTransactionUpdateValues) { + Onyx.merge(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`, values as OnyxMergeInput<`${typeof ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${string}`>); } /** diff --git a/src/pages/TransactionMerge/DetailsReviewPage.tsx b/src/pages/TransactionMerge/DetailsReviewPage.tsx index dc1ed159ca461..ce6df18a783b3 100644 --- a/src/pages/TransactionMerge/DetailsReviewPage.tsx +++ b/src/pages/TransactionMerge/DetailsReviewPage.tsx @@ -19,6 +19,7 @@ import { buildMergeFieldsData, getMergeableDataAndConflictFields, getMergeFieldErrorText, + getMergeFieldUpdatedValues, getMergeFieldValue, getSourceTransactionFromMergeTransaction, getTargetTransactionFromMergeTransaction, @@ -31,7 +32,6 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import {getTransactionDetails} from '@libs/ReportUtils'; -import {getCurrency} from '@libs/TransactionUtils'; import {createTransactionThreadReport, openReport} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -151,9 +151,10 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { // Update both the field value and track which transaction was selected (persisted in Onyx) const currentSelections = mergeTransaction?.selectedTransactionByField ?? {}; + const updatedValues = getMergeFieldUpdatedValues(transaction, field, fieldValue); + setMergeTransactionKey(transactionID, { - [field]: fieldValue, - ...(field === 'amount' && {currency: getCurrency(transaction)}), + ...updatedValues, selectedTransactionByField: { ...currentSelections, [field]: transaction.transactionID, diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index 7178c6c648a23..f125f593f2802 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -22,9 +22,8 @@ import { 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'; @@ -115,13 +114,6 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr 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, { diff --git a/src/types/onyx/MergeTransaction.ts b/src/types/onyx/MergeTransaction.ts index 1b6023585d9d5..1710628084e97 100644 --- a/src/types/onyx/MergeTransaction.ts +++ b/src/types/onyx/MergeTransaction.ts @@ -52,6 +52,9 @@ type MergeTransaction = { /** The report ID of the transaction */ reportID: string; + /** The report name of the transaction */ + reportName: string; + /** The attendees of the transaction */ attendees?: Attendee[]; }; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index bbe5c4b9fcb09..a26eeeb8f5d66 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -478,6 +478,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; diff --git a/tests/unit/MergeTransactionUtilsTest.ts b/tests/unit/MergeTransactionUtilsTest.ts index 45becac876814..68a5f1d4d4416 100644 --- a/tests/unit/MergeTransactionUtilsTest.ts +++ b/tests/unit/MergeTransactionUtilsTest.ts @@ -1,9 +1,11 @@ +import Onyx from 'react-native-onyx'; import { buildMergedTransactionData, getDisplayValue, getMergeableDataAndConflictFields, getMergeFieldErrorText, getMergeFieldTranslationKey, + getMergeFieldUpdatedValues, getMergeFieldValue, getSourceTransactionFromMergeTransaction, isEmptyMergeValue, @@ -12,14 +14,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 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 @@ -508,6 +518,7 @@ describe('MergeTransactionUtils', () => { receipt: {receiptID: 1235, source: 'merged.jpg', filename: 'merged.jpg'}, created: '2025-01-02T00:00:00.000Z', reportID: '1', + reportName: 'Test Report', }; const result = buildMergedTransactionData(targetTransaction, mergeTransaction); @@ -533,6 +544,7 @@ describe('MergeTransactionUtils', () => { created: '2025-01-02T00:00:00.000Z', modifiedCreated: '2025-01-02T00:00:00.000Z', reportID: '1', + reportName: 'Test Report', }); }); }); @@ -715,6 +727,114 @@ 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', () => { + it('should return updated values with the field value for non-special fields', () => { + // Given a transaction and a basic field like merchant + const transaction = createRandomTransaction(0); + const fieldValue = 'New Merchant Name'; + + // When we get updated values for merchant field + const result = getMergeFieldUpdatedValues(transaction, 'merchant', fieldValue); + + // Then it should return an object with the field value + expect(result).toEqual({ + merchant: 'New Merchant Name', + }); + }); + + it('should include currency when field is amount', () => { + // Given a transaction with EUR currency + const transaction = { + ...createRandomTransaction(0), + currency: CONST.CURRENCY.EUR, + }; + const fieldValue = 2500; + + // When we get updated values for amount field + const result = getMergeFieldUpdatedValues(transaction, 'amount', fieldValue); + + // Then it should include both amount and currency + expect(result).toEqual({ + amount: 2500, + currency: CONST.CURRENCY.EUR, + }); + }); + + 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', + }); + }); }); describe('getMergeFieldErrorText', () => { diff --git a/tests/utils/collections/mergeTransaction.ts b/tests/utils/collections/mergeTransaction.ts index dd45e28fa951b..938720a013235 100644 --- a/tests/utils/collections/mergeTransaction.ts +++ b/tests/utils/collections/mergeTransaction.ts @@ -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(), }; }