Skip to content
Merged
39 changes: 20 additions & 19 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
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';
Expand Down Expand Up @@ -929,7 +928,7 @@
parentReportActionParam?: OnyxInputOrEntry<ReportAction>;
personalDetails?: Partial<PersonalDetailsList>;
invoiceReceiverPolicy?: OnyxEntry<Policy>;
transactions?: SearchTransaction[];
transactions?: Transaction[];
reports?: Report[];
policies?: Policy[];
isReportArchived?: boolean;
Expand Down Expand Up @@ -961,7 +960,7 @@
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeReportID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 963 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportID = value;
Expand All @@ -969,7 +968,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 971 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -987,7 +986,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 989 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (currentUserAccountID) {
Expand All @@ -999,14 +998,14 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 1001 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 1008 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand All @@ -1021,7 +1020,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 1023 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -1059,14 +1058,14 @@
});

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({

Check warning on line 1061 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETAS,
callback: (value) => (allBetas = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
let reportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({

Check warning on line 1068 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1092,7 +1091,7 @@
});

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 1094 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -1105,7 +1104,7 @@

let allReportMetadata: OnyxCollection<ReportMetadata>;
const allReportMetadataKeyValue: Record<string, ReportMetadata> = {};
Onyx.connect({

Check warning on line 1107 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -1918,9 +1917,11 @@
/**
* 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<Report>, policy?: OnyxEntry<Policy>, iouType?: string) {
function shouldEnableNegative(report: OnyxEntry<Report>, policy?: OnyxEntry<Policy>, 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 ?? '');
Expand Down Expand Up @@ -2251,7 +2252,7 @@
/**
* Whether the provided report has expenses
*/
function hasExpenses(reportID?: string, transactions?: SearchTransaction[] | Array<OnyxEntry<Transaction>>): boolean {
function hasExpenses(reportID?: string, transactions?: Array<OnyxEntry<Transaction>>): boolean {
if (transactions) {
return !!transactions?.find((transaction) => transaction?.reportID === reportID);
}
Expand All @@ -2261,7 +2262,7 @@
/**
* Whether the provided report is a closed expense report with no expenses
*/
function isClosedExpenseReportWithNoExpenses(report: OnyxEntry<Report>, transactions?: SearchTransaction[] | Array<OnyxEntry<Transaction>>): boolean {
function isClosedExpenseReportWithNoExpenses(report: OnyxEntry<Report>, transactions?: Array<OnyxEntry<Transaction>>): boolean {
if (!report?.statusNum || report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED || !isExpenseReport(report)) {
return false;
}
Expand Down Expand Up @@ -4484,7 +4485,7 @@
isChatReportArchived = false,
report?: OnyxInputOrEntry<Report>,
policy?: OnyxEntry<Policy>,
linkedTransaction?: OnyxEntry<Transaction> | SearchTransaction,
linkedTransaction?: OnyxEntry<Transaction>,
): boolean {
const isDeleted = isDeletedAction(reportAction);

Expand Down Expand Up @@ -4626,7 +4627,7 @@
isDeleteAction?: boolean,
isChatReportArchived = false,
outstandingReportsByPolicyID?: OutstandingReportsByPolicyIDDerivedValue,
linkedTransaction?: OnyxEntry<Transaction> | SearchTransaction,
linkedTransaction?: OnyxEntry<Transaction>,
report?: OnyxInputOrEntry<Report>,
policy?: OnyxEntry<Policy>,
): boolean {
Expand Down Expand Up @@ -4924,7 +4925,7 @@
*
* 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<ReportAction | OptimisticIOUReportAction>, transactions?: SearchTransaction[]): OnyxEntry<Transaction> | SearchTransaction {
function getLinkedTransaction(reportAction: OnyxEntry<ReportAction | OptimisticIOUReportAction>, transactions?: Transaction[]): OnyxEntry<Transaction> {
let transactionID: string | undefined;

if (isMoneyRequestAction(reportAction)) {
Expand Down Expand Up @@ -4979,7 +4980,7 @@
reports,
}: {
reportAction: OnyxEntry<ReportAction | OptimisticIOUReportAction>;
transactions?: SearchTransaction[];
transactions?: Transaction[];
reports?: Report[];
}): string {
if (isReversedTransaction(reportAction)) {
Expand Down Expand Up @@ -5609,7 +5610,7 @@
personalDetails?: Partial<PersonalDetailsList>,
invoiceReceiverPolicy?: OnyxEntry<Policy>,
reportAttributes?: ReportAttributesDerivedValue['reports'],
transactions?: SearchTransaction[],
transactions?: Transaction[],
isReportArchived?: boolean,
reports?: Report[],
policies?: Policy[],
Expand Down Expand Up @@ -8938,7 +8939,7 @@
reportID: string | undefined,
transactionViolations: OnyxCollection<TransactionViolation[]>,
shouldShowInReview?: boolean,
reportTransactions?: SearchTransaction[],
reportTransactions?: Transaction[],
): boolean {
const transactions = reportTransactions ?? getReportTransactions(reportID);
return transactions.some((transaction) => hasViolation(transaction, transactionViolations, shouldShowInReview));
Expand All @@ -8951,7 +8952,7 @@
reportID: string | undefined,
transactionViolations: OnyxCollection<TransactionViolation[]>,
shouldShowInReview?: boolean,
reportTransactions?: SearchTransaction[],
reportTransactions?: Transaction[],
): boolean {
const transactions = reportTransactions ?? getReportTransactions(reportID);
return transactions.some((transaction) => hasWarningTypeViolation(transaction, transactionViolations, shouldShowInReview));
Expand Down Expand Up @@ -8984,7 +8985,7 @@
reportID: string | undefined,
transactionViolations: OnyxCollection<TransactionViolation[]>,
shouldShowInReview?: boolean,
reportTransactions?: SearchTransaction[],
reportTransactions?: Transaction[],
): boolean {
const transactions = reportTransactions ?? getReportTransactions(reportID);
return transactions.some((transaction) => hasNoticeTypeViolation(transaction, transactionViolations, shouldShowInReview));
Expand All @@ -8993,7 +8994,7 @@
/**
* Checks to see if a report contains any type of violation
*/
function hasAnyViolations(reportID: string | undefined, transactionViolations: OnyxCollection<TransactionViolation[]>, reportTransactions?: SearchTransaction[]) {
function hasAnyViolations(reportID: string | undefined, transactionViolations: OnyxCollection<TransactionViolation[]>, reportTransactions?: Transaction[]) {
return (
hasViolations(reportID, transactionViolations, undefined, reportTransactions) ||
hasNoticeTypeViolations(reportID, transactionViolations, true, reportTransactions) ||
Expand All @@ -9017,12 +9018,12 @@
reportID: string | undefined,
transactionViolations: OnyxCollection<TransactionViolation[]>,
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 = {
Expand Down Expand Up @@ -10375,7 +10376,7 @@
/**
* 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));
Expand All @@ -10384,7 +10385,7 @@
/**
* 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));
Expand Down
2 changes: 1 addition & 1 deletion src/pages/iou/request/step/IOURequestStepAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
36 changes: 24 additions & 12 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1664,17 +1663,16 @@ describe('ReportUtils', () => {
currency: 'USD',
},
};
// eslint-disable-next-line @typescript-eslint/no-deprecated
const transaction: SearchTransaction = {

const transaction: Transaction = {
transactionID: 'txn1',
reportID: '2',
amount: 1000,
currency: 'USD',
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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -8674,30 +8686,30 @@ 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);
});

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);
});

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();
});
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Loading