Skip to content
2 changes: 1 addition & 1 deletion src/components/AddPaymentMethodMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function AddPaymentMethodMenu({
// Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report
// which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee.
const isIOU = isIOUReport(iouReport);
const canUseBusinessBankAccount = isExpenseReport(iouReport) || (isIOU && !hasRequestFromCurrentAccount(iouReport?.reportID, session?.accountID ?? CONST.DEFAULT_NUMBER_ID));
const canUseBusinessBankAccount = isExpenseReport(iouReport) || (isIOU && !hasRequestFromCurrentAccount(iouReport, session?.accountID ?? CONST.DEFAULT_NUMBER_ID));

const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOU;

Expand Down
2 changes: 1 addition & 1 deletion src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,7 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt
return [];
}

const canUseBusinessBankAccount = moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport.reportID, accountID ?? CONST.DEFAULT_NUMBER_ID);
const canUseBusinessBankAccount = moneyRequestReport?.reportID && !hasRequestFromCurrentAccount(moneyRequestReport, accountID ?? CONST.DEFAULT_NUMBER_ID);
if (!canUseBusinessBankAccount) {
return [];
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/SettlementButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ function SettlementButton({
};

const canUseWallet = !isExpenseReport && !isInvoiceReport && isCurrencySupportedForGlobalReimbursement(currency as CurrencyType);
const canUseBusinessBankAccount = isExpenseReport || (isIOUReport(iouReport) && reportID && !hasRequestFromCurrentAccount(reportID, accountID ?? CONST.DEFAULT_NUMBER_ID));
const canUseBusinessBankAccount = isExpenseReport || (isIOUReport(iouReport) && reportID && !hasRequestFromCurrentAccount(iouReport, accountID ?? CONST.DEFAULT_NUMBER_ID));
const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport(iouReport);
const isPersonalOnlyOption = canUsePersonalBankAccount && !canUseBusinessBankAccount;

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useBulkPayOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function useBulkPayOptions({
const isExpenseReport = isExpenseReportUtil(selectedReportID);
const isInvoiceReport = isInvoiceReportUtil(selectedReportID);
const shouldShowPayElsewhereOption = !isInvoiceReport;
const canUseBusinessBankAccount = isExpenseReport || (isIOUReport && selectedReportID && !hasRequestFromCurrentAccount(selectedReportID, accountID ?? CONST.DEFAULT_NUMBER_ID));
const canUseBusinessBankAccount = isExpenseReport || (isIOUReport && selectedReportID && !hasRequestFromCurrentAccount(iouReport, accountID ?? CONST.DEFAULT_NUMBER_ID));
const canUsePersonalBankAccount = isIOUReport;
const isPersonalOnlyOption = canUsePersonalBankAccount && !canUseBusinessBankAccount;
const shouldShowBusinessBankAccountOptions = isExpenseReport && !isPersonalOnlyOption;
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSelectionModeReportActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function useSelectionModeReportActions({
return [];
}

const canUseBusinessBankAccount = report?.reportID && !hasRequestFromCurrentAccount(report.reportID, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID);
const canUseBusinessBankAccount = report?.reportID && !hasRequestFromCurrentAccount(report, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID);
if (!canUseBusinessBankAccount) {
return [];
}
Expand Down
7 changes: 4 additions & 3 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
}

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

Check warning on line 92 in src/libs/ReportActionsUtils.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 @@ -101,7 +101,7 @@
});

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

Check warning on line 104 in src/libs/ReportActionsUtils.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 All @@ -110,13 +110,13 @@
});

let deprecatedIsNetworkOffline = false;
Onyx.connect({

Check warning on line 113 in src/libs/ReportActionsUtils.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.NETWORK,
callback: (val) => (deprecatedIsNetworkOffline = val?.isOffline ?? false),
});

