From bd529829136d25c576acb0465b0e1ac23b5f7dac Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 6 Jun 2025 13:58:05 +0200 Subject: [PATCH 1/2] Add getSendMoneyFlowOneTransactionThreadID function --- src/libs/ReportActionsUtils.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 39b959b98d063..0b7a9cab154b4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1185,6 +1185,33 @@ function isTagModificationAction(actionName: string): boolean { ); } +/** + * Used for Send Money flow, which is a special case where we have no IOU create action and only one IOU pay action. + * In other reports, pay actions do not count as a transactions, but this is an exception to this rule. + */ +function getSendMoneyFlowOneTransactionThreadID(actions: OnyxEntry | ReportAction[], chatReportID?: string) { + if (!chatReportID) { + return undefined; + } + + const iouActions = Object.values(actions ?? {}).filter(isMoneyRequestAction); + + // sendMoneyFlow has only one IOU action... + if (iouActions.length !== 1) { + return undefined; + } + + // ...which is 'pay'... + const isFirstActionPay = getOriginalMessage(iouActions.at(0))?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY; + + const {type, chatType, parentReportID, parentReportActionID} = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? {}; + + // ...and can only be triggered on DM chats + const isDM = type === CONST.REPORT.TYPE.CHAT && !chatType && !(parentReportID && parentReportActionID); + + return isFirstActionPay && isDM ? iouActions.at(0)?.childReportID : undefined; +} + /** Whether action has no linked report by design */ const isIOUActionTypeExcludedFromFiltering = (type: OriginalMessageIOU['type'] | undefined) => [CONST.IOU.REPORT_ACTION_TYPE.SPLIT, CONST.IOU.REPORT_ACTION_TYPE.TRACK, CONST.IOU.REPORT_ACTION_TYPE.PAY].some((actionType) => actionType === type); @@ -1237,6 +1264,12 @@ function getOneTransactionThreadReportID( return; } + const sendMoneyFlowID = getSendMoneyFlowOneTransactionThreadID(reportActions, report?.chatReportID); + + if (sendMoneyFlowID) { + return sendMoneyFlowID; + } + const iouRequestActions = []; for (const action of reportActionsArray) { // If the original message is a 'pay' IOU, it shouldn't be added to the transaction count. @@ -2602,6 +2635,7 @@ export { getWorkspaceDescriptionUpdatedMessage, getWorkspaceReportFieldAddMessage, getWorkspaceCustomUnitRateAddedMessage, + getSendMoneyFlowOneTransactionThreadID, getWorkspaceTagUpdateMessage, getWorkspaceReportFieldUpdateMessage, getWorkspaceReportFieldDeleteMessage, From 72cc40a7ec693f2bbba41f10605242d4e80900b6 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 6 Jun 2025 14:29:40 +0200 Subject: [PATCH 2/2] Add tests for getSendMoneyFlowOneTransactionThreadID --- tests/unit/ReportActionsUtilsTest.ts | 67 ++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index fe7809f657004..9aa8fde275344 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -2,10 +2,10 @@ import type {KeyValueMapping} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {isExpenseReport} from '@libs/ReportUtils'; import {actionR14932 as mockIOUAction, originalMessageR14932 as mockOriginalMessage} from '../../__mocks__/reportData/actions'; -import {iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports'; +import {chatReportR14932 as mockChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports'; import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; -import {getOneTransactionThreadReportID, getOriginalMessage, isIOUActionMatchingTransactionList} from '../../src/libs/ReportActionsUtils'; +import {getOneTransactionThreadReportID, getOriginalMessage, getSendMoneyFlowOneTransactionThreadID, isIOUActionMatchingTransactionList} from '../../src/libs/ReportActionsUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; import type {Report, ReportAction} from '../../src/types/onyx'; import createRandomReport from '../utils/collections/reports'; @@ -856,7 +856,68 @@ describe('ReportActionsUtils', () => { }); }); - describe('getOneTransactionThreadReportID', () => {}); + describe('getSendMoneyFlowOneTransactionThreadID', () => { + const mockChatReportID = 'REPORT'; + const mockDMChatReportID = 'REPORT_DM'; + const childReportID = 'childReport123'; + + const mockedReports: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, Report> = { + [`${ONYXKEYS.COLLECTION.REPORT}${mockChatReportID}`]: {...mockChatReport, reportID: mockChatReportID}, + [`${ONYXKEYS.COLLECTION.REPORT}${mockDMChatReportID}`]: { + ...mockChatReport, + reportID: mockDMChatReportID, + chatType: undefined, + parentReportID: undefined, + parentReportActionID: undefined, + }, + }; + + beforeEach(async () => { + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, mockedReports); + }); + + const createAction = { + ...mockIOUAction, + childReportID, + originalMessage: {...getOriginalMessage(mockIOUAction), type: CONST.IOU.TYPE.CREATE}, + }; + + const nonIOUAction = { + ...mockIOUAction, + childReportID, + type: CONST.REPORT.ACTIONS.TYPE.CREATED, + }; + + const payAction = { + ...mockIOUAction, + childReportID, + originalMessage: {...getOriginalMessage(mockIOUAction), type: CONST.IOU.TYPE.PAY}, + }; + + it('should return undefined for a single non-IOU action', () => { + expect(getSendMoneyFlowOneTransactionThreadID([nonIOUAction], mockDMChatReportID)).toBeUndefined(); + }); + + it('should return undefined for multiple IOU actions regardless of type', () => { + expect(getSendMoneyFlowOneTransactionThreadID([payAction, payAction], mockDMChatReportID)).toBeUndefined(); + }); + + it('should return undefined for a single IOU action that is not `Pay`', () => { + expect(getSendMoneyFlowOneTransactionThreadID([createAction], mockDMChatReportID)).toBeUndefined(); + }); + + it('should return the appropriate childReportID for a valid single `Pay` IOU action in DM chat', () => { + expect(getSendMoneyFlowOneTransactionThreadID([payAction], mockDMChatReportID)).toEqual(childReportID); + }); + + it('should return undefined for a valid single `Pay` IOU action in a chat that is not DM', () => { + expect(getSendMoneyFlowOneTransactionThreadID([payAction], mockChatReportID)).toBeUndefined(); + }); + + it('should return undefined for a valid `Pay` IOU action in DM chat that has also a create IOU action', () => { + expect(getSendMoneyFlowOneTransactionThreadID([payAction, createAction], mockDMChatReportID)).toBeUndefined(); + }); + }); describe('shouldShowAddMissingDetails', () => { it('should return true if personal detail is not completed', async () => {