From 8128168e3ee94030277859b8e5eb05be4c0edd18 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 27 Mar 2024 00:54:56 +0000 Subject: [PATCH 1/2] Update version to 1.4.56-8 (cherry picked from commit 1bbb594d1735cab697ac6cba0bf3435c295dc291) --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bae37fe24ab6..c85e6bb27b4f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001045607 - versionName "1.4.56-7" + versionCode 1001045608 + versionName "1.4.56-8" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index d6e3b27605ff..417e3a4a3d3d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.56.7 + 1.4.56.8 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0f61b60623ca..9d5552892828 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.56.7 + 1.4.56.8 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 26de3a589462..1ca70277a320 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.56 CFBundleVersion - 1.4.56.7 + 1.4.56.8 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index dbd4ec08dcac..d09df29ad2ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.56-7", + "version": "1.4.56-8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.56-7", + "version": "1.4.56-8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8572e89bbb8c..00974611774f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.56-7", + "version": "1.4.56-8", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 6f549e8f220da2a3c5710b8af8e8eb108339db91 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Tue, 26 Mar 2024 17:49:47 -0700 Subject: [PATCH 2/2] Merge pull request #39021 from Expensify/cmartins-fixRogueAction Fix rogue action (cherry picked from commit 7005b0264008ac940dd338ea0f59daf1b8a1c8bb) --- src/libs/actions/IOU.ts | 11 +- tests/actions/IOUTest.ts | 3256 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 3257 insertions(+), 10 deletions(-) create mode 100644 tests/actions/IOUTest.ts diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 857f6f39173a..3638816f8cfb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4374,15 +4374,6 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT ]; const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, - value: { - [optimisticIOUReportAction.reportActionID]: { - pendingAction: null, - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -4606,7 +4597,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { - [expenseReport.reportActionID ?? '']: { + [optimisticApprovedReportAction.reportActionID]: { errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), }, }, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts new file mode 100644 index 000000000000..c71a88809302 --- /dev/null +++ b/tests/actions/IOUTest.ts @@ -0,0 +1,3256 @@ +import isEqual from 'lodash/isEqual'; +import Onyx from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OptimisticChatReport} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import * as IOU from '@src/libs/actions/IOU'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as PolicyActions from '@src/libs/actions/Policy'; +import * as Report from '@src/libs/actions/Report'; +import * as ReportActions from '@src/libs/actions/ReportActions'; +import * as User from '@src/libs/actions/User'; +import DateUtils from '@src/libs/DateUtils'; +import Navigation from '@src/libs/Navigation/Navigation'; +import * as NumberUtils from '@src/libs/NumberUtils'; +import * as PersonalDetailsUtils from '@src/libs/PersonalDetailsUtils'; +import * as ReportActionsUtils from '@src/libs/ReportActionsUtils'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {IOUMessage, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {ReportActionBase} from '@src/types/onyx/ReportAction'; +import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import PusherHelper from '../utils/PusherHelper'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import waitForNetworkPromises from '../utils/waitForNetworkPromises'; + +jest.mock('@src/libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + dismissModal: jest.fn(), + dismissModalWithReport: jest.fn(), + goBack: jest.fn(), +})); + +const CARLOS_EMAIL = 'cmartins@expensifail.com'; +const CARLOS_ACCOUNT_ID = 1; +const JULES_EMAIL = 'jules@expensifail.com'; +const JULES_ACCOUNT_ID = 2; +const RORY_EMAIL = 'rory@expensifail.com'; +const RORY_ACCOUNT_ID = 3; +const VIT_EMAIL = 'vit@expensifail.com'; +const VIT_ACCOUNT_ID = 4; + +OnyxUpdateManager(); +describe('actions/IOU', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('requestMoney', () => { + it('creates new chat if needed', () => { + const amount = 10000; + const comment = 'Giv money plz'; + const merchant = 'KFC'; + let iouReportID: string | undefined; + let createdAction: OnyxEntry; + let iouAction: OnyxEntry; + let transactionID: string | undefined; + let transactionThread: OnyxEntry; + let transactionThreadCreatedAction: OnyxEntry; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // A chat report, a transaction thread, and an iou report should be created + const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); + const iouReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); + expect(Object.keys(chatReports).length).toBe(2); + expect(Object.keys(iouReports).length).toBe(1); + const chatReport = chatReports[0]; + const transactionThreadReport = chatReports[1]; + const iouReport = iouReports[0]; + iouReportID = iouReport?.reportID; + transactionThread = transactionThreadReport; + + expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + // They should be linked together + expect(chatReport?.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + + // The IOU report should have a CREATED action and IOU action + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + const createdActions = Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + const iouActions = Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ); + expect(Object.values(createdActions).length).toBe(1); + expect(Object.values(iouActions).length).toBe(1); + createdAction = createdActions?.[0] ?? null; + iouAction = iouActions?.[0] ?? null; + + // The CREATED action should not be created after the IOU action + expect(Date.parse(createdAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? '')); + + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + + // The comment should be included in the IOU action + expect(iouAction.originalMessage.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(iouAction.originalMessage.amount).toBe(amount); + + // The IOU type should be correct + expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + + // Both actions should be pending + expect(createdAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForTransactionThread) => { + Onyx.disconnect(connectionID); + + // The transaction thread should have a CREATED action + expect(Object.values(reportActionsForTransactionThread ?? {}).length).toBe(1); + const createdActions = Object.values(reportActionsForTransactionThread ?? {}).filter( + (reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + expect(Object.values(createdActions).length).toBe(1); + transactionThreadCreatedAction = createdActions[0]; + + expect(transactionThreadCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // There should be one transaction + expect(Object.values(allTransactions ?? {}).length).toBe(1); + const transaction = Object.values(allTransactions ?? []).find((t) => !isEmptyObject(t)); + transactionID = transaction?.transactionID; + + // The transaction should be attached to the IOU report + expect(transaction?.reportID).toBe(iouReportID); + + // Its amount should match the amount of the request + expect(transaction?.amount).toBe(amount); + + // The comment should be correct + expect(transaction?.comment.comment).toBe(comment); + + // It should be pending + expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); + + expect(transaction?.merchant).toBe(merchant); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: false, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction?.pendingAction).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('updates existing chat report if there is one', () => { + const amount = 10000; + const comment = 'Giv money plz'; + let chatReport: OnyxTypes.Report = { + reportID: '1234', + type: CONST.REPORT.TYPE.CHAT, + participantAccountIDs: [CARLOS_ACCOUNT_ID], + }; + const createdAction: OnyxTypes.ReportAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + }; + let iouReportID: string | undefined; + let iouAction: OnyxEntry; + let iouCreatedAction: OnyxEntry; + let transactionID: string | undefined; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport) + .then(() => + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { + [createdAction.reportActionID]: createdAction, + }), + ) + .then(() => { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // The same chat report should be reused, a transaction thread and an IOU report should be created + expect(Object.values(allReports ?? {}).length).toBe(3); + expect(Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT)?.reportID).toBe(chatReport.reportID); + chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? chatReport; + const iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + iouReportID = iouReport?.reportID; + + expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + // They should be linked together + expect(chatReport.iouReportID).toBe(iouReportID); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (allIOUReportActions) => { + Onyx.disconnect(connectionID); + + iouCreatedAction = + Object.values(allIOUReportActions ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; + iouAction = + Object.values(allIOUReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + + // The CREATED action should not be created after the IOU action + expect(Date.parse(iouCreatedAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? '')); + + // The IOUReportID should be correct + expect(iouAction?.originalMessage?.IOUReportID).toBe(iouReportID); + + // The comment should be included in the IOU action + expect(iouAction?.originalMessage?.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(iouAction?.originalMessage?.amount).toBe(amount); + + // The IOU action type should be correct + expect(iouAction?.originalMessage?.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + + // The IOU action should be pending + expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // There should be one transaction + expect(Object.values(allTransactions ?? {}).length).toBe(1); + const transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + transactionID = transaction?.transactionID; + + // The transaction should be attached to the IOU report + expect(transaction?.reportID).toBe(iouReportID); + + // Its amount should match the amount of the request + expect(transaction?.amount).toBe(amount); + + // The comment should be correct + expect(transaction?.comment.comment).toBe(comment); + + expect(transaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + + // It should be pending + expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: true, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction?.pendingAction).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + + it('updates existing IOU report if there is one', () => { + const amount = 10000; + const comment = 'Giv money plz'; + const chatReportID = '1234'; + const iouReportID = '5678'; + let chatReport: OnyxEntry = { + reportID: chatReportID, + type: CONST.REPORT.TYPE.CHAT, + iouReportID, + participantAccountIDs: [CARLOS_ACCOUNT_ID], + }; + const createdAction: OnyxTypes.ReportAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + }; + const existingTransaction: OnyxTypes.Transaction = { + transactionID: NumberUtils.rand64(), + amount: 1000, + comment: { + comment: 'Existing transaction', + }, + created: DateUtils.getDBTime(), + currency: CONST.CURRENCY.USD, + merchant: '', + reportID: '', + }; + let iouReport: OnyxEntry = { + reportID: iouReportID, + chatReportID, + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: RORY_ACCOUNT_ID, + managerID: CARLOS_ACCOUNT_ID, + currency: CONST.CURRENCY.USD, + total: existingTransaction.amount, + }; + const iouAction: OnyxEntry = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: RORY_ACCOUNT_ID, + created: DateUtils.getDBTime(), + originalMessage: { + IOUReportID: iouReportID, + IOUTransactionID: existingTransaction.transactionID, + amount: existingTransaction.amount, + currency: CONST.CURRENCY.USD, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + participantAccountIDs: [RORY_ACCOUNT_ID, CARLOS_ACCOUNT_ID], + }, + }; + let newIOUAction: OnyxEntry; + let newTransaction: OnyxEntry; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, iouReport)) + .then(() => + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, { + [createdAction.reportActionID]: createdAction, + [iouAction.reportActionID]: iouAction, + }), + ) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransaction.transactionID}`, existingTransaction)) + .then(() => { + if (chatReport) { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // No new reports should be created + expect(Object.values(allReports ?? {}).length).toBe(3); + expect(Object.values(allReports ?? {}).find((report) => report?.reportID === chatReportID)).toBeTruthy(); + expect(Object.values(allReports ?? {}).find((report) => report?.reportID === iouReportID)).toBeTruthy(); + + chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? null; + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + + // The total on the iou report should be updated + expect(iouReport?.total).toBe(11000); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + newIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => + reportAction?.reportActionID !== createdAction.reportActionID && reportAction?.reportActionID !== iouAction?.reportActionID, + ) ?? null; + + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + + // The comment should be included in the IOU action + expect(newIOUAction?.originalMessage.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(newIOUAction?.originalMessage.amount).toBe(amount); + + // The type of the IOU action should be correct + expect(newIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + + // The IOU action should be pending + expect(newIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // There should be two transactions + expect(Object.values(allTransactions ?? {}).length).toBe(2); + + newTransaction = Object.values(allTransactions ?? {}).find((transaction) => transaction?.transactionID !== existingTransaction.transactionID) ?? null; + + expect(newTransaction?.reportID).toBe(iouReportID); + expect(newTransaction?.amount).toBe(amount); + expect(newTransaction?.comment.comment).toBe(comment); + expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + expect(newTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect((newIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(newTransaction?.transactionID); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForNetworkPromises) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + ); + }); + + it('correctly implements RedBrickRoad error handling', () => { + const amount = 10000; + const comment = 'Giv money plz'; + let chatReportID: string | undefined; + let iouReportID: string | undefined; + let createdAction: OnyxEntry; + let iouAction: OnyxEntry; + let transactionID: string; + let transactionThreadReport: OnyxEntry; + let transactionThreadAction: OnyxEntry; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // A chat report, transaction thread and an iou report should be created + const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); + const iouReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); + expect(Object.values(chatReports).length).toBe(2); + expect(Object.values(iouReports).length).toBe(1); + const chatReport = chatReports[0]; + chatReportID = chatReport?.reportID; + transactionThreadReport = chatReports[1]; + + const iouReport = iouReports[0]; + iouReportID = iouReport?.reportID; + + expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + // They should be linked together + expect(chatReport?.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + + // The chat report should have a CREATED action and IOU action + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + const createdActions = + Object.values(reportActionsForIOUReport ?? {}).filter((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; + const iouActions = + Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(Object.values(createdActions).length).toBe(1); + expect(Object.values(iouActions).length).toBe(1); + createdAction = createdActions[0]; + iouAction = iouActions[0]; + + // The CREATED action should not be created after the IOU action + expect(Date.parse(createdAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? {})); + + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + + // The comment should be included in the IOU action + expect(iouAction.originalMessage.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(iouAction.originalMessage.amount).toBe(amount); + + // The type should be correct + expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + + // Both actions should be pending + expect(createdAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // There should be one transaction + expect(Object.values(allTransactions ?? {}).length).toBe(1); + const transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + transactionID = transaction?.transactionID ?? ''; + + expect(transaction?.reportID).toBe(iouReportID); + expect(transaction?.amount).toBe(amount); + expect(transaction?.comment.comment).toBe(comment); + expect(transaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); + + resolve(); + }, + }); + }), + ) + .then((): Promise => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + return fetch.resume() as Promise; + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + iouAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (reportActionsForTransactionThread) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForTransactionThread ?? {}).length).toBe(3); + transactionThreadAction = + Object.values(reportActionsForTransactionThread?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`] ?? {}).find( + (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + expect(transactionThreadAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: false, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction?.pendingAction).toBeFalsy(); + expect(transaction?.errors).toBeTruthy(); + expect(Object.values(transaction?.errors ?? {})[0]).toEqual(expect.arrayContaining(['iou.error.genericCreateFailureMessage', {isTranslated: false}])); + resolve(); + }, + }); + }), + ) + + // If the user clears the errors on the IOU action + .then( + () => + new Promise((resolve) => { + ReportActions.clearAllRelatedReportActionErrors(iouReportID ?? '', iouAction); + resolve(); + }), + ) + + // Then the reportAction from chat report should be removed from Onyx + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + iouAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(iouAction).toBeFalsy(); + resolve(); + }, + }); + }), + ) + + // Then the reportAction from iou report should be removed from Onyx + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + iouAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(iouAction).toBeFalsy(); + resolve(); + }, + }); + }), + ) + + // Then the reportAction from transaction report should be removed from Onyx + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + expect(reportActionsForReport).toMatchObject({}); + resolve(); + }, + }); + }), + ) + + // Along with the associated transaction + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: false, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction).toBeFalsy(); + resolve(); + }, + }); + }), + ) + + // If a user clears the errors on the CREATED action (which, technically are just errors on the report) + .then( + () => + new Promise((resolve) => { + Report.deleteReport(chatReportID ?? ''); + Report.deleteReport(transactionThreadReport?.reportID ?? ''); + resolve(); + }), + ) + + // Then the report should be deleted + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + Object.values(allReports ?? {}).forEach((report) => expect(report).toBeFalsy()); + resolve(); + }, + }); + }), + ) + + // All reportActions should also be deleted + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: false, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + Object.values(allReportActions ?? {}).forEach((reportAction) => expect(reportAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + + // All transactions should also be deleted + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + + // Cleanup + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.succeed) + ); + }); + }); + + describe('split bill', () => { + it('creates and updates new chats and IOUs as needed', () => { + jest.setTimeout(10 * 1000); + /* + * Given that: + * - Rory and Carlos have chatted before + * - Rory and Jules have chatted before and have an active IOU report + * - Rory and Vit have never chatted together before + * - There is no existing group chat with the four of them + */ + const amount = 400; + const comment = 'Yes, I am splitting a bill for $4 USD'; + const merchant = 'Yema Kitchen'; + let carlosChatReport: OnyxEntry = { + reportID: NumberUtils.rand64(), + type: CONST.REPORT.TYPE.CHAT, + participantAccountIDs: [CARLOS_ACCOUNT_ID], + }; + const carlosCreatedAction: OnyxEntry = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + reportID: carlosChatReport.reportID, + }; + const julesIOUReportID = NumberUtils.rand64(); + let julesChatReport: OnyxEntry = { + reportID: NumberUtils.rand64(), + type: CONST.REPORT.TYPE.CHAT, + iouReportID: julesIOUReportID, + participantAccountIDs: [JULES_ACCOUNT_ID], + }; + const julesChatCreatedAction: OnyxEntry = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + reportID: julesChatReport.reportID, + }; + const julesCreatedAction: OnyxEntry = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + reportID: julesIOUReportID, + }; + jest.advanceTimersByTime(200); + const julesExistingTransaction: OnyxEntry = { + transactionID: NumberUtils.rand64(), + amount: 1000, + comment: { + comment: 'This is an existing transaction', + }, + created: DateUtils.getDBTime(), + currency: '', + merchant: '', + reportID: '', + }; + let julesIOUReport: OnyxEntry = { + reportID: julesIOUReportID, + chatReportID: julesChatReport.reportID, + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: RORY_ACCOUNT_ID, + managerID: JULES_ACCOUNT_ID, + currency: CONST.CURRENCY.USD, + total: julesExistingTransaction?.amount, + }; + const julesExistingIOUAction: OnyxEntry = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: RORY_ACCOUNT_ID, + created: DateUtils.getDBTime(), + originalMessage: { + IOUReportID: julesIOUReportID, + IOUTransactionID: julesExistingTransaction?.transactionID, + amount: julesExistingTransaction?.amount ?? 0, + currency: CONST.CURRENCY.USD, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + participantAccountIDs: [RORY_ACCOUNT_ID, JULES_ACCOUNT_ID], + }, + reportID: julesIOUReportID, + }; + + let carlosIOUReport: OnyxEntry; + let carlosIOUAction: OnyxEntry; + let carlosIOUCreatedAction: OnyxEntry; + let carlosTransaction: OnyxEntry; + + let julesIOUAction: OnyxEntry; + let julesIOUCreatedAction: OnyxEntry; + let julesTransaction: OnyxEntry; + + let vitChatReport: OnyxEntry; + let vitIOUReport: OnyxEntry; + let vitCreatedAction: OnyxEntry; + let vitIOUAction: OnyxEntry; + let vitTransaction: OnyxEntry; + + let groupChat: OnyxEntry; + let groupCreatedAction: OnyxEntry; + let groupIOUAction: OnyxEntry; + let groupTransaction: OnyxEntry; + + const reportCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT, [carlosChatReport, julesChatReport, julesIOUReport], (item) => item.reportID); + + const carlosActionsCollectionDataSet = toCollectionDataSet( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, + [ + { + [carlosCreatedAction.reportActionID]: carlosCreatedAction, + }, + ], + (item) => item[carlosCreatedAction.reportActionID].reportID ?? '', + ); + + const julesActionsCollectionDataSet = toCollectionDataSet( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, + [ + { + [julesCreatedAction.reportActionID]: julesCreatedAction, + [julesExistingIOUAction.reportActionID]: julesExistingIOUAction, + }, + ], + (item) => item[julesCreatedAction.reportActionID].reportID ?? '', + ); + + const julesCreatedActionsCollectionDataSet = toCollectionDataSet( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, + [ + { + [julesChatCreatedAction.reportActionID]: julesChatCreatedAction, + }, + ], + (item) => item[julesChatCreatedAction.reportActionID].reportID ?? '', + ); + + return ( + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { + ...reportCollectionDataSet, + }) + .then(() => + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { + ...carlosActionsCollectionDataSet, + ...julesCreatedActionsCollectionDataSet, + ...julesActionsCollectionDataSet, + }), + ) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction?.transactionID}`, julesExistingTransaction)) + .then(() => { + // When we split a bill offline + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + IOU.splitBill( + // TODO: Migrate after the backend accepts accountIDs + [ + [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], + [JULES_EMAIL, String(JULES_ACCOUNT_ID)], + [VIT_EMAIL, String(VIT_ACCOUNT_ID)], + ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), + RORY_EMAIL, + RORY_ACCOUNT_ID, + amount, + comment, + CONST.CURRENCY.USD, + merchant, + '', + '', + '', + ); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // There should now be 10 reports + expect(Object.values(allReports ?? {}).length).toBe(10); + + // 1. The chat report with Rory + Carlos + carlosChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === carlosChatReport?.reportID) ?? null; + expect(isEmptyObject(carlosChatReport)).toBe(false); + expect(carlosChatReport?.pendingFields).toBeFalsy(); + + // 2. The IOU report with Rory + Carlos (new) + carlosIOUReport = + Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU && report.managerID === CARLOS_ACCOUNT_ID) ?? null; + expect(isEmptyObject(carlosIOUReport)).toBe(false); + expect(carlosIOUReport?.total).toBe(amount / 4); + + // 3. The chat report with Rory + Jules + julesChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === julesChatReport?.reportID) ?? null; + expect(isEmptyObject(julesChatReport)).toBe(false); + expect(julesChatReport?.pendingFields).toBeFalsy(); + + // 4. The IOU report with Rory + Jules + julesIOUReport = Object.values(allReports ?? {}).find((report) => report?.reportID === julesIOUReport?.reportID) ?? null; + expect(isEmptyObject(julesIOUReport)).toBe(false); + expect(julesChatReport?.pendingFields).toBeFalsy(); + expect(julesIOUReport?.total).toBe((julesExistingTransaction?.amount ?? 0) + amount / 4); + + // 5. The chat report with Rory + Vit (new) + vitChatReport = + Object.values(allReports ?? {}).find( + (report) => report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participantAccountIDs, [VIT_ACCOUNT_ID]), + ) ?? null; + expect(isEmptyObject(vitChatReport)).toBe(false); + expect(vitChatReport?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + + // 6. The IOU report with Rory + Vit (new) + vitIOUReport = + Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU && report.managerID === VIT_ACCOUNT_ID) ?? null; + expect(isEmptyObject(vitIOUReport)).toBe(false); + expect(vitIOUReport?.total).toBe(amount / 4); + + // 7. The group chat with everyone + groupChat = + Object.values(allReports ?? {}).find( + (report) => + report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participantAccountIDs, [CARLOS_ACCOUNT_ID, JULES_ACCOUNT_ID, VIT_ACCOUNT_ID]), + ) ?? null; + expect(isEmptyObject(groupChat)).toBe(false); + expect(groupChat?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + + // The 1:1 chat reports and the IOU reports should be linked together + expect(carlosChatReport?.iouReportID).toBe(carlosIOUReport?.reportID); + expect(carlosIOUReport?.chatReportID).toBe(carlosChatReport?.reportID); + expect(carlosIOUReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + expect(julesChatReport?.iouReportID).toBe(julesIOUReport?.reportID); + expect(julesIOUReport?.chatReportID).toBe(julesChatReport?.reportID); + + expect(vitChatReport?.iouReportID).toBe(vitIOUReport?.reportID); + expect(vitIOUReport?.chatReportID).toBe(vitChatReport?.reportID); + expect(carlosIOUReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + + // There should be reportActions on all 7 chat reports + 3 IOU reports in each 1:1 chat + expect(Object.values(allReportActions ?? {}).length).toBe(10); + + const carlosReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${carlosChatReport?.iouReportID}`]; + const julesReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${julesChatReport?.iouReportID}`]; + const vitReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${vitChatReport?.iouReportID}`]; + const groupReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChat?.reportID}`]; + + // Carlos DM should have two reportActions – the existing CREATED action and a pending IOU action + expect(Object.values(carlosReportActions ?? {}).length).toBe(2); + carlosIOUCreatedAction = + Object.values(carlosReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + carlosIOUAction = + Object.values(carlosReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(carlosIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(carlosIOUAction?.originalMessage.IOUReportID).toBe(carlosIOUReport?.reportID); + expect(carlosIOUAction?.originalMessage.amount).toBe(amount / 4); + expect(carlosIOUAction?.originalMessage.comment).toBe(comment); + expect(carlosIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + expect(Date.parse(carlosIOUCreatedAction?.created ?? '')).toBeLessThan(Date.parse(carlosIOUAction?.created ?? '')); + + // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action + expect(Object.values(julesReportActions ?? {}).length).toBe(3); + expect(julesReportActions?.[julesCreatedAction.reportActionID]).toStrictEqual(julesCreatedAction); + julesIOUCreatedAction = + Object.values(julesReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + julesIOUAction = + Object.values(julesReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => + reportAction.reportActionID !== julesCreatedAction.reportActionID && + reportAction.reportActionID !== julesExistingIOUAction.reportActionID, + ) ?? null; + expect(julesIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesIOUAction?.originalMessage.IOUReportID).toBe(julesIOUReport?.reportID); + expect(julesIOUAction?.originalMessage.amount).toBe(amount / 4); + expect(julesIOUAction?.originalMessage.comment).toBe(comment); + expect(julesIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + expect(Date.parse(julesIOUCreatedAction?.created ?? '')).toBeLessThan(Date.parse(julesIOUAction?.created ?? '')); + + // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action + expect(Object.values(vitReportActions ?? {}).length).toBe(2); + vitCreatedAction = + Object.values(vitReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + vitIOUAction = + Object.values(vitReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(vitCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction?.originalMessage.IOUReportID).toBe(vitIOUReport?.reportID); + expect(vitIOUAction?.originalMessage.amount).toBe(amount / 4); + expect(vitIOUAction?.originalMessage.comment).toBe(comment); + expect(vitIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + expect(Date.parse(vitCreatedAction?.created ?? '')).toBeLessThan(Date.parse(vitIOUAction?.created ?? '')); + + // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT + expect(Object.values(groupReportActions ?? {}).length).toBe(2); + groupCreatedAction = + Object.values(groupReportActions ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; + groupIOUAction = + Object.values(groupReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(groupCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction?.originalMessage).not.toHaveProperty('IOUReportID'); + expect(groupIOUAction?.originalMessage?.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.SPLIT); + expect(Date.parse(groupCreatedAction?.created ?? '')).toBeLessThanOrEqual(Date.parse(groupIOUAction?.created ?? '')); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + /* There should be 5 transactions + * – one existing one with Jules + * - one for each of the three IOU reports + * - one on the group chat w/ deleted report + */ + expect(Object.values(allTransactions ?? {}).length).toBe(5); + expect(allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction?.transactionID}`]).toBeTruthy(); + + carlosTransaction = + Object.values(allTransactions ?? {}).find( + (transaction) => transaction?.transactionID === (carlosIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, + ) ?? null; + julesTransaction = + Object.values(allTransactions ?? {}).find( + (transaction) => transaction?.transactionID === (julesIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, + ) ?? null; + vitTransaction = + Object.values(allTransactions ?? {}).find( + (transaction) => transaction?.transactionID === (vitIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, + ) ?? null; + groupTransaction = Object.values(allTransactions ?? {}).find((transaction) => transaction?.reportID === CONST.REPORT.SPLIT_REPORTID) ?? null; + + expect(carlosTransaction?.reportID).toBe(carlosIOUReport?.reportID); + expect(julesTransaction?.reportID).toBe(julesIOUReport?.reportID); + expect(vitTransaction?.reportID).toBe(vitIOUReport?.reportID); + expect(groupTransaction).toBeTruthy(); + + expect(carlosTransaction?.amount).toBe(amount / 4); + expect(julesTransaction?.amount).toBe(amount / 4); + expect(vitTransaction?.amount).toBe(amount / 4); + expect(groupTransaction?.amount).toBe(amount); + + expect(carlosTransaction?.comment.comment).toBe(comment); + expect(julesTransaction?.comment.comment).toBe(comment); + expect(vitTransaction?.comment.comment).toBe(comment); + expect(groupTransaction?.comment.comment).toBe(comment); + + expect(carlosTransaction?.merchant).toBe(merchant); + expect(julesTransaction?.merchant).toBe(merchant); + expect(vitTransaction?.merchant).toBe(merchant); + expect(groupTransaction?.merchant).toBe(merchant); + + expect(carlosTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + expect(julesTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + expect(vitTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + + expect(carlosTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + expect(julesTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + expect(vitTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + + expect(carlosTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + waitForCollectionCallback: false, + callback: (allPersonalDetails) => { + Onyx.disconnect(connectionID); + expect(allPersonalDetails).toMatchObject({ + [VIT_ACCOUNT_ID]: { + accountID: VIT_ACCOUNT_ID, + displayName: VIT_EMAIL, + login: VIT_EMAIL, + }, + }); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForNetworkPromises) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + Object.values(allReports ?? {}).forEach((report) => { + if (!report?.pendingFields) { + return; + } + Object.values(report?.pendingFields).forEach((pendingField) => expect(pendingField).toBeFalsy()); + }); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + Object.values(allReportActions ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + ); + }); + }); + + describe('payMoneyRequestElsewhere', () => { + it('clears outstanding IOUReport', () => { + const amount = 10000; + const comment = 'Giv money plz'; + let chatReport: OnyxEntry; + let iouReport: OnyxEntry; + let createIOUAction: OnyxEntry; + let payIOUAction: OnyxEntry; + let transaction: OnyxEntry; + IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return ( + waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + expect(Object.values(allReports ?? {}).length).toBe(3); + + const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); + chatReport = chatReports[0]; + expect(chatReport).toBeTruthy(); + expect(chatReport).toHaveProperty('reportID'); + expect(chatReport).toHaveProperty('iouReportID'); + + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + expect(iouReport?.chatReportID).toBe(chatReport?.reportID); + + expect(chatReport?.pendingFields).toBeFalsy(); + expect(iouReport?.pendingFields).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + + createIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction).toBeTruthy(); + expect((createIOUAction?.originalMessage as IOUMessage)?.IOUReportID).toBe(iouReport?.reportID); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + expect(Object.values(allTransactions ?? {}).length).toBe(1); + transaction = Object.values(allTransactions ?? {}).find((t) => t) ?? null; + expect(transaction).toBeTruthy(); + expect(transaction?.amount).toBe(amount); + expect(transaction?.reportID).toBe(iouReport?.reportID); + expect((createIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transaction?.transactionID); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + if (chatReport && iouReport) { + IOU.payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + expect(Object.values(allReports ?? {}).length).toBe(3); + + chatReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.CHAT) ?? null; + iouReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.IOU) ?? null; + + expect(chatReport?.iouReportID).toBeFalsy(); + + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`]; + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + + payIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction) => + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, + ) ?? null; + expect(payIOUAction).toBeTruthy(); + expect(payIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + expect(Object.values(allReports ?? {}).length).toBe(3); + + chatReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.CHAT) ?? null; + iouReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.IOU) ?? null; + + expect(chatReport?.iouReportID).toBeFalsy(); + + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`]; + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + + payIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction) => + reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, + ) ?? null; + expect(payIOUAction).toBeTruthy(); + + resolve(); + }, + }); + }), + ) + ); + }); + }); + + describe('edit money request', () => { + const amount = 10000; + const comment = '💸💸💸💸'; + const merchant = 'NASDAQ'; + + afterEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + }); + + it('updates the IOU request and IOU report when offline', () => { + let thread: OptimisticChatReport; + let iouReport: OnyxEntry = null; + let iouAction: OnyxEntry = null; + let transaction: OnyxEntry = null; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return waitForBatchedUpdates() + .then(() => { + Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + + [iouAction] = Object.values(reportActionsForIOUReport ?? {}).filter((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)) ?? null; + resolve(); + }, + }); + }), + ) + .then(() => { + thread = ReportUtils.buildTransactionThread(iouAction, iouReport) ?? null; + Onyx.set(`report_${thread?.reportID ?? ''}`, thread); + return waitForBatchedUpdates(); + }) + .then(() => { + if (transaction) { + IOU.editMoneyRequest( + transaction, + thread.reportID, + {amount: 20000, comment: 'Double the amount!'}, + { + id: '123', + role: 'user', + type: 'free', + name: '', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + }, + {}, + {}, + ); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + const updatedTransaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + expect(updatedTransaction?.modifiedAmount).toBe(20000); + expect(updatedTransaction?.comment).toMatchObject({comment: 'Double the amount!'}); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (allActions) => { + Onyx.disconnect(connectionID); + const updatedAction = Object.values(allActions ?? {}).find((reportAction) => !isEmptyObject(reportAction)); + expect(updatedAction?.actionName).toEqual('MODIFIEDEXPENSE'); + expect(updatedAction?.originalMessage).toEqual( + expect.objectContaining({amount: 20000, newComment: 'Double the amount!', oldAmount: amount, oldComment: comment}), + ); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === iouReport?.chatReportID); + expect(updatedIOUReport).toEqual( + expect.objectContaining({ + total: 20000, + cachedTotal: '$200.00', + lastMessageHtml: 'requested $200.00', + lastMessageText: 'requested $200.00', + }), + ); + expect(updatedChatReport).toEqual( + expect.objectContaining({ + lastMessageHtml: `${CARLOS_EMAIL} owes $200.00`, + lastMessageText: `${CARLOS_EMAIL} owes $200.00`, + }), + ); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + }); + }); + + it('resets the IOU request and IOU report when api returns an error', () => { + let thread: OptimisticChatReport; + let iouReport: OnyxEntry; + let iouAction: OnyxEntry; + let transaction: OnyxEntry; + + IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return waitForBatchedUpdates() + .then(() => { + Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + [iouReport] = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + + [iouAction] = Object.values(reportActionsForIOUReport ?? {}).filter((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)) ?? null; + resolve(); + }, + }); + }), + ) + .then(() => { + thread = ReportUtils.buildTransactionThread(iouAction, iouReport); + Onyx.set(`report_${thread.reportID}`, thread); + return waitForBatchedUpdates(); + }) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + + if (transaction) { + IOU.editMoneyRequest( + transaction, + thread.reportID, + {amount: 20000, comment: 'Double the amount!'}, + { + id: '123', + role: 'user', + type: 'free', + name: '', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + }, + {}, + {}, + ); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + const updatedTransaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + expect(updatedTransaction?.modifiedAmount).toBe(undefined); + expect(updatedTransaction?.amount).toBe(10000); + expect(updatedTransaction?.comment).toMatchObject({comment}); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (allActions) => { + Onyx.disconnect(connectionID); + const updatedAction = Object.values(allActions ?? {}).find((reportAction) => !isEmptyObject(reportAction)); + expect(updatedAction?.actionName).toEqual('MODIFIEDEXPENSE'); + expect(Object.values(updatedAction?.errors ?? {})).toEqual(expect.arrayContaining([['iou.error.genericEditFailureMessage', {isTranslated: false}]])); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === iouReport?.chatReportID); + expect(updatedIOUReport).toEqual( + expect.objectContaining({ + total: 10000, + cachedTotal: '$100.00', + lastMessageHtml: `requested $${amount / 100}.00 for ${comment}`, + lastMessageText: `requested $${amount / 100}.00 for ${comment}`, + }), + ); + expect(updatedChatReport).toEqual( + expect.objectContaining({ + lastMessageHtml: '', + }), + ); + resolve(); + }, + }); + }), + ); + }); + }); + + describe('pay expense report via ACH', () => { + const amount = 10000; + const comment = '💸💸💸💸'; + const merchant = 'NASDAQ'; + + afterEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + }); + + it('updates the expense request and expense report when paid while offline', () => { + let expenseReport: OnyxEntry; + let chatReport: OnyxEntry; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); + return waitForBatchedUpdates() + .then(() => { + PolicyActions.createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT) ?? null; + + resolve(); + }, + }); + }), + ) + .then(() => { + if (chatReport) { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + + resolve(); + }, + }); + }), + ) + .then(() => { + if (chatReport && expenseReport) { + IOU.payMoneyRequest(CONST.IOU.PAYMENT_TYPE.VBBA, chatReport, expenseReport); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + waitForCollectionCallback: false, + callback: (allActions) => { + Onyx.disconnect(connectionID); + expect(Object.values(allActions ?? {})).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + message: expect.arrayContaining([ + expect.objectContaining({ + html: `paid $${amount / 100}.00 with Expensify`, + text: `paid $${amount / 100}.00 with Expensify`, + }), + ]), + originalMessage: expect.objectContaining({ + amount, + paymentType: CONST.IOU.PAYMENT_TYPE.VBBA, + type: 'pay', + }), + }), + ]), + ); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === expenseReport?.chatReportID); + expect(updatedIOUReport).toEqual( + expect.objectContaining({ + lastMessageHtml: `paid $${amount / 100}.00 with Expensify`, + lastMessageText: `paid $${amount / 100}.00 with Expensify`, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + }), + ); + expect(updatedChatReport).toEqual( + expect.objectContaining({ + lastMessageHtml: `paid $${amount / 100}.00 with Expensify`, + lastMessageText: `paid $${amount / 100}.00 with Expensify`, + }), + ); + resolve(); + }, + }); + }), + ); + }); + + it('shows an error when paying results in an error', () => { + let expenseReport: OnyxEntry; + let chatReport: OnyxEntry; + + Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); + return waitForBatchedUpdates() + .then(() => { + PolicyActions.createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT) ?? null; + + resolve(); + }, + }); + }), + ) + .then(() => { + if (chatReport) { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + if (chatReport && expenseReport) { + IOU.payMoneyRequest('ACH', chatReport, expenseReport); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + waitForCollectionCallback: false, + callback: (allActions) => { + Onyx.disconnect(connectionID); + const erroredAction = Object.values(allActions ?? {}).find((action) => !isEmptyObject(action?.errors)); + expect(Object.values(erroredAction?.errors ?? {})).toEqual(expect.arrayContaining([['iou.error.other', {isTranslated: false}]])); + resolve(); + }, + }); + }), + ); + }); + }); + + describe('deleteMoneyRequest', () => { + const amount = 10000; + const comment = 'Send me money please'; + let chatReport: OnyxEntry; + let iouReport: OnyxEntry; + let createIOUAction: OnyxEntry; + let transaction: OnyxEntry; + let thread: OptimisticChatReport; + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@test.com'; + let IOU_REPORT_ID: string; + let reportActionID; + const REPORT_ACTION: OnyxEntry = { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorAccountID: TEST_USER_ACCOUNT_ID, + automatic: false, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment', translationKey: ''}], + person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], + shouldShow: true, + created: DateUtils.getDBTime(), + reportActionID: '1', + originalMessage: { + html: '', + whisperedTo: [], + }, + }; + + let reportActions: OnyxCollection; + + beforeEach(async () => { + // Given mocks are cleared and helpers are set up + jest.clearAllMocks(); + PusherHelper.setup(); + + // Given a test user is signed in with Onyx setup and some initial data + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); + User.subscribeToUserEvents(); + await waitForBatchedUpdates(); + await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); + + // When an IOU request for money is made + IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment, {}); + await waitForBatchedUpdates(); + + // When fetching all reports from Onyx + const allReports = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connectionID); + resolve(reports); + }, + }); + }); + + // Then we should have exactly 3 reports + expect(Object.values(allReports ?? {}).length).toBe(3); + + // Then one of them should be a chat report with relevant properties + chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? null; + expect(chatReport).toBeTruthy(); + expect(chatReport).toHaveProperty('reportID'); + expect(chatReport).toHaveProperty('iouReportID'); + + // Then one of them should be an IOU report with relevant properties + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // Then their IDs should reference each other + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + expect(iouReport?.chatReportID).toBe(chatReport?.reportID); + + // Storing IOU Report ID for further reference + IOU_REPORT_ID = chatReport?.iouReportID ?? ''; + + await waitForBatchedUpdates(); + + // When fetching all report actions from Onyx + const allReportActions = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connectionID); + resolve(actions); + }, + }); + }); + + // Then we should find an IOU action with specific properties + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(createIOUAction).toBeTruthy(); + expect(createIOUAction?.originalMessage.IOUReportID).toBe(iouReport?.reportID); + + // When fetching all transactions from Onyx + const allTransactions = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (transactions) => { + Onyx.disconnect(connectionID); + resolve(transactions); + }, + }); + }); + + // Then we should find a specific transaction with relevant properties + transaction = Object.values(allTransactions ?? {}).find((t) => t) ?? null; + expect(transaction).toBeTruthy(); + expect(transaction?.amount).toBe(amount); + expect(transaction?.reportID).toBe(iouReport?.reportID); + expect(createIOUAction?.originalMessage.IOUTransactionID).toBe(transaction?.transactionID); + }); + + afterEach(PusherHelper.teardown); + + it('delete a money request (IOU Action and transaction) successfully', async () => { + // Given the fetch operations are paused and a money request is initiated + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + + if (transaction && createIOUAction) { + // When the money request is deleted + IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, true); + } + await waitForBatchedUpdates(); + + // Then we check if the IOU report action is removed from the report actions collection + let reportActionsForReport = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (actionsForReport) => { + Onyx.disconnect(connectionID); + resolve(actionsForReport); + }, + }); + }); + + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + // Then the IOU Action should be truthy for offline support. + expect(createIOUAction).toBeTruthy(); + + // Then we check if the transaction is removed from the transactions collection + const t = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, + waitForCollectionCallback: false, + callback: (transactionResult) => { + Onyx.disconnect(connectionID); + resolve(transactionResult); + }, + }); + }); + + expect(t).toBeFalsy(); + + // Given fetch operations are resumed + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + await waitForBatchedUpdates(); + + // Then we recheck the IOU report action from the report actions collection + reportActionsForReport = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (actionsForReport) => { + Onyx.disconnect(connectionID); + resolve(actionsForReport); + }, + }); + }); + + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction).toBeFalsy(); + + // Then we recheck the transaction from the transactions collection + const tr = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, + waitForCollectionCallback: false, + callback: (transactionResult) => { + Onyx.disconnect(connectionID); + resolve(transactionResult); + }, + }); + }); + + expect(tr).toBeFalsy(); + }); + + it('delete the IOU report when there are no visible comments left in the IOU report', async () => { + // Given an IOU report and a paused fetch state + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + + if (transaction && createIOUAction) { + // When the IOU money request is deleted + IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, true); + } + await waitForBatchedUpdates(); + + let report = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (res) => { + Onyx.disconnect(connectionID); + resolve(res); + }, + }); + }); + + // Then the report should be truthy for offline support + expect(report).toBeTruthy(); + + // Given the resumed fetch state + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + await waitForBatchedUpdates(); + + report = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (res) => { + Onyx.disconnect(connectionID); + resolve(res); + }, + }); + }); + + // Then the report should be falsy so that there is no trace of the money request. + expect(report).toBeFalsy(); + }); + + it('does not delete the IOU report when there are visible comments left in the IOU report', async () => { + // Given the initial setup is completed + await waitForBatchedUpdates(); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + // When a comment is added to the IOU report + Report.addComment(IOU_REPORT_ID, 'Testing a comment'); + await waitForBatchedUpdates(); + + // Then verify that the comment is correctly added + const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT); + reportActionID = resultAction?.reportActionID ?? ''; + + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + expect(resultAction?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed + expect(Object.values(reportActions ?? {}).length).toBe(3); + + // Then check the loading state of our action + const resultActionAfterUpdate = reportActions?.[reportActionID]; + expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); + + // When we attempt to delete a money request from the IOU report + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + if (transaction && createIOUAction) { + IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + } + await waitForBatchedUpdates(); + + // Then expect that the IOU report still exists + let allReports = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connectionID); + resolve(reports); + }, + }); + }); + + await waitForBatchedUpdates(); + + iouReport = Object.values(allReports ?? {}).find((report) => ReportUtils.isIOUReport(report)) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // Given the resumed fetch state + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + + allReports = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connectionID); + resolve(reports); + }, + }); + }); + // Then expect that the IOU report still exists + iouReport = Object.values(allReports ?? {}).find((report) => ReportUtils.isIOUReport(report)) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + }); + + it('delete the transaction thread if there are no visible comments in the thread', async () => { + // Given all promises are resolved + await waitForBatchedUpdates(); + jest.advanceTimersByTime(10); + + // Given a transaction thread + thread = ReportUtils.buildTransactionThread(createIOUAction, iouReport); + + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + // Given User logins from the participant accounts + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + + // When Opening a thread report with the given details + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); + await waitForBatchedUpdates(); + + // Then The iou action has the transaction report id as a child report ID + const allReportActions = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connectionID); + resolve(actions); + }, + }); + }); + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + // Given Fetch is paused and timers have advanced + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + jest.advanceTimersByTime(10); + + if (transaction && createIOUAction) { + // When Deleting a money request + IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + } + await waitForBatchedUpdates(); + + // Then The report for the given thread ID does not exist + let report = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (reportData) => { + Onyx.disconnect(connectionID); + resolve(reportData); + }, + }); + }); + + expect(report).toBeFalsy(); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + + // Then After resuming fetch, the report for the given thread ID still does not exist + report = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (reportData) => { + Onyx.disconnect(connectionID); + resolve(reportData); + }, + }); + }); + + expect(report).toBeFalsy(); + }); + + it('delete the transaction thread if there are only changelogs (i.e. MODIFIEDEXPENSE actions) in the thread', async () => { + // Given all promises are resolved + await waitForBatchedUpdates(); + jest.advanceTimersByTime(10); + + // Given a transaction thread + thread = ReportUtils.buildTransactionThread(createIOUAction, iouReport); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + // Given User logins from the participant accounts + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + + // When Opening a thread report with the given details + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); + await waitForBatchedUpdates(); + + // Then The iou action has the transaction report id as a child report ID + const allReportActions = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connectionID); + resolve(actions); + }, + }); + }); + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + if (transaction && createIOUAction) { + IOU.editMoneyRequest( + transaction, + thread.reportID, + {amount: 20000, comment: 'Double the amount!'}, + { + id: '123', + role: 'user', + type: 'free', + name: '', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + }, + {}, + {}, + ); + } + await waitForBatchedUpdates(); + + // Verify there are two actions (created + changelog) + expect(Object.values(reportActions ?? {}).length).toBe(2); + + // Fetch the updated IOU Action from Onyx + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + resolve(); + }, + }); + }); + + if (transaction && createIOUAction) { + // When Deleting a money request + IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + } + await waitForBatchedUpdates(); + + // Then, the report for the given thread ID does not exist + const report = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (reportData) => { + Onyx.disconnect(connectionID); + resolve(reportData); + }, + }); + }); + + expect(report).toBeFalsy(); + }); + + it('does not delete the transaction thread if there are visible comments in the thread', async () => { + // Given initial environment is set up + await waitForBatchedUpdates(); + + // Given a transaction thread + thread = ReportUtils.buildTransactionThread(createIOUAction, iouReport); + + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + jest.advanceTimersByTime(10); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); + await waitForBatchedUpdates(); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: true, + callback: (report) => { + Onyx.disconnect(connectionID); + expect(report).toBeTruthy(); + resolve(); + }, + }); + }); + + jest.advanceTimersByTime(10); + + // When a comment is added + Report.addComment(thread.reportID, 'Testing a comment'); + await waitForBatchedUpdates(); + + // Then comment details should match the expected report action + const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT); + reportActionID = resultAction?.reportActionID ?? ''; + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + + await waitForBatchedUpdates(); + + // Then the report should have 2 actions + expect(Object.values(reportActions ?? {}).length).toBe(2); + const resultActionAfter = reportActions?.[reportActionID]; + expect(resultActionAfter?.pendingAction).toBeUndefined(); + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + + if (transaction && createIOUAction) { + // When deleting money request + IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); + } + await waitForBatchedUpdates(); + + // Then the transaction thread report should still exist + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (report) => { + Onyx.disconnect(connectionID); + expect(report).toBeTruthy(); + resolve(); + }, + }); + }); + + // When fetch resumes + // Then the transaction thread report should still exist + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: true, + callback: (report) => { + Onyx.disconnect(connectionID); + expect(report).toBeTruthy(); + resolve(); + }, + }); + }); + }); + + it('update the moneyRequestPreview to show [Deleted request] when appropriate', async () => { + await waitForBatchedUpdates(); + + // Given a thread report + + jest.advanceTimersByTime(10); + thread = ReportUtils.buildTransactionThread(createIOUAction, iouReport); + + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); + + await waitForBatchedUpdates(); + + const allReportActions = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connectionID); + resolve(actions); + }, + }); + }); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + // Given an added comment to the thread report + + jest.advanceTimersByTime(10); + + Report.addComment(thread.reportID, 'Testing a comment'); + await waitForBatchedUpdates(); + + // Fetch the updated IOU Action from Onyx due to addition of comment to transaction thread. + // This needs to be fetched as `deleteMoneyRequest` depends on `childVisibleActionCount` in `createIOUAction`. + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + resolve(); + }, + }); + }); + + let resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT); + reportActionID = resultAction?.reportActionID ?? ''; + + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + expect(resultAction?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Verify there are three actions (created + addcomment) and our optimistic comment has been removed + expect(Object.values(reportActions ?? {}).length).toBe(2); + + let resultActionAfterUpdate = reportActions?.[reportActionID]; + + // Verify that our action is no longer in the loading state + expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Given an added comment to the IOU report + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + Report.addComment(IOU_REPORT_ID, 'Testing a comment'); + await waitForBatchedUpdates(); + + resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT); + reportActionID = resultAction?.reportActionID ?? ''; + + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + expect(resultAction?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed + expect(Object.values(reportActions ?? {}).length).toBe(3); + + resultActionAfterUpdate = reportActions?.[reportActionID]; + + // Verify that our action is no longer in the loading state + expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + if (transaction && createIOUAction) { + // When we delete the money request + IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false); + } + await waitForBatchedUpdates(); + + // Then we expect the moneyRequestPreview to show [Deleted request] + + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction?.message?.[0].isDeletedParentAction).toBeTruthy(); + resolve(); + }, + }); + }); + + // When we resume fetch + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + + // Then we expect the moneyRequestPreview to show [Deleted request] + + await new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction?.message?.[0].isDeletedParentAction).toBeTruthy(); + resolve(); + }, + }); + }); + }); + + it('update IOU report and reportPreview with new totals and messages if the IOU report is not deleted', async () => { + await waitForBatchedUpdates(); + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + callback: (val) => (iouReport = val), + }); + await waitForBatchedUpdates(); + + // Given a second money request in addition to the first one + + jest.advanceTimersByTime(10); + const amount2 = 20000; + const comment2 = 'Send me money please 2'; + if (chatReport) { + IOU.requestMoney(chatReport, amount2, CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment2, {}); + } + + await waitForBatchedUpdates(); + + // Then we expect the IOU report and reportPreview to update with new totals + + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(30000); + + const ioupreview = ReportActionsUtils.getReportPreviewAction(chatReport?.reportID ?? '', iouReport?.reportID ?? ''); + expect(ioupreview).toBeTruthy(); + expect(ioupreview?.message?.[0].text).toBe('rory@expensifail.com owes $300.00'); + + // When we delete the first money request + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + jest.advanceTimersByTime(10); + if (transaction && createIOUAction) { + IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false); + } + await waitForBatchedUpdates(); + + // Then we expect the IOU report and reportPreview to update with new totals + + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(20000); + + // When we resume + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + + // Then we expect the IOU report and reportPreview to update with new totals + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(20000); + }); + + it('navigate the user correctly to the iou Report when appropriate', async () => { + await waitForBatchedUpdates(); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + // Given an added comment to the iou report + + jest.advanceTimersByTime(10); + + Report.addComment(IOU_REPORT_ID, 'Testing a comment'); + await waitForBatchedUpdates(); + + const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT); + reportActionID = resultAction?.reportActionID; + + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + + await waitForBatchedUpdates(); + + // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed + expect(Object.values(reportActions ?? {}).length).toBe(3); + + await waitForBatchedUpdates(); + + // Given a thread report + + jest.advanceTimersByTime(10); + thread = ReportUtils.buildTransactionThread(createIOUAction, iouReport); + + expect(thread.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread?.participantAccountIDs ?? []); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction?.reportActionID); + await waitForBatchedUpdates(); + + const allReportActions = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connectionID); + resolve(actions); + }, + }); + }); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + // When we delete the money request in SingleTransactionView and we should not delete the IOU report + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + + if (transaction && createIOUAction) { + IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, true); + } + await waitForBatchedUpdates(); + + let allReports = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connectionID); + resolve(reports); + }, + }); + }); + + await waitForBatchedUpdates(); + + iouReport = Object.values(allReports ?? {}).find((report) => ReportUtils.isIOUReport(report)) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.resume(); + + allReports = await new Promise>((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connectionID); + resolve(reports); + }, + }); + }); + + iouReport = Object.values(allReports ?? {}).find((report) => ReportUtils.isIOUReport(report)) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // Then we expect to navigate to the iou report + + expect(Navigation.goBack).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(IOU_REPORT_ID)); + }); + + it('navigate the user correctly to the chat Report when appropriate', () => { + if (transaction && createIOUAction) { + // When we delete the money request and we should delete the IOU report + IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false); + } + // Then we expect to navigate to the chat report + expect(Navigation.goBack).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID ?? '')); + }); + }); + + describe('submitReport', () => { + it('correctly submits a report', () => { + const amount = 10000; + const comment = '💸💸💸💸'; + const merchant = 'NASDAQ'; + let expenseReport: OnyxEntry; + let chatReport: OnyxEntry; + return waitForBatchedUpdates() + .then(() => { + PolicyActions.createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT) ?? null; + + resolve(); + }, + }); + }), + ) + .then(() => { + if (chatReport) { + IOU.requestMoney( + chatReport, + amount, + CONST.CURRENCY.USD, + '', + merchant, + RORY_EMAIL, + RORY_ACCOUNT_ID, + {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, + comment, + {}, + ); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE) ?? null; + Onyx.merge(`report_${expenseReport?.reportID}`, { + statusNum: 0, + stateNum: 0, + }); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE) ?? null; + + // Verify report is a draft + expect(expenseReport?.stateNum).toBe(0); + expect(expenseReport?.statusNum).toBe(0); + resolve(); + }, + }); + }), + ) + .then(() => { + if (expenseReport) { + IOU.submitReport(expenseReport); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE) ?? null; + + // Report was submitted correctly + expect(expenseReport?.stateNum).toBe(1); + expect(expenseReport?.statusNum).toBe(1); + resolve(); + }, + }); + }), + ); + }); + it('correctly implements error handling', () => { + const amount = 10000; + const comment = '💸💸💸💸'; + const merchant = 'NASDAQ'; + let expenseReport: OnyxEntry; + let chatReport: OnyxEntry; + return waitForBatchedUpdates() + .then(() => { + PolicyActions.createWorkspace(CARLOS_EMAIL, true, "Carlos's Workspace"); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT) ?? null; + + resolve(); + }, + }); + }), + ) + .then(() => { + if (chatReport) { + IOU.requestMoney( + chatReport, + amount, + CONST.CURRENCY.USD, + '', + merchant, + RORY_EMAIL, + RORY_ACCOUNT_ID, + {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, + comment, + {}, + ); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE) ?? null; + Onyx.merge(`report_${expenseReport?.reportID}`, { + statusNum: 0, + stateNum: 0, + }); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE) ?? null; + + // Verify report is a draft + expect(expenseReport?.stateNum).toBe(0); + expect(expenseReport?.statusNum).toBe(0); + resolve(); + }, + }); + }), + ) + .then(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.fail(); + if (expenseReport) { + IOU.submitReport(expenseReport); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE) ?? null; + + // Report was submitted with some fail + expect(expenseReport?.stateNum).toBe(0); + expect(expenseReport?.statusNum).toBe(0); + resolve(); + }, + }); + }), + ); + }); + }); +});