let deprecatedCurrentUserAccountID: number | undefined;
Onyx.connect({

Check warning on line 119 in src/libs/ReportActionsUtils.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, value is undefined
Expand Down Expand Up @@ -2419,16 +2419,17 @@
* @param currentAccountID
* @returns
*/
function hasRequestFromCurrentAccount(reportID: string | undefined, currentAccountID: number): boolean {
function hasRequestFromCurrentAccount(report: OnyxEntry<Report>, currentAccountID: number): boolean {
const reportID = report?.reportID;
if (!reportID) {
return false;
}

const reportActions = Object.values(getAllReportActions(reportID));
if (reportActions.length === 0) {
// In case the reportActions of the report have not been loaded, we will check based on the transactions.
const report = getReportOrDraftReport(reportID);
return doesReportContainRequestsFromMultipleUsers(report, true);
const resolvedReport = getReportOrDraftReport(reportID, undefined, undefined, undefined, report);
return doesReportContainRequestsFromMultipleUsers(resolvedReport, true);
}

return reportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && action.actorAccountID === currentAccountID && !isDeletedAction(action));
Expand Down
12 changes: 9 additions & 3 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@
};

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

Check warning on line 1048 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) => {
conciergeReportIDOnyxConnect = value;
Expand All @@ -1053,7 +1053,7 @@
});

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

Check warning on line 1056 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 @@ -1071,7 +1071,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 1074 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 (deprecatedCurrentUserAccountID) {
Expand All @@ -1083,7 +1083,7 @@
});

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

Check warning on line 1086 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),
Expand All @@ -1091,7 +1091,7 @@

let allPolicies: OnyxCollection<Policy>;
let policiesArray: Policy[] = [];
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.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1101,7 +1101,7 @@
});

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

