Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransact
import useThemeStyles from '@hooks/useThemeStyles';
import {canIOUBePaid} from '@libs/actions/IOU/ReportWorkflow';
import {getPayMoneyOnSearchInvoiceParams, payMoneyRequestOnSearch} from '@libs/actions/Search';
import {isInvoiceReport} from '@libs/ReportUtils';
import {getReimbursableTotal, isInvoiceReport} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -76,7 +76,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall,
shouldUseShortForm
buttonSize={extraSmall ? CONST.DROPDOWN_BUTTON_SIZE.EXTRA_SMALL : CONST.DROPDOWN_BUTTON_SIZE.SMALL}
currency={currency}
formattedAmount={convertToDisplayString(Math.abs(iouReport?.total ?? 0), currency)}
formattedAmount={convertToDisplayString(Math.abs(getReimbursableTotal(iouReport)), currency)}
policyID={policyID || iouReport?.policyID}
iouReport={iouReport}
chatReportID={iouReport?.chatReportID}
Expand Down
50 changes: 21 additions & 29 deletions src/components/Search/SearchSelectionProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useEffect, useRef, useState} from 'react';
import {isMoneyRequestReport} from '@libs/ReportUtils';
import {getReimbursableTotal, isMoneyRequestReport} from '@libs/ReportUtils';
import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
import {hasValidModifiedAmount} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -44,35 +44,27 @@ function deriveSelectedReports(
}
return item.transactions.every(({keyForList}) => transactionIDs[keyForList]?.isSelected);
})
.map(
({
reportID,
action = CONST.SEARCH.ACTION_TYPES.VIEW,
total = CONST.DEFAULT_NUMBER_ID,
policyID,
allActions = [action],
currency,
chatReportID,
managerID,
ownerAccountID,
parentReportActionID,
parentReportID,
type,
}) => ({
reportID,
action,
total,
policyID,
allActions,
currency,
chatReportID,
managerID,
ownerAccountID,
parentReportActionID,
parentReportID,
type,
.map((item) => ({
reportID: item.reportID,
action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW,
// Prefer the freshly computed reimbursableTotal over the (sometimes stale) stored
// total column so bulk-pay and bulk-action summaries reflect the current sum of
// reimbursable transactions.
total: getReimbursableTotal({
total: item.total ?? CONST.DEFAULT_NUMBER_ID,
nonReimbursableTotal: item.nonReimbursableTotal,
reimbursableTotal: item.reimbursableTotal,
}),
);
policyID: item.policyID,
allActions: item.allActions ?? [item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW],
currency: item.currency,
chatReportID: item.chatReportID,
managerID: item.managerID,
ownerAccountID: item.ownerAccountID,
parentReportActionID: item.parentReportActionID,
parentReportID: item.parentReportID,
type: item.type,
}));
}
if (data.length && data.every(isTransactionListItemType)) {
return data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getBase62ReportID from '@libs/getBase62ReportID';
import {getReportName} from '@libs/ReportNameUtils';
import {isExpenseReport} from '@libs/ReportUtils';
import {getReimbursableTotal, isExpenseReport} from '@libs/ReportUtils';
import {
getAmount,
getConvertedAmount,
Expand Down Expand Up @@ -289,7 +289,7 @@ function TransactionItemRowWide({
reportID={transactionItem.reportID}
policyID={report?.policyID}
hash={transactionItem?.hash}
amount={report?.total}
amount={getReimbursableTotal(report)}
shouldDisablePointerEvents={isDisabled}
/>
)}
Expand Down
4 changes: 4 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@ function validateReportDraftProperty(key: keyof Report | keyof ReportNameValuePa
case 'unheldTotal':
case 'nonReimbursableTotal':
case 'unheldNonReimbursableTotal':
case 'reimbursableTotal':
case 'unheldReimbursableTotal':
case 'transactionCount':
return validateNumber(value);
case 'chatType':
Expand Down Expand Up @@ -615,6 +617,8 @@ function validateReportDraftProperty(key: keyof Report | keyof ReportNameValuePa
total: CONST.RED_BRICK_ROAD_PENDING_ACTION,
unheldTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION,
unheldNonReimbursableTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION,
reimbursableTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION,
unheldReimbursableTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION,
isWaitingOnBankAccount: CONST.RED_BRICK_ROAD_PENDING_ACTION,
isCancelledIOU: CONST.RED_BRICK_ROAD_PENDING_ACTION,
hasReportBeenRetracted: CONST.RED_BRICK_ROAD_PENDING_ACTION,
Expand Down
9 changes: 9 additions & 0 deletions src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,23 @@ function updateIOUOwnerAndTotal<TReport extends OnyxInputOrEntry<Report>>(
// Let us ensure a valid value before updating the total amount.
iouReportUpdate.total = iouReportUpdate.total ?? 0;
iouReportUpdate.unheldTotal = iouReportUpdate.unheldTotal ?? 0;
// IOU reports have no non-reimbursable transactions, so reimbursableTotal mirrors total optimistically.
iouReportUpdate.reimbursableTotal = iouReportUpdate.reimbursableTotal ?? iouReportUpdate.total;
iouReportUpdate.unheldReimbursableTotal = iouReportUpdate.unheldReimbursableTotal ?? iouReportUpdate.unheldTotal;

if (actorAccountID === iouReport.ownerAccountID) {
iouReportUpdate.total += isDeleting ? -amount : amount;
iouReportUpdate.reimbursableTotal += isDeleting ? -amount : amount;
if (!isOnHold) {
iouReportUpdate.unheldTotal += isDeleting ? -unHeldAmount : unHeldAmount;
iouReportUpdate.unheldReimbursableTotal += isDeleting ? -unHeldAmount : unHeldAmount;
}
} else {
iouReportUpdate.total += isDeleting ? amount : -amount;
iouReportUpdate.reimbursableTotal += isDeleting ? amount : -amount;
if (!isOnHold) {
iouReportUpdate.unheldTotal += isDeleting ? unHeldAmount : -unHeldAmount;
iouReportUpdate.unheldReimbursableTotal += isDeleting ? unHeldAmount : -unHeldAmount;
}
}

Expand All @@ -245,6 +252,8 @@ function updateIOUOwnerAndTotal<TReport extends OnyxInputOrEntry<Report>>(
iouReportUpdate.managerID = iouReport.ownerAccountID;
iouReportUpdate.total = -iouReportUpdate.total;
iouReportUpdate.unheldTotal = -iouReportUpdate.unheldTotal;
iouReportUpdate.reimbursableTotal = -iouReportUpdate.reimbursableTotal;
iouReportUpdate.unheldReimbursableTotal = -iouReportUpdate.unheldReimbursableTotal;
}

return iouReportUpdate;
Expand Down
76 changes: 66 additions & 10 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,8 @@ type OptimisticExpenseReport = Pick<
| 'unheldTotal'
| 'nonReimbursableTotal'
| 'unheldNonReimbursableTotal'
| 'reimbursableTotal'
| 'unheldReimbursableTotal'
| 'parentReportID'
| 'created'
| 'lastVisibleActionCreated'
Expand All @@ -416,6 +418,8 @@ type OptimisticNewReport = Pick<
| 'currency'
| 'total'
| 'nonReimbursableTotal'
| 'reimbursableTotal'
| 'unheldReimbursableTotal'
| 'parentReportID'
| 'created'
| 'lastVisibleActionCreated'
Expand Down Expand Up @@ -820,6 +824,8 @@ type OptimisticIOUReport = Pick<
| 'unheldTotal'
| 'nonReimbursableTotal'
| 'unheldNonReimbursableTotal'
| 'reimbursableTotal'
| 'unheldReimbursableTotal'
| 'reportName'
| 'parentReportID'
| 'created'
Expand Down Expand Up @@ -4333,6 +4339,30 @@ function requiresAttentionFromCurrentUser(
return !!getReasonAndReportActionThatRequiresAttention(optionOrReport, currentUserLogin, currentUserAccountID, parentReportAction, isReportArchived);
}

/**
* Returns the freshly computed reimbursableTotal from the backend, falling back to the legacy
* derivation (`total - nonReimbursableTotal`) when the local copy of the report does not yet have
* the fresh field set.
*/
function getReimbursableTotal(report: OnyxInputOrEntry<Report> | Pick<Report, 'total' | 'nonReimbursableTotal' | 'reimbursableTotal'> | undefined): number {
if (!report) {
return 0;
}
return report.reimbursableTotal ?? (report.total ?? 0) - (report.nonReimbursableTotal ?? 0);
}

/**
* Returns the freshly computed unheldReimbursableTotal from the backend, falling back to the legacy
* derivation (`unheldTotal - unheldNonReimbursableTotal`) when the local copy of the report does
* not yet have the fresh field set.
*/
function getUnheldReimbursableTotal(report: OnyxInputOrEntry<Report> | Pick<Report, 'unheldTotal' | 'unheldNonReimbursableTotal' | 'unheldReimbursableTotal'> | undefined): number {
if (!report) {
return 0;
}
return report.unheldReimbursableTotal ?? (report.unheldTotal ?? 0) - (report.unheldNonReimbursableTotal ?? 0);
}

/**
* Checks if the report contains at least one Non-Reimbursable transaction
*/
Expand All @@ -4352,9 +4382,13 @@ function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry<Report>, searchR
}
if (moneyRequestReport) {
let nonReimbursableSpend = moneyRequestReport.nonReimbursableTotal ?? 0;
let totalSpend = moneyRequestReport.total ?? 0;
// Prefer the freshly computed reimbursableTotal from the backend over deriving from the stored
// total column (which can be stale). Fall back to total - nonReimbursableTotal when the fresh
// field is not yet present on the local copy of the report.
const reimbursableSpendStored = getReimbursableTotal(moneyRequestReport);
let totalSpend = reimbursableSpendStored + nonReimbursableSpend;

if (nonReimbursableSpend + totalSpend !== 0) {
if (totalSpend !== 0) {
// There is a possibility that if the Expense report has a negative total.
// This is because there are instances where you can get a credit back on your card,
// or you enter a negative expense to "offset" future expenses
Expand Down Expand Up @@ -6577,6 +6611,8 @@ function buildOptimisticIOUReport(
unheldTotal: total,
nonReimbursableTotal: 0,
unheldNonReimbursableTotal: 0,
reimbursableTotal: total,
unheldReimbursableTotal: total,

// We don't translate reportName because the server response is always in English
reportName: `${payerEmail} owes ${formattedTotal}`,
Expand Down Expand Up @@ -6608,10 +6644,15 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe

const createdDate = report.lastVisibleActionCreated ? new Date(report.lastVisibleActionCreated) : undefined;

const totalAmount = report.total !== undefined && !Number.isNaN(report.total) ? Math.abs(report.total) : 0;
const nonReimbursableTotal =
'nonReimbursableTotal' in report && report.nonReimbursableTotal !== undefined && !Number.isNaN(report.nonReimbursableTotal) ? Math.abs(report.nonReimbursableTotal) : 0;
const reimbursableAmount = totalAmount - nonReimbursableTotal;
const reportTotal = report.total ?? undefined;
const totalAmount = reportTotal !== undefined && !Number.isNaN(reportTotal) ? Math.abs(reportTotal) : 0;
const reportNonReimbursableTotal = 'nonReimbursableTotal' in report ? (report.nonReimbursableTotal ?? undefined) : undefined;
const nonReimbursableTotal = reportNonReimbursableTotal !== undefined && !Number.isNaN(reportNonReimbursableTotal) ? Math.abs(reportNonReimbursableTotal) : 0;
// Prefer the freshly computed reimbursableTotal from the backend over deriving it from the stored
// total column (which can be stale).
const reportReimbursableTotal = 'reimbursableTotal' in report ? (report.reimbursableTotal ?? undefined) : undefined;
const freshReimbursableTotal = reportReimbursableTotal !== undefined && !Number.isNaN(reportReimbursableTotal) ? Math.abs(reportReimbursableTotal) : undefined;
const reimbursableAmount = freshReimbursableTotal ?? totalAmount - nonReimbursableTotal;

const result = formula
// We don't translate because the server response is always in English
Expand Down Expand Up @@ -6790,6 +6831,8 @@ function buildOptimisticExpenseReport({
unheldTotal: storedTotal,
nonReimbursableTotal: storedNonReimbursableTotal,
unheldNonReimbursableTotal: storedNonReimbursableTotal,
reimbursableTotal: storedTotal - storedNonReimbursableTotal,
unheldReimbursableTotal: storedTotal - storedNonReimbursableTotal,
participants: {
[payeeAccountID]: {
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
Expand Down Expand Up @@ -6840,6 +6883,8 @@ function buildOptimisticEmptyReport(
statusNum,
total: 0,
nonReimbursableTotal: 0,
reimbursableTotal: 0,
unheldReimbursableTotal: 0,
participants: {},
created: timeOfCreation,
lastVisibleActionCreated: timeOfCreation,
Expand Down Expand Up @@ -10891,11 +10936,20 @@ function getNonHeldAndFullAmount(iouReport: OnyxEntry<Report>, shouldExcludeNonR
// if the report is an expense report, the total amount should be negated
const coefficient = isExpenseReport(iouReport) ? -1 : 1;

let total = iouReport?.total ?? 0;
let unheldTotal = iouReport?.unheldTotal ?? 0;
// Prefer the freshly computed totals from the backend. The stored total column can lag behind the
// sum of the underlying transactions, so deriving from reimbursableTotal/nonReimbursableTotal keeps
// displayed amounts in sync with what the user actually entered.
const reimbursableTotal = getReimbursableTotal(iouReport);
const unheldReimbursableTotal = getUnheldReimbursableTotal(iouReport);

let total: number;
let unheldTotal: number;
if (shouldExcludeNonReimbursables) {
total -= iouReport?.nonReimbursableTotal ?? 0;
unheldTotal -= iouReport?.unheldNonReimbursableTotal ?? 0;
total = reimbursableTotal;
unheldTotal = unheldReimbursableTotal;
} else {
total = reimbursableTotal + (iouReport?.nonReimbursableTotal ?? 0);
unheldTotal = iouReport?.unheldTotal ?? unheldReimbursableTotal + (iouReport?.unheldNonReimbursableTotal ?? 0);
}

const adjustedUnheldTotal = unheldTotal * coefficient;
Expand Down Expand Up @@ -13309,6 +13363,8 @@ export {
getLastVisibleMessage,
getMoneyRequestSpendBreakdown,
getNonHeldAndFullAmount,
getReimbursableTotal,
getUnheldReimbursableTotal,
getOptimisticDataForAncestors,
getOriginalReportID,
getOutstandingChildRequest,
Expand Down
15 changes: 3 additions & 12 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ import {
getMoneyRequestSpendBreakdown,
getPersonalDetailsForAccountID,
getPolicyName,
getReimbursableTotal,
getReportOrDraftReport,
getReportStatusTranslation,
getSearchReportName,
Expand Down Expand Up @@ -3925,18 +3926,8 @@ function getSortedReportData(

if (sortBy === CONST.SEARCH.TABLE_COLUMNS.REIMBURSABLE_TOTAL) {
return data.sort((a, b) => {
const aTotal = a.total;
const bTotal = b.total;

const aNonReimbursableTotal = a.nonReimbursableTotal;
const bNonReimbursableTotal = b.nonReimbursableTotal;

if (aTotal == null || bTotal == null || aNonReimbursableTotal == null || bNonReimbursableTotal == null) {
return 0;
}

const aValue = aTotal - aNonReimbursableTotal;
const bValue = bTotal - bNonReimbursableTotal;
const aValue = getReimbursableTotal(a);
const bValue = getReimbursableTotal(b);
return compareValues(aValue, bValue, sortOrder, sortBy, localeCompare);
});
}
Expand Down
12 changes: 12 additions & 0 deletions src/libs/actions/IOU/DeleteMoneyRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ function prepareToCleanUpMoneyRequest(
updatedIOUReport.nonReimbursableTotal += nonReimbursableAmountDiff;
}

if (transaction?.reimbursable && typeof updatedIOUReport.reimbursableTotal === 'number') {
const reimbursableAmountDiff =
getAmount(transaction, true) + (transactionPendingDelete?.reduce((prev, curr) => prev + (curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0);
updatedIOUReport.reimbursableTotal += reimbursableAmountDiff;
}

if (!isTransactionOnHold) {
if (typeof updatedIOUReport.unheldTotal === 'number') {
updatedIOUReport.unheldTotal += unheldAmountDiff;
Expand All @@ -183,6 +189,12 @@ function prepareToCleanUpMoneyRequest(
(transactionPendingDelete?.reduce((prev, curr) => prev + (!isOnHold(curr) && !curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0);
updatedIOUReport.unheldNonReimbursableTotal += unheldNonReimbursableAmountDiff;
}

if (transaction?.reimbursable && typeof updatedIOUReport.unheldReimbursableTotal === 'number') {
const unheldReimbursableAmountDiff =
getAmount(transaction, true) + (transactionPendingDelete?.reduce((prev, curr) => prev + (!isOnHold(curr) && curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0);
updatedIOUReport.unheldReimbursableTotal += unheldReimbursableAmountDiff;
}
}
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/libs/actions/IOU/Duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
buildTransactionThread,
canAddTransaction,
generateReportID,
getReimbursableTotal,
getTransactionDetails,
} from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
Expand Down Expand Up @@ -213,18 +214,21 @@ function mergeDuplicates({
}, 0);

const expenseReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`];
const previousReimbursableTotal = getReimbursableTotal(expenseReport);
const expenseReportOptimisticData: OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT> = {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`,
value: {
total: (expenseReport?.total ?? 0) - duplicateTransactionTotals,
reimbursableTotal: previousReimbursableTotal - duplicateTransactionTotals,
},
};
const expenseReportFailureData: OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT> = {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`,
value: {
total: expenseReport?.total,
reimbursableTotal: previousReimbursableTotal,
},
};

Expand Down
Loading
Loading