From e6da1758b575987b11057f336917039911e8fe9b Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Oct 2025 12:41:28 +0200 Subject: [PATCH 1/2] Expand getTransactionDetails with transaction.report fallback --- src/components/TransactionItemRow/index.tsx | 3 + src/libs/ReportUtils.ts | 21 ++-- tests/unit/ReportUtilsTest.ts | 129 +++++++++++++++++++- 3 files changed, 140 insertions(+), 13 deletions(-) diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 7c62c303053c..662596b2467d 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -78,6 +78,9 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & { /** Used to initiate payment from search page */ hash?: number; + + /** Report to which the transaction belongs */ + report?: Report; }; type TransactionItemRowProps = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 99bd81d69858..7e7fad778690 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -22,6 +22,7 @@ import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; +import type {TransactionWithOptionalSearchFields} from '@components/TransactionItemRow'; import type {ThemeColors} from '@styles/theme/types'; import type {IOUAction, IOUType, OnboardingAccounting} from '@src/CONST'; import CONST, {TASK_TO_FEATURE} from '@src/CONST'; @@ -1190,10 +1191,10 @@ function getChatType(report: OnyxInputOrEntry | Participant): ValueOf | SearchReport { +function getReportOrDraftReport(reportID: string | undefined, searchReports?: SearchReport[], fallbackReport?: Report): OnyxEntry | SearchReport { const searchReport = searchReports?.find((report) => report.reportID === reportID); const onyxReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - return searchReport ?? onyxReport ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; + return searchReport ?? onyxReport ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`] ?? fallbackReport; } function reportTransactionsSelector(transactions: OnyxCollection, reportID: string | undefined): Transaction[] { @@ -4259,7 +4260,7 @@ function getMoneyRequestReportName({ */ function getTransactionDetails( - transaction: OnyxInputOrEntry, + transaction: OnyxInputOrEntry | TransactionWithOptionalSearchFields, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING, policy: OnyxEntry = undefined, allowNegativeAmount = false, @@ -4268,20 +4269,16 @@ function getTransactionDetails( if (!transaction) { return; } - const report = getReportOrDraftReport(transaction?.reportID); + + const report = getReportOrDraftReport(transaction?.reportID, undefined, 'report' in transaction ? transaction.report : undefined); const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction); + const isFromExpenseReport = !isEmptyObject(report) && isExpenseReport(report); return { created: getFormattedCreated(transaction, createdDateFormat), - amount: getTransactionAmount( - transaction, - !isEmptyObject(report) && isExpenseReport(report), - transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID, - allowNegativeAmount, - disableOppositeConversion, - ), + amount: getTransactionAmount(transaction, isFromExpenseReport, transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID, allowNegativeAmount, disableOppositeConversion), attendees: getAttendees(transaction), - taxAmount: getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), + taxAmount: getTaxAmount(transaction, isFromExpenseReport), taxCode: getTaxCode(transaction), currency: getCurrency(transaction), comment: getDescription(transaction), diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 5ca35c952756..63732960c50c 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -59,6 +59,7 @@ import { getReportActionActorAccountID, getReportIDFromLink, getReportName, + getReportOrDraftReport, getReportStatusTranslation, getReportURLForCurrentContext, getSearchReportName, @@ -115,7 +116,7 @@ import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import type {ACHAccount} from '@src/types/onyx/Policy'; import type {Participant, Participants} from '@src/types/onyx/Report'; -import type {SearchTransaction} from '@src/types/onyx/SearchResults'; +import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {chatReportR14932 as mockedChatReport} from '../../__mocks__/reportData/reports'; import * as NumberUtils from '../../src/libs/NumberUtils'; @@ -8285,4 +8286,130 @@ describe('ReportUtils', () => { expect(canRejectReportAction(approver, expenseReport, reportPolicy)).toBe(false); }); }); + + describe('getReportOrDraftReport', () => { + const mockReportID = '12345'; + const mockSearchReport: SearchReport = { + reportID: mockReportID, + reportName: 'Test Search Report', + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + participants: {}, + policyID: '', + ownerAccountID: 0, + managerID: 0, + accountID: 0, + isPolicyExpenseChat: false, + isOwnPolicyExpenseChat: false, + parentReportID: '', + parentReportActionID: '', + private_isArchived: '0', + }; + const mockOnyxReport: Report = { + ...createPolicyExpenseChat(1), + reportID: mockReportID, + reportName: 'Test Onyx Report', + }; + const mockDraftReport: Report = { + ...createExpenseReport(2), + reportID: mockReportID, + reportName: 'Test Draft Report', + }; + const mockFallbackReport: Report = { + ...createExpenseRequestReport(3), + reportID: mockReportID, + reportName: 'Test Fallback Report', + }; + + beforeEach(async () => { + await Onyx.clear(); + }); + + test('returns search report when found in searchReports array', () => { + const searchReports = [mockSearchReport]; + const result = getReportOrDraftReport(mockReportID, searchReports); + expect(result).toEqual(mockSearchReport); + }); + + test('returns onyx report when search report is not found but onyx report exists', async () => { + const searchReports: SearchReport[] = []; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); + const result = getReportOrDraftReport(mockReportID, searchReports); + expect(result).toEqual(mockOnyxReport); + }); + + test('returns draft report when neither search nor onyx report exists but draft exists', async () => { + const searchReports: SearchReport[] = []; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${mockReportID}`, mockDraftReport); + const result = getReportOrDraftReport(mockReportID, searchReports); + expect(result).toEqual(mockDraftReport); + }); + + test('returns fallback report when no other reports exist', () => { + const searchReports: SearchReport[] = []; + const result = getReportOrDraftReport(mockReportID, searchReports, mockFallbackReport); + expect(result).toEqual(mockFallbackReport); + }); + + test('returns undefined when no reports exist and no fallback provided', () => { + const searchReports: SearchReport[] = []; + const result = getReportOrDraftReport(mockReportID, searchReports); + expect(result).toBeUndefined(); + }); + + test('returns undefined when reportID is undefined', () => { + const searchReports = [mockSearchReport]; + const result = getReportOrDraftReport(undefined, searchReports); + expect(result).toBeUndefined(); + }); + + test('prioritizes search report over onyx report when both exist', async () => { + const searchReports = [mockSearchReport]; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); + const result = getReportOrDraftReport(mockReportID, searchReports); + expect(result).toEqual(mockSearchReport); + expect(result).not.toEqual(mockOnyxReport); + }); + + test('prioritizes onyx report over draft report when both exist', async () => { + const searchReports: SearchReport[] = []; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${mockReportID}`, mockDraftReport); + const result = getReportOrDraftReport(mockReportID, searchReports); + expect(result).toEqual(mockOnyxReport); + expect(result).not.toEqual(mockDraftReport); + }); + + test('prioritizes draft report over fallback when both exist', async () => { + const searchReports: SearchReport[] = []; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${mockReportID}`, mockDraftReport); + const result = getReportOrDraftReport(mockReportID, searchReports, mockFallbackReport); + expect(result).toEqual(mockDraftReport); + expect(result).not.toEqual(mockFallbackReport); + }); + + test('handles empty searchReports array gracefully', async () => { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); + const result = getReportOrDraftReport(mockReportID); + expect(result).toEqual(mockOnyxReport); + }); + + test('handles different reportID formats correctly', async () => { + const numericReportID = '12345'; + const stringReportID = 'abc123'; + const uuidReportID = '550e8400-e29b-41d4-a716-446655440000'; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${numericReportID}`, {...mockOnyxReport, reportID: numericReportID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${stringReportID}`, {...mockOnyxReport, reportID: stringReportID}); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${uuidReportID}`, {...mockOnyxReport, reportID: uuidReportID}); + + const numericResult = getReportOrDraftReport(numericReportID); + const stringResult = getReportOrDraftReport(stringReportID); + const uuidResult = getReportOrDraftReport(uuidReportID); + + expect(numericResult?.reportID).toBe(numericReportID); + expect(stringResult?.reportID).toBe(stringReportID); + expect(uuidResult?.reportID).toBe(uuidReportID); + }); + }); }); From ee077a3a3d4618ca0f2a1bd3ba10ff430f7023ea Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Thu, 23 Oct 2025 13:11:16 +0200 Subject: [PATCH 2/2] Add getReportOrDraftReport unit tests --- tests/unit/ReportUtilsTest.ts | 64 ++++++++++++----------------------- 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 63732960c50c..e9caa166a4f2 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -8288,37 +8288,24 @@ describe('ReportUtils', () => { }); describe('getReportOrDraftReport', () => { - const mockReportID = '12345'; + const mockReportIDIndex = 1; + const mockReportID = mockReportIDIndex.toString(); const mockSearchReport: SearchReport = { - reportID: mockReportID, - reportName: 'Test Search Report', + ...createRandomReport(mockReportIDIndex), + reportName: 'Search Report', type: CONST.REPORT.TYPE.CHAT, - chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - participants: {}, - policyID: '', - ownerAccountID: 0, - managerID: 0, - accountID: 0, - isPolicyExpenseChat: false, - isOwnPolicyExpenseChat: false, - parentReportID: '', - parentReportActionID: '', - private_isArchived: '0', }; const mockOnyxReport: Report = { - ...createPolicyExpenseChat(1), - reportID: mockReportID, - reportName: 'Test Onyx Report', + ...createPolicyExpenseChat(mockReportIDIndex), + reportName: 'Onyx Report', }; const mockDraftReport: Report = { - ...createExpenseReport(2), - reportID: mockReportID, - reportName: 'Test Draft Report', + ...createExpenseReport(mockReportIDIndex), + reportName: 'Draft Report', }; const mockFallbackReport: Report = { - ...createExpenseRequestReport(3), - reportID: mockReportID, - reportName: 'Test Fallback Report', + ...createExpenseRequestReport(mockReportIDIndex), + reportName: 'Fallback Report', }; beforeEach(async () => { @@ -8347,7 +8334,7 @@ describe('ReportUtils', () => { test('returns fallback report when no other reports exist', () => { const searchReports: SearchReport[] = []; - const result = getReportOrDraftReport(mockReportID, searchReports, mockFallbackReport); + const result = getReportOrDraftReport('unknownReportID', searchReports, mockFallbackReport); expect(result).toEqual(mockFallbackReport); }); @@ -8363,6 +8350,17 @@ describe('ReportUtils', () => { expect(result).toBeUndefined(); }); + test('returns undefined when only reportID is provided and it is not found', () => { + const result = getReportOrDraftReport('unknownReportID'); + expect(result).toBeUndefined(); + }); + + test('returns fallback report when reportID is undefined', () => { + const searchReports = [mockSearchReport]; + const result = getReportOrDraftReport(undefined, searchReports, mockFallbackReport); + expect(result).toEqual(mockFallbackReport); + }); + test('prioritizes search report over onyx report when both exist', async () => { const searchReports = [mockSearchReport]; await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); @@ -8393,23 +8391,5 @@ describe('ReportUtils', () => { const result = getReportOrDraftReport(mockReportID); expect(result).toEqual(mockOnyxReport); }); - - test('handles different reportID formats correctly', async () => { - const numericReportID = '12345'; - const stringReportID = 'abc123'; - const uuidReportID = '550e8400-e29b-41d4-a716-446655440000'; - - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${numericReportID}`, {...mockOnyxReport, reportID: numericReportID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${stringReportID}`, {...mockOnyxReport, reportID: stringReportID}); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${uuidReportID}`, {...mockOnyxReport, reportID: uuidReportID}); - - const numericResult = getReportOrDraftReport(numericReportID); - const stringResult = getReportOrDraftReport(stringReportID); - const uuidResult = getReportOrDraftReport(uuidReportID); - - expect(numericResult?.reportID).toBe(numericReportID); - expect(stringResult?.reportID).toBe(stringReportID); - expect(uuidResult?.reportID).toBe(uuidReportID); - }); }); });