Check warning on line 1104 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_DRAFTS,
waitForCollectionCallback: true,
callback: (value) => (allPolicyDrafts = value),
Expand Down Expand Up @@ -1283,9 +1283,15 @@
/**
* Get the report or draft report given a reportID
*/
function getReportOrDraftReport(reportID: string | undefined, searchReports?: Array<OnyxEntry<Report>>, fallbackReport?: Report, reportDrafts?: OnyxCollection<Report>): OnyxEntry<Report> {
const searchReport = searchReports?.find((report) => report?.reportID === reportID);
const onyxReport = deprecatedAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
function getReportOrDraftReport(
Comment thread
truph01 marked this conversation as resolved.
reportID: string | undefined,
searchReports?: Array<OnyxEntry<Report>>,
fallbackReport?: Report,
reportDrafts?: OnyxCollection<Report>,
report?: OnyxEntry<Report>,
): OnyxEntry<Report> {
const searchReport = searchReports?.find((searchItem) => searchItem?.reportID === reportID);
const onyxReport = report ?? deprecatedAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
return searchReport ?? onyxReport ?? (reportDrafts ?? allReportsDraft)?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`] ?? fallbackReport;
}

Expand Down
5 changes: 2 additions & 3 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6068,11 +6068,10 @@ function moveIOUReportToPolicy(
if (!policy || !iouReport || !isIOUReportUsingReport(iouReport)) {
return;
}
const reportID = iouReport.reportID;
const isReimbursed = isReportManuallyReimbursed(iouReport);

// We do not want to create negative amount expenses
if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID) && !isFromSettlementButton) {
if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID) && !isFromSettlementButton) {
return;
}

Expand Down Expand Up @@ -6132,7 +6131,7 @@ function moveIOUReportToPolicyAndInviteSubmitter(
const isReimbursed = isReportManuallyReimbursed(iouReport);

// We only allow moving IOU report to a policy if it doesn't have requests from multiple users, as we do not want to create negative amount expenses
if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(reportID, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) {
if (!isReimbursed && ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, iouReport.managerID ?? CONST.DEFAULT_NUMBER_ID)) {
return;
}

Expand Down
88 changes: 88 additions & 0 deletions tests/actions/ReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3426,6 +3426,79 @@ describe('actions/Report', () => {
expect(Object.values(reportActions ?? {}).at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.MOVED);
});

it('should return undefined when iouReport is not an IOU report', () => {
const expenseReport: OnyxTypes.Report = {
...createRandomReport(1, undefined),
type: CONST.REPORT.TYPE.EXPENSE,
};
const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN};
const result = Report.moveIOUReportToPolicy(expenseReport, policy);
expect(result).toBeUndefined();
});

it('should return undefined when hasRequestFromCurrentAccount is true and isFromSettlementButton is false', async () => {
const currentUserAccountID = 12;
const iouReport: OnyxTypes.Report = {
...createRandomReport(1, undefined),
type: CONST.REPORT.TYPE.IOU,
managerID: currentUserAccountID,
};
const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN};

// Create a non-deleted IOU report action from the current account so hasRequestFromCurrentAccount returns true
const iouReportAction: OnyxTypes.ReportAction = {
reportActionID: '100',
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
actorAccountID: currentUserAccountID,
created: '2024-01-01 00:00:00',
message: [{html: '$100.00 expense', text: '$100.00 expense', type: 'COMMENT'}],
} as OnyxTypes.ReportAction;

await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, {[iouReportAction.reportActionID]: iouReportAction});
await waitForBatchedUpdates();

const result = Report.moveIOUReportToPolicy(iouReport, policy, false);
expect(result).toBeUndefined();
});

it('should proceed when hasRequestFromCurrentAccount is true but isFromSettlementButton is true', async () => {
const currentUserAccountID = 12;
const ownerAccountID = 1;
const ownerEmail = 'owner@gmail.com';
const adminEmail = 'admin@gmail.com';
const iouReport: OnyxTypes.Report = {
...createRandomReport(1, undefined),
type: CONST.REPORT.TYPE.IOU,
ownerAccountID,
managerID: currentUserAccountID,
};
const policy: OnyxTypes.Policy = {
...createRandomPolicy(1),
role: CONST.POLICY.ROLE.ADMIN,
employeeList: {[adminEmail]: {email: adminEmail, role: CONST.POLICY.ROLE.ADMIN}, [ownerEmail]: {email: ownerEmail, role: CONST.POLICY.ROLE.USER}},
};

// Create a non-deleted IOU report action from the current account so hasRequestFromCurrentAccount returns true
const iouReportAction: OnyxTypes.ReportAction = {
reportActionID: '100',
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
actorAccountID: currentUserAccountID,
created: '2024-01-01 00:00:00',
message: [{html: '$100.00 expense', text: '$100.00 expense', type: 'COMMENT'}],
} as OnyxTypes.ReportAction;

await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[ownerAccountID]: {login: ownerEmail}});
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport);
Comment thread
truph01 marked this conversation as resolved.
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, {[iouReportAction.reportActionID]: iouReportAction});
await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy);
await waitForBatchedUpdates();

// When isFromSettlementButton is true, it should proceed despite hasRequestFromCurrentAccount being true
const result = Report.moveIOUReportToPolicy(iouReport, policy, true);
expect(result).toBeDefined();
expect(result?.policyExpenseChatReportID).toBeDefined();
});

describe('convertIOUReportToExpenseReport', () => {
it('should set reportName to default formula when policy field list is empty', () => {
// Given a policy with an empty field list
Expand Down Expand Up @@ -3703,6 +3776,21 @@ describe('actions/Report', () => {
expect(updatedReport?.policyID).toBe(policy.id);
expect(updatedReport?.total).toBe(-3000);
});

it('should return undefined when policy is missing', () => {
const iouReport: OnyxTypes.Report = {
...createRandomReport(1, undefined),
type: CONST.REPORT.TYPE.IOU,
};
const result = Report.moveIOUReportToPolicyAndInviteSubmitter(iouReport, undefined as unknown as OnyxTypes.Policy, (phone: string) => phone, {});
expect(result).toBeUndefined();
});

it('should return undefined when iouReport is missing', () => {
const policy: OnyxTypes.Policy = {...createRandomPolicy(1), role: CONST.POLICY.ROLE.ADMIN};
const result = Report.moveIOUReportToPolicyAndInviteSubmitter(undefined, policy, (phone: string) => phone, {});
expect(result).toBeUndefined();
});
});

describe('buildOptimisticChangePolicyData', () => {
Expand Down
34 changes: 30 additions & 4 deletions tests/unit/ReportActionsUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1151,12 +1151,12 @@ describe('ReportActionsUtils', () => {
});

it('should return false for a deleted IOU report action', () => {
const result = ReportActionsUtils.hasRequestFromCurrentAccount(deletedIOUReportID, currentUserAccountID);
const result = ReportActionsUtils.hasRequestFromCurrentAccount({reportID: deletedIOUReportID} as Report, currentUserAccountID);
expect(result).toBe(false);
});

it('should return true for an active IOU report action', () => {
const result = ReportActionsUtils.hasRequestFromCurrentAccount(activeIOUReportID, currentUserAccountID);
const result = ReportActionsUtils.hasRequestFromCurrentAccount({reportID: activeIOUReportID} as Report, currentUserAccountID);
expect(result).toBe(true);
});

Expand All @@ -1183,7 +1183,7 @@ describe('ReportActionsUtils', () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionFromOtherUser.transactionID}`, transactionFromOtherUser);

// Then: should return true
let result = ReportActionsUtils.hasRequestFromCurrentAccount(unloadedActionsReportID, currentUserAccountID);
let result = ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, currentUserAccountID);
expect(result).toBe(true);

