diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index e40facb21c48..1aa5f5fe101e 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -33,6 +33,34 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getDistanceInMeters from './getDistanceInMeters'; +type TransactionParams = { + amount: number; + currency: string; + reportID: string; + comment?: string; + attendees?: Attendee[]; + created?: string; + merchant?: string; + receipt?: OnyxEntry; + category?: string; + tag?: string; + taxCode?: string; + taxAmount?: number; + billable?: boolean; + pendingFields?: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}>; + reimbursable?: boolean; + source?: string; + filename?: string; +}; + +type BuildOptimisticTransactionParams = { + originalTransactionID?: string; + existingTransactionID?: string; + existingTransaction?: OnyxEntry; + policy?: OnyxEntry; + transactionParams: TransactionParams; +}; + let allTransactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, @@ -130,29 +158,27 @@ function isManualRequest(transaction: Transaction): boolean { * @param [existingTransactionID] When creating a distance expense, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have * it's transactionID match what was already generated. */ -function buildOptimisticTransaction( - amount: number, - currency: string, - reportID: string, - comment = '', - attendees: Attendee[] = [], - created = '', - source = '', - originalTransactionID = '', - merchant = '', - receipt?: OnyxEntry, - filename = '', - existingTransactionID: string | null = null, - category = '', - tag = '', - taxCode = '', - taxAmount = 0, - billable = false, - pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}> | undefined = undefined, - reimbursable = true, - existingTransaction: OnyxEntry | undefined = undefined, - policy: OnyxEntry = undefined, -): Transaction { +function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): Transaction { + const {originalTransactionID = '', existingTransactionID, existingTransaction, policy, transactionParams} = params; + const { + amount, + currency, + reportID, + comment = '', + attendees = [], + created = '', + merchant = '', + receipt, + category = '', + tag = '', + taxCode = '', + taxAmount = 0, + billable = false, + pendingFields, + reimbursable = true, + source = '', + filename = '', + } = transactionParams; // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = existingTransactionID ?? NumberUtils.rand64(); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 2658f8925e5c..405976ab425c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2144,25 +2144,23 @@ function getSendInvoiceInformation( receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; filename = receipt.name; } - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction( - amount, - currency, - optimisticInvoiceReport.reportID, - trimmedComment, - [], - created, - '', - '', - merchant, - receiptObject, - filename, - undefined, - category, - tag, - taxCode, - taxAmount, - billable, - ); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount, + currency, + reportID: optimisticInvoiceReport.reportID, + comment: trimmedComment, + created, + merchant, + receipt: receiptObject, + category, + tag, + taxCode, + taxAmount, + billable, + filename, + }, + }); const optimisticPolicyRecentlyUsedCategories = Category.buildOptimisticPolicyRecentlyUsedCategories(optimisticInvoiceReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); @@ -2312,29 +2310,27 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma // STEP 3: Build an optimistic transaction with the receipt const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; - let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( - ReportUtils.isExpenseReport(iouReport) ? -amount : amount, - currency, - iouReport.reportID, - comment, - attendees, - created, - '', - '', - merchant, - receipt, - '', + let optimisticTransaction = TransactionUtils.buildOptimisticTransaction({ existingTransactionID, - category, - tag, - taxCode, - ReportUtils.isExpenseReport(iouReport) ? -(taxAmount ?? 0) : taxAmount, - billable, - isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, - undefined, existingTransaction, policy, - ); + transactionParams: { + amount: ReportUtils.isExpenseReport(iouReport) ? -amount : amount, + currency, + reportID: iouReport.reportID, + comment, + attendees, + created, + merchant, + receipt, + category, + tag, + taxCode, + taxAmount: ReportUtils.isExpenseReport(iouReport) ? -(taxAmount ?? 0) : taxAmount, + billable, + pendingFields: isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + }, + }); const optimisticPolicyRecentlyUsedCategories = Category.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Tag.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); @@ -2571,29 +2567,28 @@ function getTrackExpenseInformation( filename = existingTransaction?.filename; } const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; - let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( - ReportUtils.isExpenseReport(iouReport) ? -amount : amount, - currency, - shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1', - comment, - [], - created, - '', - '', - merchant, - receiptObject, - filename, - existingTransactionID ?? null, - category, - tag, - taxCode, - taxAmount, - billable, - isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, - false, + let optimisticTransaction = TransactionUtils.buildOptimisticTransaction({ + existingTransactionID, existingTransaction, policy, - ); + transactionParams: { + amount: ReportUtils.isExpenseReport(iouReport) ? -amount : amount, + currency, + reportID: shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1', + comment, + created, + merchant, + receipt: receiptObject, + category, + tag, + taxCode, + taxAmount, + billable, + pendingFields: isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + reimbursable: false, + filename, + }, + }); // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction @@ -4263,28 +4258,24 @@ function createSplitsAndOnyxData( const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; - let splitTransaction = TransactionUtils.buildOptimisticTransaction( - amount, - currency, - CONST.REPORT.SPLIT_REPORTID, - comment, - [], - created, - '', - '', - merchant || Localize.translateLocal('iou.expense'), - receipt, - undefined, - undefined, - category, - tag, - taxCode, - taxAmount, - billable, - isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, - undefined, + let splitTransaction = TransactionUtils.buildOptimisticTransaction({ existingTransaction, - ); + transactionParams: { + amount, + currency, + reportID: CONST.REPORT.SPLIT_REPORTID, + comment, + created, + merchant: merchant || Localize.translateLocal('iou.expense'), + receipt, + category, + tag, + taxCode, + taxAmount, + billable, + pendingFields: isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + }, + }); // Important data is set on the draft distance transaction, such as the iouRequestType marking it as a distance request, so merge it into the optimistic split transaction if (isDistanceRequest) { @@ -4507,25 +4498,23 @@ function createSplitsAndOnyxData( } // STEP 3: Build optimistic transaction - const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction( - ReportUtils.isExpenseReport(oneOnOneIOUReport) ? -splitAmount : splitAmount, - currency, - oneOnOneIOUReport.reportID, - comment, - [], - created, - CONST.IOU.TYPE.SPLIT, - splitTransaction.transactionID, - merchant || Localize.translateLocal('iou.expense'), - undefined, - undefined, - undefined, - category, - tag, - taxCode, - ReportUtils.isExpenseReport(oneOnOneIOUReport) ? -splitTaxAmount : splitTaxAmount, - billable, - ); + const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction({ + originalTransactionID: splitTransaction.transactionID, + transactionParams: { + amount: ReportUtils.isExpenseReport(oneOnOneIOUReport) ? -splitAmount : splitAmount, + currency, + reportID: oneOnOneIOUReport.reportID, + comment, + created, + merchant: merchant || Localize.translateLocal('iou.expense'), + category, + tag, + taxCode, + taxAmount: ReportUtils.isExpenseReport(oneOnOneIOUReport) ? -splitTaxAmount : splitTaxAmount, + billable, + source: CONST.IOU.TYPE.SPLIT, + }, + }); // STEP 4: Build optimistic reportActions. We need: // 1. CREATED action for the chatReport @@ -4862,25 +4851,22 @@ function startSplitBill({ const receiptObject: Receipt = {state, source}; // ReportID is -2 (aka "deleted") on the group transaction - const splitTransaction = TransactionUtils.buildOptimisticTransaction( - 0, - currency, - CONST.REPORT.SPLIT_REPORTID, - comment, - [], - '', - '', - '', - CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - receiptObject, - filename, - undefined, - category, - tag, - taxCode, - taxAmount, - billable, - ); + const splitTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 0, + currency, + reportID: CONST.REPORT.SPLIT_REPORTID, + comment, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + receipt: receiptObject, + category, + tag, + taxCode, + taxAmount, + billable, + filename, + }, + }); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const splitChatCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); @@ -5273,25 +5259,25 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA oneOnOneIOUReport = IOUUtils.updateIOUOwnerAndTotal(oneOnOneIOUReport, sessionAccountID, splitAmount, currency ?? ''); } - const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction( - isPolicyExpenseChat ? -splitAmount : splitAmount, - currency ?? '', - oneOnOneIOUReport?.reportID ?? '-1', - updatedTransaction?.comment?.comment, - [], - updatedTransaction?.modifiedCreated, - CONST.IOU.TYPE.SPLIT, - transactionID, - updatedTransaction?.modifiedMerchant, - {...updatedTransaction?.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, - updatedTransaction?.filename, - undefined, - updatedTransaction?.category, - updatedTransaction?.tag, - updatedTransaction?.taxCode, - isPolicyExpenseChat ? -splitTaxAmount : splitAmount, - updatedTransaction?.billable, - ); + const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction({ + originalTransactionID: transactionID, + transactionParams: { + amount: isPolicyExpenseChat ? -splitAmount : splitAmount, + currency: currency ?? '', + reportID: oneOnOneIOUReport?.reportID ?? '-1', + comment: updatedTransaction?.comment?.comment, + created: updatedTransaction?.modifiedCreated, + merchant: updatedTransaction?.modifiedMerchant, + receipt: {...updatedTransaction?.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN}, + category: updatedTransaction?.category, + tag: updatedTransaction?.tag, + taxCode: updatedTransaction?.taxCode, + taxAmount: isPolicyExpenseChat ? -splitTaxAmount : splitAmount, + billable: updatedTransaction?.billable, + source: CONST.IOU.TYPE.SPLIT, + filename: updatedTransaction?.filename, + }, + }); const [oneOnOneCreatedActionForChat, oneOnOneCreatedActionForIOU, oneOnOneIOUAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( @@ -6297,7 +6283,14 @@ function getSendMoneyParams( } const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientAccountID, managerID, amount, chatReport.reportID, currency, true); - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency, optimisticIOUReport.reportID, comment); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount, + currency, + reportID: optimisticIOUReport.reportID, + comment, + }, + }); const optimisticTransactionData: OnyxUpdate = { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 415612afe414..ac212d546054 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3410,8 +3410,20 @@ describe('actions/IOU', () => { test('Resolving duplicates of two transaction by keeping one of them should properly set the other one on hold even if the transaction thread reports do not exist in onyx', () => { // Given two duplicate transactions const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); - const transaction1 = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); - const transaction2 = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); + const transaction1 = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + const transaction2 = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); const transactionCollectionDataSet: TransactionCollectionDataSet = { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`]: transaction2, diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 162711b85499..56aaafe1f80d 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -420,7 +420,13 @@ describe('SidebarLinksData', () => { // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); - const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: expenseReport.reportID, + }, + }); const expenseCreatedAction = ReportUtils.buildOptimisticIOUReportAction( 'create', 100, diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index 7031045e3f05..6eb96b57c804 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -28,8 +28,20 @@ describe('IOUUtils', () => { test('Submitting an expense offline in a different currency will show the pending conversion message', () => { const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); - const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); - const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'AED', iouReport.reportID); + const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'AED', + reportID: iouReport.reportID, + }, + }); const MergeQueries: TransactionCollectionDataSet = {}; MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`] = usdPendingTransaction; MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`] = aedPendingTransaction; @@ -42,8 +54,20 @@ describe('IOUUtils', () => { test('Submitting an expense online in a different currency will not show the pending conversion message', () => { const iouReport = ReportUtils.buildOptimisticIOUReport(2, 3, 100, '1', 'USD'); - const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); - const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction(100, 'AED', iouReport.reportID); + const usdPendingTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); + const aedPendingTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'AED', + reportID: iouReport.reportID, + }, + }); const MergeQueries: TransactionCollectionDataSet = {}; MergeQueries[`${ONYXKEYS.COLLECTION.TRANSACTION}${usdPendingTransaction.transactionID}`] = { @@ -150,14 +174,26 @@ describe('isValidMoneyRequestType', () => { describe('Check valid amount for IOU/Expense request', () => { test('IOU amount should be positive', () => { const iouReport = ReportUtils.buildOptimisticIOUReport(1, 2, 100, '1', 'USD'); - const iouTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', iouReport.reportID); + const iouTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: iouReport.reportID, + }, + }); const iouAmount = TransactionUtils.getAmount(iouTransaction, false, false); expect(iouAmount).toBeGreaterThan(0); }); test('Expense amount should be negative', () => { const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); - const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: expenseReport.reportID, + }, + }); const expenseAmount = TransactionUtils.getAmount(expenseTransaction, true, false); expect(expenseAmount).toBeLessThan(0); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 0f1f68c1cae3..bc3d5baa4bff 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1228,7 +1228,13 @@ describe('ReportUtils', () => { it('should return true when the report has outstanding violations', async () => { const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); - const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: expenseReport.reportID, + }, + }); const expenseCreatedAction1 = ReportUtils.buildOptimisticIOUReportAction( 'create', 100, @@ -1469,7 +1475,13 @@ describe('ReportUtils', () => { it('should return false when the report is the single transaction thread', async () => { const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); - const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount: 100, + currency: 'USD', + reportID: expenseReport.reportID, + }, + }); const expenseCreatedAction = ReportUtils.buildOptimisticIOUReportAction( 'create', 100, diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 558d09e6a0fa..505422068380 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -11,7 +11,16 @@ function generateTransaction(values: Partial = {}): Transaction { const comment = ''; const attendees: Attendee[] = []; const created = '2023-10-01'; - const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, attendees, created); + const baseValues = TransactionUtils.buildOptimisticTransaction({ + transactionParams: { + amount, + currency, + reportID, + comment, + attendees, + created, + }, + }); return {...baseValues, ...values}; }