Skip to content
10 changes: 4 additions & 6 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
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';
Expand All @@ -44,7 +43,6 @@
canEditMoneyRequest,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
getReportName,
getReportOrDraftReport,
getTransactionDetails,
getTripIDFromTransactionParentReportID,
isInvoiceReport,
Expand Down Expand Up @@ -549,7 +547,7 @@

const getAttendeesTitle = useMemo(() => {
return Array.isArray(actualAttendees) ? actualAttendees.map((item) => item?.displayName ?? item?.login).join(', ') : '';
}, [transactionAttendees]);

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

View workflow job for this annotation

GitHub Actions / ESLint check

React Hook useMemo has a missing dependency: 'actualAttendees'. Either include it or remove the dependency array

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

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

React Hook useMemo has a missing dependency: 'actualAttendees'. Either include it or remove the dependency array
const attendeesCopyValue = !canEdit ? getAttendeesTitle : undefined;

const previousTagLength = getLengthOfTag(previousTag ?? '');
Expand Down Expand Up @@ -616,9 +614,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 @@ -915,7 +913,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 @@ -926,6 +926,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
}
switch (key) {
case 'reportID':
case 'reportName':
case 'currency':
case 'tag':
case 'category':
Expand Down Expand Up @@ -1050,6 +1051,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
33 changes: 31 additions & 2 deletions src/libs/MergeTransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type MergeFieldData = {
options: MergeFieldOption[];
};

/** Type for merge transaction values that can be null to clear existing values in Onyx */
type MergeTransactionUpdateValues = Partial<Record<keyof MergeTransaction, MergeTransaction[keyof MergeTransaction] | null>>;

const MERGE_FIELD_TRANSLATION_KEYS = {
amount: 'iou.amount',
currency: 'iou.currency',
Expand Down Expand Up @@ -322,6 +325,7 @@ function buildMergedTransactionData(targetTransaction: OnyxEntry<Transaction>, m
created: mergeTransaction.created,
modifiedCreated: mergeTransaction.created,
reportID: mergeTransaction.reportID,
reportName: mergeTransaction.reportName,
};
}

Expand Down Expand Up @@ -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) : '';
Expand Down Expand Up @@ -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<K extends MergeFieldKey>(transaction: OnyxEntry<Transaction>, 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,
Expand All @@ -442,8 +470,9 @@ export {
getDisplayValue,
buildMergeFieldsData,
getReportIDForExpense,
getMergeFieldUpdatedValues,
getMergeFieldErrorText,
MERGE_FIELDS,
};

export type {MergeFieldKey, MergeFieldData};
export type {MergeFieldKey, MergeFieldData, MergeTransactionUpdateValues};
8 changes: 4 additions & 4 deletions src/libs/actions/MergeTransaction.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -34,8 +34,8 @@ function setupMergeTransactionData(transactionID: string, values: Partial<MergeT
/**
* Sets merge transaction data for a specific transaction
*/
function setMergeTransactionKey(transactionID: string, values: Partial<MergeTransaction>) {
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}`>);
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/pages/TransactionMerge/DetailsReviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
buildMergeFieldsData,
getMergeableDataAndConflictFields,
getMergeFieldErrorText,
getMergeFieldUpdatedValues,
getMergeFieldValue,
getSourceTransactionFromMergeTransaction,
getTargetTransactionFromMergeTransaction,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
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 @@ -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';
Expand Down Expand Up @@ -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, {
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 @@ -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[];
};
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 @@ -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;

Expand Down
120 changes: 120 additions & 0 deletions tests/unit/MergeTransactionUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Onyx from 'react-native-onyx';
import {
buildMergedTransactionData,
getDisplayValue,
getMergeableDataAndConflictFields,
getMergeFieldErrorText,
getMergeFieldTranslationKey,
getMergeFieldUpdatedValues,
getMergeFieldValue,
getSourceTransactionFromMergeTransaction,
isEmptyMergeValue,
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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',
});
});
});
Expand Down Expand Up @@ -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', () => {
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