// When: all transactions from the current user account have been deleted
Expand All @@ -1193,9 +1193,35 @@ describe('ReportActionsUtils', () => {
});

// Then: should return false
result = ReportActionsUtils.hasRequestFromCurrentAccount(unloadedActionsReportID, currentUserAccountID);
result = ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, currentUserAccountID);
expect(result).toBe(false);
});

it('should use explicit allReports parameter when provided and reportActions are unloaded', async () => {
const unloadedActionsReportID = '6';
const iouReport = {
type: CONST.REPORT.TYPE.IOU,
reportID: unloadedActionsReportID,
};
const transactionFromCurrentUser = {
...createRandomTransaction(1),
reportID: unloadedActionsReportID,
amount: -100,
};
const transactionFromOtherUser = {
...createRandomTransaction(2),
reportID: unloadedActionsReportID,
amount: 500,
};

// Set up transactions
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionFromCurrentUser.transactionID}`, transactionFromCurrentUser);
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionFromOtherUser.transactionID}`, transactionFromOtherUser);

// Pass report explicitly instead of relying on Onyx state
const result = ReportActionsUtils.hasRequestFromCurrentAccount(iouReport, currentUserAccountID);
expect(result).toBe(true);
});
});

describe('getLastVisibleAction', () => {
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11301,6 +11301,24 @@ describe('ReportUtils', () => {
const result = getReportOrDraftReport(mockReportID);
expect(result).toEqual(mockOnyxReport);
});

test('returns explicit report parameter instead of Onyx state', async () => {
const explicitReport: Report = {
...createRandomReport(mockReportIDIndex, undefined),
reportName: 'Explicit Report',
type: CONST.REPORT.TYPE.CHAT,
};
// Set a different report in Onyx to verify the explicit param takes precedence
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${mockReportID}`, mockOnyxReport);
const result = getReportOrDraftReport(mockReportID, undefined, undefined, undefined, explicitReport);
expect(result).toEqual(explicitReport);
expect(result).not.toEqual(mockOnyxReport);
});

test('returns undefined when explicit report is undefined and no Onyx state', () => {
const result = getReportOrDraftReport(mockReportID, undefined, undefined, undefined, undefined);
expect(result).toBeUndefined();
});
});

describe('buildOptimisticExpenseReport', () => {
Expand Down
Loading