diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d9a7179de534..90571ffb883e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -75,7 +75,6 @@ import type {NotificationPreference, Participants, Participant as ReportParticip import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction'; import type {PendingChatMember} from '@src/types/onyx/ReportMetadata'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {SearchTransaction} from '@src/types/onyx/SearchResults'; import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type {FileObject} from '@src/types/utils/Attachment'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -929,7 +928,7 @@ type GetReportNameParams = { parentReportActionParam?: OnyxInputOrEntry; personalDetails?: Partial; invoiceReceiverPolicy?: OnyxEntry; - transactions?: SearchTransaction[]; + transactions?: Transaction[]; reports?: Report[]; policies?: Policy[]; isReportArchived?: boolean; @@ -1918,9 +1917,11 @@ function isSelfDMOrSelfDMThread(report: OnyxEntry): boolean { /** * Returns true if the report is an expense report, a group policy, a self-DM, or the iouType is create, and the iouType is not split or invoice. */ -function shouldEnableNegative(report: OnyxEntry, policy?: OnyxEntry, iouType?: string) { +function shouldEnableNegative(report: OnyxEntry, policy?: OnyxEntry, iouType?: string, participants?: Participant[]) { const isSelfDMReport = isSelfDMOrSelfDMThread(report); - const isFirstTimeCreatingReport = !report && !policy && iouType === CONST.IOU.TYPE.SUBMIT; + + const isUserInRecipients = participants?.some((participant) => !participant.isSender && !participant.isPolicyExpenseChat && participant.accountID); + const isFirstTimeCreatingReport = !report && !policy && iouType === CONST.IOU.TYPE.SUBMIT && !isUserInRecipients; const isExpenseReportType = isExpenseReport(report); const isGroupPolicyType = isGroupPolicy(policy?.type ?? ''); @@ -2251,7 +2252,7 @@ function findLastAccessedReport(ignoreDomainRooms: boolean, openOnAdminRoom = fa /** * Whether the provided report has expenses */ -function hasExpenses(reportID?: string, transactions?: SearchTransaction[] | Array>): boolean { +function hasExpenses(reportID?: string, transactions?: Array>): boolean { if (transactions) { return !!transactions?.find((transaction) => transaction?.reportID === reportID); } @@ -2261,7 +2262,7 @@ function hasExpenses(reportID?: string, transactions?: SearchTransaction[] | Arr /** * Whether the provided report is a closed expense report with no expenses */ -function isClosedExpenseReportWithNoExpenses(report: OnyxEntry, transactions?: SearchTransaction[] | Array>): boolean { +function isClosedExpenseReportWithNoExpenses(report: OnyxEntry, transactions?: Array>): boolean { if (!report?.statusNum || report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED || !isExpenseReport(report)) { return false; } @@ -4484,7 +4485,7 @@ function canEditMoneyRequest( isChatReportArchived = false, report?: OnyxInputOrEntry, policy?: OnyxEntry, - linkedTransaction?: OnyxEntry | SearchTransaction, + linkedTransaction?: OnyxEntry, ): boolean { const isDeleted = isDeletedAction(reportAction); @@ -4626,7 +4627,7 @@ function canEditFieldOfMoneyRequest( isDeleteAction?: boolean, isChatReportArchived = false, outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue, - linkedTransaction?: OnyxEntry | SearchTransaction, + linkedTransaction?: OnyxEntry, report?: OnyxInputOrEntry, policy?: OnyxEntry, ): boolean { @@ -4924,7 +4925,7 @@ function areAllRequestsBeingSmartScanned(iouReportID: string | undefined, report * * NOTE: This method is only meant to be used inside this action file. Do not export and use it elsewhere. Use useOnyx instead. */ -function getLinkedTransaction(reportAction: OnyxEntry, transactions?: SearchTransaction[]): OnyxEntry | SearchTransaction { +function getLinkedTransaction(reportAction: OnyxEntry, transactions?: Transaction[]): OnyxEntry { let transactionID: string | undefined; if (isMoneyRequestAction(reportAction)) { @@ -4979,7 +4980,7 @@ function getTransactionReportName({ reports, }: { reportAction: OnyxEntry; - transactions?: SearchTransaction[]; + transactions?: Transaction[]; reports?: Report[]; }): string { if (isReversedTransaction(reportAction)) { @@ -5609,7 +5610,7 @@ function getReportName( personalDetails?: Partial, invoiceReceiverPolicy?: OnyxEntry, reportAttributes?: ReportAttributesDerivedValue['reports'], - transactions?: SearchTransaction[], + transactions?: Transaction[], isReportArchived?: boolean, reports?: Report[], policies?: Policy[], @@ -8938,7 +8939,7 @@ function hasViolations( reportID: string | undefined, transactionViolations: OnyxCollection, shouldShowInReview?: boolean, - reportTransactions?: SearchTransaction[], + reportTransactions?: Transaction[], ): boolean { const transactions = reportTransactions ?? getReportTransactions(reportID); return transactions.some((transaction) => hasViolation(transaction, transactionViolations, shouldShowInReview)); @@ -8951,7 +8952,7 @@ function hasWarningTypeViolations( reportID: string | undefined, transactionViolations: OnyxCollection, shouldShowInReview?: boolean, - reportTransactions?: SearchTransaction[], + reportTransactions?: Transaction[], ): boolean { const transactions = reportTransactions ?? getReportTransactions(reportID); return transactions.some((transaction) => hasWarningTypeViolation(transaction, transactionViolations, shouldShowInReview)); @@ -8984,7 +8985,7 @@ function hasNoticeTypeViolations( reportID: string | undefined, transactionViolations: OnyxCollection, shouldShowInReview?: boolean, - reportTransactions?: SearchTransaction[], + reportTransactions?: Transaction[], ): boolean { const transactions = reportTransactions ?? getReportTransactions(reportID); return transactions.some((transaction) => hasNoticeTypeViolation(transaction, transactionViolations, shouldShowInReview)); @@ -8993,7 +8994,7 @@ function hasNoticeTypeViolations( /** * Checks to see if a report contains any type of violation */ -function hasAnyViolations(reportID: string | undefined, transactionViolations: OnyxCollection, reportTransactions?: SearchTransaction[]) { +function hasAnyViolations(reportID: string | undefined, transactionViolations: OnyxCollection, reportTransactions?: Transaction[]) { return ( hasViolations(reportID, transactionViolations, undefined, reportTransactions) || hasNoticeTypeViolations(reportID, transactionViolations, true, reportTransactions) || @@ -9017,12 +9018,12 @@ function shouldBlockSubmitDueToStrictPolicyRules( reportID: string | undefined, transactionViolations: OnyxCollection, areStrictPolicyRulesEnabled: boolean, - reportTransactions?: Transaction[] | SearchTransaction[], + reportTransactions?: Transaction[], ) { if (!areStrictPolicyRulesEnabled) { return false; } - return hasAnyViolations(reportID, transactionViolations, reportTransactions as SearchTransaction[]); + return hasAnyViolations(reportID, transactionViolations, reportTransactions); } type ReportErrorsAndReportActionThatRequiresAttention = { @@ -10375,7 +10376,7 @@ function getAllHeldTransactions(iouReportID?: string): Transaction[] { /** * Check if Report has any held expenses */ -function hasHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean { +function hasHeldExpenses(iouReportID?: string, allReportTransactions?: Transaction[]): boolean { const iouReportTransactions = getReportTransactions(iouReportID); const transactions = allReportTransactions ?? iouReportTransactions; return transactions.some((transaction) => isOnHoldTransactionUtils(transaction)); @@ -10384,7 +10385,7 @@ function hasHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTra /** * Check if all expenses in the Report are on hold */ -function hasOnlyHeldExpenses(iouReportID?: string, allReportTransactions?: SearchTransaction[]): boolean { +function hasOnlyHeldExpenses(iouReportID?: string, allReportTransactions?: Transaction[]): boolean { const transactionsByIouReportID = getReportTransactions(iouReportID); const reportTransactions = allReportTransactions ?? transactionsByIouReportID; return reportTransactions.length > 0 && !reportTransactions.some((transaction) => !isOnHoldTransactionUtils(transaction)); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index e96640d0fa61..d6ff8ae34078 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -99,7 +99,7 @@ function IOURequestStepAmount({ const isSubmitType = iouType === CONST.IOU.TYPE.SUBMIT; const isEditingSplitBill = isEditing && isSplitBill; const currentTransaction = isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction; - const allowNegative = shouldEnableNegative(report, policy, iouType); + const allowNegative = shouldEnableNegative(report, policy, iouType, transaction?.participants); const disableOppositeConversion = isCreateAction || (isSubmitType && isSubmitAction); const {amount: transactionAmount} = getTransactionDetails(currentTransaction, undefined, undefined, allowNegative, disableOppositeConversion) ?? {amount: 0}; const {currency: originalCurrency} = getTransactionDetails(isEditing && !isEmptyObject(draftTransaction) ? draftTransaction : transaction) ?? {currency: CONST.CURRENCY.USD}; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 18a76121814e..f1300f8b6274 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -133,7 +133,6 @@ import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback} from '@src/types import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import type {ACHAccount} from '@src/types/onyx/Policy'; import type {Participant, Participants} from '@src/types/onyx/Report'; -import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {actionR14932 as mockIOUAction} from '../../__mocks__/reportData/actions'; import {chatReportR14932 as mockedChatReport, iouReportR14932 as mockIOUReport} from '../../__mocks__/reportData/reports'; @@ -1664,8 +1663,8 @@ describe('ReportUtils', () => { currency: 'USD', }, }; - // eslint-disable-next-line @typescript-eslint/no-deprecated - const transaction: SearchTransaction = { + + const transaction: Transaction = { transactionID: 'txn1', reportID: '2', amount: 1000, @@ -1673,8 +1672,7 @@ describe('ReportUtils', () => { merchant: 'Test Merchant', created: testDate, modifiedMerchant: 'Test Merchant', - // eslint-disable-next-line @typescript-eslint/no-deprecated - } as SearchTransaction; + } as Transaction; const reportName = getSearchReportName({ report: baseExpenseReport, @@ -8205,6 +8203,20 @@ describe('ReportUtils', () => { expect(shouldEnableNegative(undefined, undefined, CONST.IOU.TYPE.CREATE)).toBe(true); }); + it('should return true when report is null and iouType is SUBMIT', () => { + expect(shouldEnableNegative(undefined, undefined, CONST.IOU.TYPE.SUBMIT)).toBe(true); + }); + + it('should return true when report is null, the iouType is SUBMIT, and the receipts do not include a user', () => { + const participants = [{accountID: 0, isPolicyExpenseChat: true, isSender: false}]; + expect(shouldEnableNegative(undefined, undefined, CONST.IOU.TYPE.SUBMIT, participants)).toBe(true); + }); + + it('should return false when report is null, the iouType is SUBMIT, and the receipts include a user', () => { + const participants = [{accountID: 1, isPolicyExpenseChat: false, isSender: false}]; + expect(shouldEnableNegative(undefined, undefined, CONST.IOU.TYPE.SUBMIT, participants)).toBe(false); + }); + it('should handle undefined policy type gracefully', () => { const policyWithUndefinedType = { ...createRandomPolicy(4), @@ -8644,7 +8656,7 @@ describe('ReportUtils', () => { const mockReportIDIndex = 1; const mockReportID = mockReportIDIndex.toString(); // eslint-disable-next-line @typescript-eslint/no-deprecated - const mockSearchReport: SearchReport = { + const mockSearchReport: Report = { ...createRandomReport(mockReportIDIndex, undefined), reportName: 'Search Report', type: CONST.REPORT.TYPE.CHAT, @@ -8674,7 +8686,7 @@ describe('ReportUtils', () => { test('returns onyx report when search report is not found but onyx report exists', async () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const searchReports: SearchReport[] = []; + const searchReports: Report[] = []; await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); const result = getReportOrDraftReport(mockReportID, searchReports); expect(result).toEqual(mockOnyxReport); @@ -8682,7 +8694,7 @@ describe('ReportUtils', () => { test('returns draft report when neither search nor onyx report exists but draft exists', async () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const searchReports: SearchReport[] = []; + const searchReports: Report[] = []; await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${mockReportID}`, mockDraftReport); const result = getReportOrDraftReport(mockReportID, searchReports); expect(result).toEqual(mockDraftReport); @@ -8690,14 +8702,14 @@ describe('ReportUtils', () => { test('returns fallback report when no other reports exist', () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const searchReports: SearchReport[] = []; + const searchReports: Report[] = []; const result = getReportOrDraftReport('unknownReportID', searchReports, mockFallbackReport); expect(result).toEqual(mockFallbackReport); }); test('returns undefined when no reports exist and no fallback provided', () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const searchReports: SearchReport[] = []; + const searchReports: Report[] = []; const result = getReportOrDraftReport(mockReportID, searchReports); expect(result).toBeUndefined(); }); @@ -8729,7 +8741,7 @@ describe('ReportUtils', () => { test('prioritizes onyx report over draft report when both exist', async () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const searchReports: SearchReport[] = []; + const searchReports: Report[] = []; await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${mockReportID}`, mockDraftReport); const result = getReportOrDraftReport(mockReportID, searchReports); @@ -8739,7 +8751,7 @@ describe('ReportUtils', () => { test('prioritizes draft report over fallback when both exist', async () => { // eslint-disable-next-line @typescript-eslint/no-deprecated - const searchReports: SearchReport[] = []; + const searchReports: Report[] = []; await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${mockReportID}`, mockDraftReport); const result = getReportOrDraftReport(mockReportID, searchReports, mockFallbackReport); expect(result).toEqual(mockDraftReport);